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

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

基础定义

字符串

\(S\):无特殊说明,字符串仅由26个小写字母\('a'-'z'\) 构成, 并用大写字母表示一个字符串。

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

\(S[i]\) : 表示字符串 \(S\)\(i\) 个位置的字母,下标从 \(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[i - p]\) , 对于 \(p < i \le |S|\) 成立, 则称 \(p\) 为字符串 \(S\) 的一个周期。

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

重要性质

\(p\)\(S\) 的周期 $\iff |S| - p $ 是 \(S\) 的 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[i-1]\) 的Border

因此,求 \(pre[i]\) 的Border时,可以遍历 \(pre[i-1]\) 的所有Border,

\(next[i-1],next[next[i-1]],...,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 字符串匹配(模板)

题目链接

题意

给出两个字符串 \(S\)\(T\), 求出 \(T\)\(S\) 中所有出现的位置。

思路

枚举每个位置,遇到不同时,只需要跳到下一个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\)\((m\le n)\)

求出 \(A\) 中有多少个长度为 \(m\) 的连续子序列 \(A'\) 满足 \((A'_1+B_1)\%k=(A'_2+B_2)\%k=...=(A'_m+B_m)\%k\)

思路

遇到这种式子,一个很经典的处理方式就是将相同类型的移项到同一边。于是有 \((A'_1-A'_2)\%k=-(B_1-B_2)\%k\)。从 \(1\)\(m-1\) 都满足。那么,我们可以预处理这两个差分数组。然后问题就变成了匹配问题。我们就可以魔改一下 \(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 @ 2024-07-29 20:07  Showball  阅读(46)  评论(0)    收藏  举报