KMP算法

1.KMP算法的一些例题

KMP算法经常考察我们对于KMP算法实质的理解,衍生出来的问题有:循环节问题、匹配长度问题等。
AcWing 141.周期
这个题目是我写了KMP模板后做的第一道KMP的题。那么这题的思路是什么呢?其实如果我们了解了KMP中的next数组的本质后这题就不再困难了。next数组的本质就是求出模式串的所有前缀的最长公共前后缀。那么就会有如下的性质:对于任意的next[i],a[1 ~ i-next[i]]=a[i-next[i]+1 ~ 2i-2next[i]]=……(这个结论可以通过对应相等的方法证明),那么这样,如果(i-ne[i])|i,那么a[1~i-next[i]]就是a[i]的循环节,并且是最小循环节,符合题意。
代码如下,可供复习:

#include<iostream>
#include<cstdio>
using namespace std;
const int N=1000005;
int ne[N];
char a[N];
int cnt;
int main(){
    int n;
    while(cin>>n>>a+1 && n){
        printf("Test case #%d\n",++cnt);
        for(int i=2,j=0;i<=n;i++){
            while(j && a[i]!=a[j+1]) j=ne[j];
            if(a[i]==a[j+1]) j++;
            ne[i]=j;
        }
        for(int i=2;i<=n;i++){
            if(i%(i-ne[i])==0 && i/(i-ne[i])>1){
                printf("%d %d\n",i,i/(i-ne[i]));
            }
        }
        puts("");
    }
}

AcWing 159.奶牛矩阵
这题不就是一个二维的KMP吗?
那么有了这个大胆猜想,那么我们想一想怎么做:不难猜想到分别处理出每一行每一列的所有循环节(不一定要能整除R或C),那么这个可以根据上一题的思路轻松解决。然后就是在所有行的循环节长度中找出一个共有的长度l,也就是说找出每一行都有的一个长度为l的循环节,并且使得这个l的值最小。同理在列中也找出这样的一个最小的h,最终l*h就是答案。但是证明的话还是自己要去想明白。

#include<iostream>
using namespace std;
const int N=1e4+5,M=100;
int n,m;
char a[N][M];
int ner[N][M],nec[M][N];//ner表示一行的next数组,nec表示一列的next数组
int sr[N][M],sc[M][N];
int main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        cin>>a[i]+1;
    }
    for(int k=1;k<=n;k++){
        for(int i=2,j=0;i<=m;i++){
            while(j && a[k][i]!=a[k][j+1]) j=ner[k][j];
            if(a[k][i]==a[k][j+1]) j++;
            ner[k][i]=j;
        }
    }
    for(int k=1;k<=m;k++){
        for(int i=2,j=0;i<=n;i++){
            while(j && a[i][k]!=a[j+1][k]) j=nec[k][j];
            if(a[i][k]==a[j+1][k]) j++;
            nec[k][i]=j;
        }
    }
    for(int i=1;i<=n;i++){
        int j=ner[i][m];
        while(j){//这样枚举得出的sr数组具有单调性(递减),所以想到用二分来解决
            sr[i][++sr[i][0]]=j;
            j=ner[i][j];
        }
    }
    for(int i=1;i<=m;i++){
        int j=nec[i][n];
        while(j){
            sc[i][++sc[i][0]]=j;
            j=nec[i][j];
        }
    }
    int len=0;
    for(int i=1;i<=sr[1][0];i++){
        bool flag=true;
        for(int j=2;j<=n;j++){
            int l=1,r=sr[j][0];
            while(l<r){//利用二分查找,看循环节的长度是否在所有的行中都出现过。
                int mid=(l+r+1)>>1;
                if(sr[j][mid]>=sr[1][i]) l=mid;
                else r=mid-1;
            }
            if(sr[j][l]!=sr[1][i]){
                flag=false;
                break;
            }
        }
        if(flag){
            len=sr[1][i];
            break;
        }
    }
    int h=0;
    for(int i=1;i<=sc[1][0];i++){
        bool flag=true;
        for(int j=2;j<=m;j++){
            int l=1,r=sc[j][0];
            while(l<r){
                int mid=(l+r+1)>>1;
                if(sc[j][mid]>=sc[1][i]) l=mid;
                else r=mid-1;
            }
            if(sc[j][l]!=sc[1][i]){
                flag=false;
                break;
            }
        }
        if(flag){
            h=sc[1][i];
            break;
        }
    }
    cout<<(m-len)*(n-h)<<endl;//记住最后循环节的长度是n-ne[n]
    return 0;
}

同样利用这个原理,可以写出一份更加简洁的代码:
思路:在这个 n∗m 的矩阵中,把每一行看成一个整体,对行求一个不一定要整除的最小循环节,长度为 k。再在这个 k∗m 的子矩阵中,把每一列看成一个整体,对列求一个不一定要整除的最小循环节,长度为 p。答案即为(k∗p)。
by pengyule
代码如下:

#include <bits/stdc++.h>
using namespace std;
string s[10005],_s[10005];
int nxt[10005];
int main()
{
    int n,m;
    cin>>n>>m;
    for(int i=0;i<n;i++) cin>>s[i];
    nxt[0]=-1;
    for(int i=1;i<=n;i++){
        int j=nxt[i-1];
        while(j>=0 && s[i-1]!=s[j]) j=nxt[j];
        nxt[i]=j+1;
    }
    int k=n-nxt[n];
    for(int i=0;i<m;i++)
        for(int j=0;j<k;j++)
            _s[i]+=s[j][i];
    for(int i=0;i<m;i++) s[i]=_s[i];
    for(int i=1;i<=m;i++){
        int j=nxt[i-1];
        while(j>=0 && s[i-1]!=s[j]) j=nxt[j];
        nxt[i]=j+1;
    }
    int ans=(m-nxt[m])*k;
    cout<<ans<<endl;
    return 0;
}

AcWing 160.匹配统计
这题如果能想到用哈希的话其实相当简单,只要对于A的每一个后缀,用二分来求出它与B的最大匹配长度即可。总的时间复杂度为O(NlogN+M+Q)。由于这篇blog讲的是KMP算法,所以我们介绍一下KMP的解法:
这题还是考察对于KMP算法实质的理解:
在这里插入图片描述
在这里插入图片描述
上述图片节选自垫底抽风大佬的题解。(大佬写得还是很好啊,非常易懂)
这样的话,时间复杂度就降低到了O(N+M+Q),码量也大大减少了。

2.最小表示法

一个字符串的最小表示的定义:见《算法进阶指南》P75-76。 那么它的求法与KMP算法在原理上没有太大的联系,把它放到这里来是因为它们都是字符串匹配的内容。
算法流程:见《算法进阶指南》P76。
代码如下:

int n = strlen(s + 1);
for (int i = 1; i <= n; i++) s[n+i] = s[i];
int i = 1, j = 2, k;
while (i <= n && j <= n) {
    for (k = 0; k < n && s[i+k] == s[j+k]; k++);
    if (k == n) break;//说明扫完了字符串的循环节,现已找到最小表示
    if (s[i+k] > s[j+k]) {
        i = i + k + 1;
        if (i == j) i++;
    } else {
        j = j + k + 1;
        if (i == j) j++;
    }
}
ans = min(i, j);

例题:AcWing 158.项链
这个题目初看起来像是这一题:AcWing 137.雪花雪花雪花。想到利用哈希求出一条项链所有可能的哈希值取最小,然后与另一条比较。这样虽然可以解决判断二者是否相等的问题,但却无法解决求出最小表示的问题。所以我们要采用最小表示法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值