kmp 与 扩展kmp

1.kmp字符串匹配

将一文本串(母串)与一模式串(子串)进行匹配,输出所有匹配位置,暴力是O(n^2),而kmp算法把它优化到了O(n)。

模板题链接:【模板】KMP字符串匹配 - 洛谷 

代码:

#include <bits/stdc++.h>
using namespace std;
char s[1000005], t[1000005];
int ne[1000005];
int main(){
	ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
	cin>>(s+1)>>(t+1); int l1=strlen(s+1), l2=strlen(t+1);
	for(int i=2,j=0; i<=l2; i++){ //get_ne of t
		while(j && t[i]!=t[j+1]) j=ne[j];
		if(t[i]==t[j+1]) j++;
		ne[i] = j;
	}
	int i=1,j=0;
	for(; i<=l1; i++){ //match s with t
		while(j && s[i]!=t[j+1]) j=ne[j];
		if(s[i]==t[j+1]) j++;
		if(j==l2) {cout<<i-l2+1<<'\n'; j=ne[j];}
	}
	for(int i=1; i<=l2; i++) cout<<ne[i]<<' ';
}

一道应用题:[COCI2011-2012#4] KRIPTOGRAM - 洛谷

注意到明文和暗纹之间只要能对应上即可,比如a a b a d能对应上z z x z y。

考虑这样处理:把首次出现的串记为inf,不是首次出现的,记录它和上一个该串的距离。这样的话,a a b a d和z z x z y都被记为inf 1 inf 2 inf,就能够进行字符串匹配了。

但是要考虑到一种特殊情况,假如是把a a b a d换成b a a b a d,那就变成了inf inf 1 3 2 inf,第二个b对应的数字换了,不再能对应。所以在 数字>=当前匹配长度 的情况(比如这里3>=3),该数字同样视为inf。

#include <bits/stdc++.h>
using namespace std;
#define FOR(i, a, b) for (int i = (a); i <= (b); i++)
#define int long long
#define pii pair<int,int>
const int N = 1e6+5, inf = 1LL<<61;
int n,m,a[N],b[N],ne[N];
string s;
map<string,int> mp;

void solve(){
    for(n=1; cin>>s && s!="$"; n++){ //处理明文数组
        if(mp.find(s)==mp.end()) a[n]=inf; //如果之前没出现过,记为inf
        else a[n]=n-mp[s]; //如果之前出现过,记录和上一个的距离
        mp[s] = n; //重新记录最右位置
    } n--;
    mp.clear(); //map记得清空,留给密文重新用
    for(m=1; cin>>s && s!="$"; m++){ //处理暗纹数组
        if(mp.find(s)==mp.end()) b[m]=inf;
        else b[m]=m-mp[s];
        mp[s] = m;
    } m--;

    //get_next of b
    for(int i=2,j=0; i<=m; i++){
        while(j && b[i]!=b[j+1]) j=ne[j];
        if(b[i]==b[j+1]) j++;
        ne[i] = j;
    }
    //match b to a
    int i=1, j=0;
    for(; i<=n; i++){
        while(j && !(a[i]==b[j+1] || (b[j+1]==inf && a[i]>=j+1))) j=ne[j]; //2.再对匹配条件取反,得到不匹配条件
        if(a[i]==b[j+1] || (b[j+1]==inf && a[i]>=j+1)) j++; //1.先写出匹配条件(a和b对应位置相等,或者b处是inf,而a处上次出现位置不在匹配范围中)
        if(j==m) {cout<<i-j+1; return;}
    }
}
signed main(){
    ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
    int T=1; //cin>>T;
    while(T--) solve();
}

 2.扩展kmp

学习链接:题解-P5410 - xfrvq's blogs - 洛谷博客

模板题链接:【模板】扩展 KMP(Z 函数) - 洛谷

代码:

#include <bits/stdc++.h>
using namespace std;
#define FOR(i, a, b) for (int i = (a); i <= (b); i++)
#define int long long
#define pii pair<int,int>
const int N = 2e7+5;
char s[N], t[N];
int n, m;
int ne[N], ex[N];

void get_ne(){
    ne[0]=m; //从0位置开始的子串就是原串本身,匹配长度为m
    int j=0;
    while(j+1 < m && t[j]==t[j+1]) j++; //暴力算出ne[1]
    ne[1]=j; //记录ne[1]

    int k=1; //k是max{j+ne[j]-1}(0<=j<i)的j,而p = k+ne[k]-1
    for(int i=2; i<m; i++){
        int p = k+ne[k]-1;
        if(i+ne[i-k]-1 < p) ne[i] = ne[i-k]; //情况1,蓝色部分不长于红色部分
        else{ //情况2,后面部分要暴力计算
            j = max(p-i+1, 0LL); //注意j的含义是“匹配进度”
            while(i+j < m && t[j]==t[i+j]) j++;
            ne[i] = j, k = i; //记录匹配进度;因为此时匹配的位置最远,所以k值更新
        }
    }
}
void get_ex(){
    int j=0;
    while(j<n && j<m && s[j]==t[j]) j++;
    ex[0] = j; //暴力计算ex[0]并记录下来

    int k=0; //k是max{j+ex[j]-1}(0<=j<i)的j,而p = k+ex[k]-1
    for(int i=1; i<n; i++){
        int p = k+ex[k]-1;
        if(i+ne[i-k]-1 < p) ex[i] = ne[i-k];
        else{
            j = max(p-i+1, 0LL);
            while(i+j < n && j<m && s[i+j]==t[j]) j++;
            ex[i] = j, k = i;
        }
    }
}
void solve(){
    cin>>s>>t;
    n=strlen(s); m=strlen(t);
    get_ne();
    get_ex();
    int ans1=0, ans2=0;
    FOR(i,0,m-1) ans1^=(i+1)*(ne[i]+1);
    FOR(i,0,n-1) ans2^=(i+1)*(ex[i]+1);
    cout<<ans1<<'\n'<<ans2;
}
signed main(){
    ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
    int T=1; //cin>>T;
    while(T--) solve();
}

kmp的题目经常和循环节有关,要学会应用kmp解决问题,还必须了解最小循环节长度的计算公式:

//最小循环节长度计算公式
len = n%(n-ne[n])==0 ? n-ne[n] : n;

本题中,可以分三种情况讨论答案:

  1. s的循环节就是它本身,也就是len=n,此时不需要分割,答案是1,1

  2. s的循环节长度是1,也就是所有字符都相等,此时每两个字符间都要分割,答案是n-1,1

  3. 其他情况,都可以分成s[1~1]和s[2~n]这两部分,使得两部分都不是循环串(没有严格证明)所以此时第一个答案是2,而第二个答案必须O(n)扫一遍验证,这里需要用到双向的next数组 。

 对于上述第3种情况,为什么一定要暴力扫一遍匹配呢,直接在除了循环节分割处之外的地方分割不是必定可以吗?比如bcbcbcbc,在1,3,5,7位置分割就可以了,事实上,答案也确实是2,4,是满足的。

因为分割后有可能出现新的循环节,考虑这样一个hack数据:aababaabab,它的循环节是aabab,但是不能在6位置进行分割(这样分割成aababa和abab),因为此时右半边abab是循环串。所以这里只能暴力匹配,不能想当然。

#include <bits/stdc++.h>
using namespace std;
#define FOR(i,a,b) for(int i=(a), (i##i)=(b); i<=(i##i); ++i)
#define ROF(i,a,b) for(int i=(a), (i##i)=(b); i>=(i##i); --i)
template<class T>inline bool cmax(T&a,const T&b){return a<b?a=b,1:0;}
template<class T>inline bool cmin(T&a,const T&b){return a>b?a=b,1:0;}
const int N = 5e5+5;
int n,ne[N], ne2[N];
char s[N],t[N];
signed main(){
    ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
    cin>>(s+1); n=strlen(s+1); //输入s
    FOR(i,1,n) t[i]=s[n-i+1]; t[n+1]='\0'; //构造t,使得t是s的反串
    for(int i=2,j=0; i<=n; i++){ //get_ne of s
        while(j && s[i]!=s[j+1]) j=ne[j];
        if(s[i]==s[j+1]) j++;
        ne[i]=j;
    }
    for(int i=2,j=0; i<=n; i++){ //get_ne of t
        while(j && t[i]!=t[j+1]) j=ne2[j];
        if(t[i]==t[j+1]) j++;
        ne2[i]=j;
    }
    /**
     * 下面分三种情况讨论答案,我们记循环节长度为len:
     * 1.s的循环节就是它本身,也就是len=n,此时不需要分割,答案是1,1
     * 2.s的循环节长度是1,也就是所有字符都相等,此时每两个字符间都要分割,答案是n-1,1
     * 3.其他情况,都可以分成s[1~1]和s[2~n]这两部分,使得两部分都不是循环串(没有严格证明)
     *  所以第一个答案是2,而第二个答案必须O(n)扫一遍验证,这里需要用到双向的next数组
     */
    //判断循环节长度的方式:
    //循环节长度 = n%(n-ne[n])==0 ? n-ne[n] : n;
    int len = n%(n-ne[n])==0 ? n-ne[n] : n;
    if(len==n) {cout<<"1\n1"; return 0;} //无循环节
    if(len==1) {cout<<n<<"\n1"; return 0;} //循环节长度是1

    int ans=0;
    FOR(i,1,n-1){
        int j=n-i; //s[i+1]对应t[j]
        // cout<<i<<' '<<j<<' '<<i-ne[i]<<' '<<j-ne[j]<<endl;
        int l1 = i%(i-ne[i])==0 ? i-ne[i] : i; //左边循环节长度
        int l2 = j%(j-ne2[j])==0 ? j-ne2[j] : j; //右边循环节长度
        // int l2 = (n-i)%(n-i-ne2[j])==0 ? (n-i)-ne2[j] : (n-i); //右边循环节长度
        if(l1==i && l2==j) ans++;
    }
    cout<<2<<'\n'<<ans;
}

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值