KMP1(字符串基本概念,KMP算法和简单应用)

KMP1(字符串基本概念,KMP算法和简单应用)

基础定义

字符串

S:无特殊说明,字符串仅由26个小写字母az 构成, 并用大写字母表示一个字符串。

|S|:表示一个字符串的长度

S[i] : 表示字符串 Si 个位置的字母,下标从 1 开始。

子串

S[l,r] : 表示字符串 S 从第 l 到第 r 个字母顺次连接而成的新字符串。

Pre[i]: 表示字符串 S 的长度为 i 的前缀, Pre[i]=s[1,i]

Suf[i]:表示字符串 S 的长度为 i 的后缀, Suf[i]=s[|S|i+1,|S|]

Border

如果字符串 S 的同长度的前缀和后缀完全相同,则称此前缀为一个Border。

特殊地,字符串本身也可以是它的 Border。(根据语境判断)

e.g. 若 S=bbabbab,试求所有 Border

b

bbab

周期和循环节

对于字符串 S 和正整数 p,如果有 S[i]=S[ip] , 对于 p<i|S| 成立, 则称 p 为字符串 S 的一个周期。

特殊地p=|S| 一定是 S 的周期

重要性质

pS 的周期 |S|pS 的 Border

因此周期和Border 等价,遇到题目可以相互转化

注意: Border 不具有二分性!

Border的性质

传递性S 的 Border 的 Border 也是 S 的 Border

求 S 的所有 Border 等价于求所有前缀的最大Border

KMP

Next数组

next[i] 表示 pre[i] 的非平凡最大Border

next[1]=0

我们发现 pre[i] 的 Border 去掉最后一个字母就会变成 pre[i1] 的Border

因此,求 pre[i] 的Border时,可以遍历 pre[i1] 的所有Border,

next[i1],next[next[i1]],...,0 ,检查后一个字母是否等于 s[i]

参考代码:

struct KMP{
    vector<int> nxt;
	void init(string s){//用来维护出nxt数组
		int n=s.size();
		nxt.assign(n+1,0);
		s="?"+s;
		for(int i=2;i<=n;i++){
			nxt[i]=nxt[i-1];
			while(nxt[i]&&s[nxt[i]+1]!=s[i]) nxt[i]=nxt[nxt[i]];
			nxt[i]+=(s[nxt[i]+1]==s[i]);
		}
	}
};

例题1 字符串的问题

题目链接

题意

找出字符串 S 中满足下列条件的最长子串:

1.该子串是 S 的前缀

2.该子串是 S 的后缀

3.该子串除了开头的结尾还出现一次

思路

转化一下题意,找到最长的在中间出现一次的Border。

朴素做法自然是枚举所有的Border,然后判断是否出现即可。

但其实,我们只用计算 nxt[n]nxt[nxt[n]]。因为 nxt[nxt[n]]

说明对应的Border至少出现了四次。一定是满足题意的。

代码

struct KMP{
    vector<int> nxt;
	void init(string s){
		int n=s.size();
		nxt.assign(n+1,0);
		s="?"+s;
		for(int i=2;i<=n;i++){
			nxt[i]=nxt[i-1];
			while(nxt[i]&&s[nxt[i]+1]!=s[i]) nxt[i]=nxt[nxt[i]];
			nxt[i]+=(s[nxt[i]+1]==s[i]);
		}
	}
};
void Showball(){
    string s;
    cin>>s;
    int n=s.size();
    KMP kmp;
    kmp.init(s);
    int border=kmp.nxt[n];
    for(int i=1;i<n;i++){
    	if(border&&kmp.nxt[i]==border){
    		return cout<<s.substr(0,border),void();
    	}
    }
    border=kmp.nxt[kmp.nxt[n]];
    if(border==0) cout<<"Just a legend";
    else cout<<s.substr(0,border);
}

例题2 字符串匹配(模板)

题目链接

题意

给出两个字符串 ST, 求出 TS 中所有出现的位置。

思路

枚举每个位置,遇到不同时,只需要跳到下一个Border即可。

template<class Type>
struct KMP{
	vector<int> init(Type s){
		int n=(int)s.size()-1;
		vector<int> nxt(n+1,0);
		for(int i=2;i<=n;i++){
			nxt[i]=nxt[i-1];
			while(nxt[i]&&s[nxt[i]+1]!=s[i]) nxt[i]=nxt[nxt[i]];
			nxt[i]+=(s[nxt[i]+1]==s[i]);
		}
		return nxt;
	}

	vector<int> match(Type s,Type t){
    	vector<int> pos;
		vector<int> nxt=init(t);
		int n=(int)s.size()-1,m=(int)t.size()-1;
		for(int i=1,j=0;i<=n;i++){
			while(j&&s[i]!=t[j+1]) j=nxt[j];
			j+=(t[j+1]==s[i]);
			if(j==m){//匹配成功
				j=nxt[j];
				pos.push_back(i-m+1);
			}
		}
		return pos;
	}
};
KMP<string> kmp;
void Showball(){
    string s,t;
    cin>>s>>t;
    s="?"+s;
    t="?"+t;
    vector<int> nxt=kmp.init(t);
    vector<int> pos=kmp.match(s,t);
    for(auto x:pos) cout<<x<<endl;
    int n=t.size();
    for(int i=1;i<n;i++) cout<<nxt[i]<<" \n"[i==n-1];
}

例题3 栗酱的数列

题目链接

题意

给你一个长度为 n 的数列 A 和一个长度为 m 的数列 B(mn)

求出 A 中有多少个长度为 m 的连续子序列 A 满足 (A1+B1)%k=(A2+B2)%k=...=(Am+Bm)%k

思路

遇到这种式子,一个很经典的处理方式就是将相同类型的移项到同一边。于是有 (A1A2)%k=(B1B2)%k。从 1m1 都满足。那么,我们可以预处理这两个差分数组。然后问题就变成了匹配问题。我们就可以魔改一下 KMP s算法即可解决。

代码

void Showball(){
    int n,m,k;
    cin>>n>>m>>k;
    vector<int> a(n+1),b(m+1);
    for(int i=1;i<=n;i++){
    	cin>>a[i];
    	a[i]%=k;
    }
    for(int i=1;i<=m;i++){
    	cin>>b[i];
    	b[i]%=k;
    }

    vector<int> da(n),db(m);
    for(int i=1;i<n;i++) da[i]=(a[i]-a[i+1]+k)%k;	 
    for(int i=1;i<m;i++) db[i]=(k-(b[i]-b[i+1]+k)%k)%k;

    vector<int> ans=kmp.match(da,db);
	cout<<ans.size()<<endl;
}

拓展

Border的性质

周期定理:若 p,q 均为串 S 的周期,则 gcd(p,q) 也为 S 的周期

一个串的 Border 数量是 O(n) 个,但他们组成了 O(logN) 个等差数列

KMP 的推广

扩展KMP(Z 算法)

KMP 自动机, Border树

AC自动机, 即KMP的多串模式。

Trie图, 即KMP自动机的多串模式。

Border树

定义

对于一个字符串 S, n=|S| , 它的 Border 树 共有 n+1 个节点: 0,1,2,3,...,n

0 是这课有向树的根,对于其他点,每个点的父节点是 nxt[i]

性质

1.每个前缀 pre[i] 的所有Border:节点 i 到根的链

2.哪些前缀有长度为 x 的Border: x 的子树

3.求两个前缀的公共Border 等价于求LCA

posted @   Showball  阅读(18)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示