字符串
之前就是史,重新来写,字符串还是有必要学的。
KMP
用于文本串匹配。其和暴力的区别在于失配后会从一个特定位置重新开始匹配而不是从头开始,从而节约时间。
这个失配数组也就是 \(nex_i\) 表示 \(S[1\ldots i]\) 的最长 \(\text{border}\) 长度,建出来之后相当于一个自动机。
记住代码实现就行了,实际上大部分情况也可以二分哈希替代,但毕竟多个 \(\log\)。
模板题代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll int
const ll N=1145140,M=1919810,inf=2147483646;
ll n,m;
char s[N],t[N];
ll nex[N];
int main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin>>(s+1)>>(t+1);
n=strlen(s+1),m=strlen(t+1);
ll p=0;
for(int i=2;i<=m;++i){
while(p&&t[i]!=t[p+1]) p=nex[p];
if(t[i]==t[p+1]) ++p;
nex[i]=p;
}
p=0;
for(int i=1;i<=n;++i){
while(p&&s[i]!=t[p+1]) p=nex[p];
if(s[i]==t[p+1]) ++p;
if(p==m){
cout<<i-m+1<<'\n';
p=nex[p];
}
}
for(int i=1;i<=m;++i) cout<<nex[i]<<" ";
return 0;
}
为啥我以前就是不肯静下来理解它的本质呢?
思考重点
\(\text{border}\) 是从哪里来的
令文本串 \(S=abaaabab\),模式串 \(T=abab\),考虑两串中的指针匹配到 \(i,j\) ,\(i+1,j+1\) 失配时的情况:
显然第一次失配是在 \(i=3,j=3\) 的时候。
\(i\) 指针是不能前移的,显然要让 \(j\) 前移才可能继续匹配。那么也就是找到一个 \(T_{1\sim j}\) 的前缀 \(T_{1\sim k}\) 满足:
根据这条结论,并且当前已经有 \(T_{1\sim j}=S_{i-j+1\sim i}\),所以我们又能知道:
这条结论的本质是找出一个长度也为 \(k\) 的 \(T_{1\sim j}\) 的后缀。
再通过等量代换,我们就能得到:
于是我们就得到了前移 \(j\) 后得到的 \(T_{1\sim j}\) 的前缀 一定也是 \(T_{1\sim j}\) 的后缀,也就是 \(\text{border}\),\(\text{border}\) 就是这么来的。
为什么要对每个前缀都找到它的最长 \(\text{border}\)
首先要知道一个性质:令字符串 \(S\) 的任意一个 \(\text{border}\) 为 \(T\),则 \(T\) 的任意一个 \(\text{border}\) 也一定是 \(S\) 的 \(\text{border}\),感性理解显然。
于是,从 \(S\) 开始不断跳当前最长的 \(\text{border}\) 便可以枚举到 \(S\) 的所有 \(\text{border}\)。
放在上面所述的匹配过程中也能用最少的枚举次数找到最长满足为 \(S_{i-j+1\sim i}\) 的后缀的 \(T_{1\sim j}\) 的 \(\text{border}\)。
考虑时间复杂度:
显然,每次跳 \(\text{border}\) 时 \(j\) 必定至少减少 \(1\)。
注意到当且仅当 \(S_{i+1}=T_{j=1}\) 时,\(j\) 才会增加 \(1\),因此 \(j\) 最多自增 \(O(|S|)\) 次,同时也最多只会自减 \(O(|S|)\),所以跳 \(\text{border}\) 的次数也是至多 \(O(|S|)\) 次。算上模式串的长度,时间复杂度也就是 \(O(|S|+|T|)\)。
P3426
一个思维题,只需要KMP,不需要后缀数据结构也能做。
首先读完题可以发现,满足题意的子串一定是原串的 \(\text{border}\)。暴力的想法就是去枚举所有 \(\text{border}\) 然后用 KMP 判断匹配的位置之差是否大于串的长度。
考虑进行 dp(想到这一步有点跳跃),设 \(dp[i]\) 为前 \(i\) 个字符的最小答案。对于某个 \(nex_i=0\),也就是没有 \(\text{boeder}\) 的情况时,就只能 \(dp_i=i\),这是显然的。然后考虑答案转移,经过手玩可以发现,\(dp[i]=dp[nex[i]]\),这是可以证明的,但我懒。
那么转移关系就是 \(dp[i]=i\) 或 \(dp[i]=dp[nex[i]]\)。当前面有一个点 \(j\) 可以被 \(dp[nex[i]]\) 覆盖到且能覆盖到当前要覆盖的后缀部分,那么 \(dp[i]\) 就可以从 \(dp[nex[i]]\) 转移过来,这样是一定不劣的。维护这个过程的话可以开一个桶来记录每种长度的 \(\text{border}\) 最多能覆盖到哪里。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll int
const ll N=5*114514,M=1919810,inf=2147483646;
ll n;
char s[N];
ll nex[N],dp[N],bu[N]; //前i个字符的答案
int main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin>>(s+1); n=strlen(s+1);
ll p=0;
for(int i=2;i<=n;++i){
while(p&&s[i]!=s[p+1]) p=nex[p];
if(s[i]==s[p+1]) ++p;
nex[i]=p;
}
dp[1]=bu[1]=1;
for(int i=2;i<=n;++i){
if(bu[dp[nex[i]]]+dp[nex[i]]>=i){
dp[i]=dp[nex[i]];
bu[dp[nex[i]]]=i;
}
else dp[i]=bu[i]=i;
}
cout<<dp[n];
return 0;
}
后缀自动机(SAM)
定义不想多写了,感性理解,SAM 就是个可以 \(O(n)\) 构造并在上面可以 \(O(n)\) 完成操作的有向图,存了原串所有的后缀,有很多有用的性质,能拿来做很多题。
结束位置 \(\text{endpos}\)
对于原串 \(s\) 的任意非空子串 \(t\),其 \(\text{endpos}(t)\) 为 \(t\) 在 \(s\) 中的所有结束位置。两个子串 \(t_1,t_2\) 的 \(\text{endpos}\) 集合可能相等,这样我们可以把所有字串根据 \(\text{endpos}\) 集合划分为若干个等价类。
显然,SAM 中的每个状态对应一个或多个 \(\text{endpos}\) 相同的子串。换句话说,SAM 中的状态数等于所有子串的等价类的个数,再加上初始状态。SAM 的状态个数等价于 \(\text{endpos}\) 相同的一个或多个子串所组成的集合的个数 \(+1\)。同时关于 \(\text{endpos}\) 有一些性质,我就简略写了,一下都考虑 \(s\) 的两个子串 \(u,w\) 并假设 \(|u|\le|w|\):
\(1\).若 \(\text{endpos}(u)\cap\text{endpos}(w)=\oslash\),则 \(u\) 不是 \(w\) 后缀,反之则是。
\(2\).一个等价类中的所有串的长度的集合恰能覆盖一整段区间 \([x,y]\)。
后缀链接 \(\text{link}\)
有点像 AC自动机的失配链接。
待补。

我就是史
浙公网安备 33010602011771号