当前位置: 首页 > article >正文

(K)MP有限状态自动机

模式匹配自动机

什么是有限状态自动机?
定义 n 个不同状态,记为 {1,2…n},在状态 i 时输入 s,达到状态 j,记为 goto (i,s)=j
对于字符串 s 而言,在一个状态 i 下输入一个字符 ch,也会达到一个指定状态 :
假定新的状态为串 s [1,i]+ch 的最长相等前后缀 ,便能够用这个状态机模拟 KMP 算法匹配字符串的过程。
当字符集仅为 a、b 时,有:


其中 goto (4,a)=3,也就是说 abab+a 的最长相等前后缀 对应的状态是状态 3 ,也即表示字符串 “aba” 的状态。
似乎这样就足够了。
我们获得了 goto 函数,定义为:

goto (Si,a):串 s [1,i] a 的最长相等前后缀。

为了得到这个 goto 函数的值,我们需要定义 fail 函数:

fail (Si): 串 s [1,i] 的最长相等前后缀。

因为得到 goto (i,a) 的前提是,知道 s [1,i] 的最长相等前后缀 s\[1,j] :若 s [j+1] 与 a 相同,则 goto (i,a)=j+1,否则求 s [1,j] 的最长相等前后缀,直到长度为 0。
为了表示 “s [j+1] 与 a 相同” 这一条件,定义函数:

follow (Si, a): 状态 Si 输入 a 后,来到下一个状态。

对于字符串 abcde,follow (0,a)=1,follow (1,b)=2,follow (2,c)=3… 以此类推,而其他值未定义。
到这里,goto 函数就可表示为:

state go_to(state s,char ch){

    while(follow(s,ch)未定义)
    {
        s=fail[s];
    }
    return follow(s,ch);
}

若 s 为模式串的状态,ch 为 s 的后继字符,则这一 goto 值可当做新的 fail 值。

未定义状态,比如 follow (0,b), 计为 0 可不可行?
与之配套地,fail (0), 计为 0,也就是说空串的最长相等前后缀长度 为 0。
若 fail (0) 记为 - 1,则 follow (s==-1,ch) 将陷入故障状态:没有状态被记为 - 1。
问题出现了!函数不得不进入死循环:因为 s 一直为 0。
破环方式也很简单:

引入状态 - 1,未定义状态记为 - 1,fail (0)=-1,follow (-1, 任何字符)=0。

这样,当计算 ab+c 的最长相等前后缀 时,便能够得到 go_to (2,c)=0。
类似地,计算fail数组的函数为:

Compute_fail()
{
    fail(s0) = ⊥;
    s = s0;
    for( i =1 to |P| ){
        s = goto(s, P[i]);
        fail(si) = s ;
    }
}

goto 和 fail 数组的关系:fail 反映模式串中的某部分字符串的最长相等前后缀 ,goto 反映文本串和模式串的匹配情况。诚然,fail 数组可以通过 goto 函数得到,但记录一些中间状态有利于加速算法。

匹配函数:

Match(t)
{
    s= s0;
    for(int i=1 to |T|){
        if( s 是终止状态 )
           return 匹配!
        else  
            s=goto(s,T[i]);
    }
} 

MP 有限状态自动机

我们都知道 mp 的 c++ 写法。
基于以上定义,我们艰难地知道 mp 的有限状态自动机写法:

#include<bits/stdc++.h>
using namespace  std;
#define state int
string P; //模式串
string T;
state fail[1000005];
state edge[1000005][26];

state follow(state s,char ch){
    if(s==-1) return 0;
    if(edge[s][ch-'A']==s+1) return s+1;

     return -1;
}
state go_to(state s,char ch){

    while(follow(s,ch)==-1)
    {
        s=fail[s];
    }
    return follow(s,ch);
}

