字符串匹配

本文深入探讨了字符串匹配算法,包括KMP算法、Rabin-Karp算法和AC自动机等。详细介绍了KMP算法的原理及其改进,以及AC自动机在多串匹配中的应用,并附带模板代码。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

这里简要的总结一下字符串匹配算法,包括KMP,Rabin_Karp,和AC自动机

KMP

这个算法利用的是子串前缀的信息,对于模式串,P,与文本串T,而言当我们匹配到P[q]与T[i]的时候,我们可以知道,此时p[0,…,q-1]一定已经与T[i-q+1,…i-1]是匹配好了的,所以我们移动子串的时候就不必用暴力的方法一个一个的移动,而是移动到
PkPq1 P k ⊂ P q − 1 即P[0,…k]为q-1的后缀用k+1去与T[i]比较,因为P[0,…,k]肯定匹配了P[q-1]的后缀,即T[i]的后缀。

构造f

下面我们说明对于模式串的每一个元素 q q ,预处理出满足PkPq1的最大的 K K
如果已经知道f[q-1] (q-1对应的K值)为k,那么,只要P[q] == P[k+1],f[q] = k+1;同时把匹配光标k,前移动一位。这是比较好理解的。
如果说没有匹配呢,因为我们已经知道f[q-1]对应的k值所以我们可以将当前匹配的光标回退到f[q-1]直到P[q]与P[k+1]匹配为止。
我们将0处的k值定义为-1,因此总会到达一个”匹配的位置”

void KMP_next( char * P,int *f,int n =-1)
{
    int m = (n==-1)?strlen(P):n;
    int k = f[0] = -1;//当前匹配下标
    for(int q = 1 ; q<m ; ++q)
    {
        while(k >-1&& P[k+1]!=P[q])
            k = f[k];
        f[q] = P[k+1]==P[q]?++k:k;
    }
}

匹配算法

/*
*找出模式串第一次出现的下标
*/
int  KMP(int * T,int* P,int* f)
{
    int n = strlen(T);
    int m = strlen(P);
    int j = -1;//当前匹配的下标位置
    for(int i=0 ; i<n ; ++i)
    {
        while(j>=0 && P[j+1]!=T[i])j = f[j];
        if(P[j+1]==T[i])++j;
        if(j==m-1)return i-m+1;
    }
    return -1;
}

不难发现这和预处理的过程非常的相似,因为预处理过程就是在用自己匹配自己。
复杂度
O(n+m)

改进

由上面的预处理函数我们已经知道,当在 P[q+1] P [ q + 1 ] T[i] T [ i ] 匹配失败的时候下一次匹配的位置应该是 P[f[q]+1] P [ f [ q ] + 1 ] ,如果说我们已经知道了 P[f[q]+1] P [ f [ q ] + 1 ] P[q+1] P [ q + 1 ] 是一样的那我们知道这一次比较一定是徒劳的,他至少应该回到 f[f[q]]+1 f [ f [ q ] ] + 1 (建议画图理解)

改进的KMP中 nt[i] n t [ i ] 表示的是失配函数,即: P[i]!=T[j] P [ i ] ! = T [ j ] i i 应该去的地方在串 P 中表示, P[0....nt[i]1]=P[...i1] P [ 0.... n t [ i ] − 1 ] = P [ . . . i − 1 ] , 就是避免重复匹配

void ex_KMP_preprocess(const char pat[]) {
    // pat[0....nt[i]-1] = pat[i-1], nt[i] 是 i 失败后应该转移的地方
    int q=1;
    nt[q] = 0;
    int k=0;
    while (pat[q]) {
        if(k==0 || pat[k]==pat[q])//j=0 通配符
        {
            ++k;++q;
            nt[q] = (pat[k]!=pat[q]? k :  nt[k] );
        }else k = nt[k];
    }
}

int KMP(const char pat[],const char T[]){
    int i =1,j=0;
    int len_pat  = strlen(pat+1);
    while (T[i]) {
        if(j==0 || pat[j]== T[i]){
            ++i;++j;
            if(j==len_pat+1)return i-len_pat;
        }else j = nt[j];
    }
}

f最长匹配前缀的性质

对于串 P[0,...,n1] P [ 0 , . . . , n − 1 ] 我们一定有

P[i]=P[i+n1f[n1]] P [ i ] = P [ i + n − 1 − f [ n − 1 ] ]

即对于 0,...,f[n1] 0 , . . . , f [ n − 1 ] 我们有他的周期一定是n-1-f[n-1],对于字符串来说 n1f[n1] n − 1 − f [ n − 1 ] 就是该字符串的最短循环节!
The Minimum Length

重复因子

对于串 x=yr x = y r 我们定义他的重复因子为 r r x y y 重复r次拼接而成。则由上面的结论,一定有

r(i1f[i1])=i r ∗ ( i − 1 − f [ i − 1 ] ) = i

例题:
poj 2406
基于重复因子Galil 和 Seifras提出了只需要 O(1) O ( 1 ) 额外空间的匹配方法
Galil & Seifras

AC自动机

这个是一个由Aho-Corasick发明的一个多串匹配的算法,简单的讲就是在Trie上的KMP,至于KMP中的失配函数则是用BFS来进行构建的
这里不准备详细讲解AC自动机算法,因为没人能比他自己的论文讲的明白,有兴趣的读者请移步:
Efficient
String Matching:
An Aid to
Bibliographic Search
Alfred V. Aho and Margaret J. Corasick