void get_fail(){
    fail[0]=-1;
    state s_=0;
    for(int i=1;i<P.size();i++){
        s_=go_to(s_,P[i]);
        fail[state(i)]=s_;
        
           if(fail[s_]!=-1&&P[s_+1]-'A'==P[i+1]-'A'){
            fail[state(i)]=fail[s_];
        }//!!!K优化!!!
        
        edge[i-1][P[i] - 'A'] = i;
    }
}
void match(){
    state s_=0;
    for(int i=1;i<T.size();i++){
        s_=go_to(s_,T[i]);
        if(s_==state(P.size()-1)){
            cout<<i-P.size()+2<<endl;

        }
    }
}
signed main() {

    cin>>T>>P;
    P=" "+P;
    T=" "+T;

    get_fail();

    match();

for(int i=1;i<P.size();i++){
    cout<<fail[i]<<" ";
}

}

洛谷提交情况如下: 洛谷
这是一种没有任何实战意义的写法。
需要注意俩点:

if(edge[s][ch-‘A’]==s+1) return s+1;

只有计算 fail 函数时,遍历过某个字符时,才连一条 edge 边。
也就是说,在未遍历时,字符串 abc 的 follow (0,a)=-1,follow (1,b)=-1,follow (1,c)=-1, 而当遍历 b 时,follow (0,a)=1,follow (1,b)=2,follow (1,c)=-1。这样做的原因是,若模式串天然有 follow 边,则 fail 数组的值会依次为 - 1,1,2,3,4…

if(fail[s_]!=-1&&P[s_+1]-‘A’==P[i+1]-‘A’){
fail[state(i)]=fail[s_]; }

这是 knuth 优化。对于字符串 aaaa,mp 的 fail 数组是 0,1,2,3 而 kmp 的 fail 数组是 0,0,0,3。
因为 kmp 的 fail 数组不能很好地反映字符串的前后缀的关系,而我们通常需要利用这种关系,故现常用 mp,且把 mp 称为 kmp。

MP 算法是一个 O (m+n) 的算法,证明如下:

1. 在 check 函数中,对文本串扫描一遍,无回头扫描,消耗 O (n)
2. 自动机向右的移动距离 >= 向左移动的距离 >= 调用 fail 的次数,而向右的移动距离 = 对文本串扫描的距离 = n,故调用 fail 的次数 = O (n)
3. 构造 fail 数组时,向右的移动距离 = 对模式串扫描的距离 = m,即 Fail 构造复杂度的复杂度为 O (m)

综合为 O (m+n)。实际上,除了 aaab 匹配 aaaaaaaa 这种极端数据外,mp 和暴力算法复杂度接近:随机情况下,暴力的复杂度也接近 O (m+n),在数据随机生成的情况下,暴力匹配也基本很快就会失配。


http://www.kler.cn/news/359589.html

相关文章:

  • VSCode中使用 Live Server 扩展时设置默认浏览器
  • 【Java后端】之 ThreadLocal 详解
  • 内核调度hh
  • 搜维尔科技:xsens动作捕捉+manus VR数据手套,元宇宙数字人制作流程
  • 开源限流组件分析(二):uber-go/ratelimit
  • 【机器学习】支持向量机SVM|高斯核 讲解及代码实现
  • RAG进阶形态之GraphRAG
  • 适合学生党的平价蓝牙耳机有哪些?四款便宜又好的蓝牙耳机盘点
  • RGB-D摄像头三维重建
  • 【景观生态学实验】实验一 ArcGIS地理数据处理及制图基础
  • Synopsys工具中命令中filter选项
  • Databend 产品月报(2024年9月)
  • 笑脸漏洞复现
  • RuoYi-Vue若依 环境搭建 速成
  • Android Automotive 获得谷歌地图事故报告功能
  • 数据轻松上云——Mbox边缘计算网关
  • 同济子豪兄--随机游走的艺术-图嵌入表示学习【斯坦福CS224W图机器学习】
  • 旋转花键材质及运用场景
  • 02电力电子技术简介
  • R数据科学1.9练习题