这里仅对他的实现做一个简单总结

Trie中的数据说明

struct ACTrie
{

    int m;//模式串个数
    int next[max_node][Sigma_size];//状态的转移,已经优化之后的
    int f[max_node];//匹配失败函数,即后缀边
    int val[max_node];//每个字符串的末节点处的值(即作为 (string,val)对)
    const int fail = -1;//AC论文中的失配标记
    int size;//状态总数
    void init()//初始化
    {
        val[0] = fail;memset(next[0],fail,sizeof(next[0]));size = 1;
    }
    int new_node()//新增加一个状态节点
    {
        memset(next[size],fail,sizeof(next[size]));
        val[size]= fail;
        size++;
        return size-1;
    }
    void add(char * s,int id)//存储字符串编号可用于在文本串中输出
    {
        int state = 0,n = strlen(s);
        for(int i=0 ; i<n ; ++i)
        {
            int id = IDX(s[i]);
            if(next[state][id]==fail)
                next[state][id] = new_node();
            state =next[state][id];
        }
        val[state] =id;
    }
};

失配函数,next表

void make_fail()
    {
        queue<int> Q;f[0] =0;
        for(int i=0 ; i<Sigma_size ;++i)
        if(next[0][i]!=fail){f[next[0][i]] = 0;Q.push(next[0][i]);}//depth =0的状态
        else{next[0][i]= 0;}//空状态的下一个状态为0
        while(!Q.empty())
        {
            int r = Q.front();Q.pop();
            for(int i=0 ; i<Sigma_size ; ++i)
            {
                int s = next[r][i];
                if(s!=fail){
                    Q.push(s);
                    f[s] = next[f[r]][i];
                }else next[r][i] = next[f[r]][i];//把无效比较进行压缩,对应AC论文中说的优化,
            }
        }
    }

这里所做的优化和KMP是一样的应为我们实际可以从模式串中推断出有些匹配肯定会无效

匹配函数

void  match(char * T)
    {
        int n = strlen(T);
        int state= 0;
        for(int i=0 ; i<n ; ++i)
        {
            int c = IDX(T[i]);
            state = next[state][c];
            int temp = state;
            while(val[temp]!=fail)//沿着后缀边一直找子串
            {
                cnt[val[temp]]++;
                temp = f[temp];
            }
        }
    }

上述匹配模式不是必须的要看你计算的是什么,如果是简单统计每个串的个数,就可以这样做

来几道模板题压压惊

hdu 3065
给你m个子串让你求出每个串在模式串中出现的次数
完整AC代码

#include <iostream>
#include<cstring>
#include<cstdio>
#include<queue>
#include<algorithm>
#define maxn 2000009
#define IDX(x) ((x))
#define Sigma_size 128
#define max_node 50010
using namespace std;


char T[maxn];
char P[1010][60];
char cnt[1010];
struct ACTrie
{

    int m;//模式串个数
    int next[max_node][Sigma_size];
    int f[max_node];//匹配失败函数
    int val[max_node];
    const int fail = -1;
    int size;
    void init()
    {
        val[0] = fail;memset(next[0],fail,sizeof(next[0]));size = 1;
    }
    int new_node()
    {
        memset(next[size],fail,sizeof(next[size]));
        val[size]= fail;
        size++;
        return size-1;
    }
    void add(char * s,int id)//存储字符串编号可用于在文本串中输出
    {
        int state = 0,n = strlen(s);
        for(int i=0 ; i<n ; ++i)
        {
            int id = IDX(s[i]);
            if(next[state][id]==fail)
            {
                next[state][id] = new_node();
            }
            state =next[state][id];
        }
        val[state] =id;
    }
    void make_fail()
    {
        queue<int> Q;f[0] =0;
        for(int i=0 ; i<Sigma_size ;++i)
        if(next[0][i]!=fail){f[next[0][i]] = 0;Q.push(next[0][i]);}//depth =0的状态
        else{next[0][i]= 0;}
        while(!Q.empty())
        {
            int r = Q.front();Q.pop();
            for(int i=0 ; i<Sigma_size ; ++i)
            {
                int s = next[r][i];
                if(s!=fail){
                    Q.push(s);
                    f[s] = next[f[r]][i];
                }else next[r][i] = next[f[r]][i];
            }
        }
    }
    void  match(char * T)
    {

        int n = strlen(T);
        int state= 0;
        for(int i=0 ; i<n ; ++i)
        {
            int c = IDX(T[i]);
            state = next[state][c];
            int temp = state;
            while(val[temp]!=fail)
            {
                cnt[val[temp]]++;
                temp = f[temp];
            }
        }
    }

};

ACTrie ac;
int main()
{
    //freopen("H:\\c++\\file\\stdin.txt","r",stdin);
    while(scanf("%d",&ac.m)!=EOF)
    {
        ac.init();
        memset(cnt,0,sizeof(cnt[0])*(ac.m+1));
        for(int i=1  ;i<=ac.m ; ++i)
        {
            scanf("%s",P[i]);
            ac.add(P[i],i);
        }
        ac.make_fail();
        scanf("%s",T);
        ac.match(T);
        for(int i=1 ; i<=ac.m ;++i )
        {
            if(cnt[i])
            {
                printf("%s: %d\n",P[i],cnt[i]);
            }
        }
    }
    return 0;
}

hdu 2222
这里要求的只是模式串中出现了几个,而且模式串可能会重复!

hdu 2896模板题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值