KMP 与 Z 函数
KMP-单模匹配
KMP 算法借助 \(nxt\) 数组避免无意义的回溯使得时间复杂度均摊为 \(O(n+m)\)
\(nxt_i\) 记录的是 \(s\) 串前缀 \(i\) 的 border 长度(最长公共前后缀),可以看作是文本串与自己的匹配。模式串与文本串的匹配过程是在用文本串的后缀匹配模式串的前缀
代码:
void getNext()
{
nxt[0]=nxt[1]=0;
for (int i=1;i<m;i++)
{
int j=nxt[i];
while (j&&t[j]!=t[i]) j=nxt[j];
nxt[i+1]=(t[j]==t[i]?j+1:0);
}
}
void solve()
{
int j=0;
for (int i=1;i<n;i++)
{
while (j&&s[i]!=t[j]) j=nxt[j];
if (s[i]==t[j]) j++;
if (j==m) { ans[++tol]=i-m+2; j=nxt[j]; }
}
}
复杂度证明
匹配成功的 j++ 相当于增加势能,失配时跳 \(nxt\) 相当于减少势能,操作复杂度都是 \(O(1)\)。势能一定非负,所以增加势能的次数一定比减少势能的次数要多;而显然增加势能的上限为 \(O(m)\) 次,所以总的复杂度也是 \(O(m)\) 级别
border 理论
定义若 \(\forall i\in[1,|S|-k],S_i=S_{i+k}\),则称 \(k\) 是字符串 \(S\) 的一个周期
定理0:若 \(i\) 是字符串 \(S\) 的一个 border,则 \(|S|-i\) 是 \(S\) 的一个周期
由 \(i\) 是 \(S\) 的 border 有 \(S_j=S_{j+|S|-i},j\in[1,i]\),显然 \(|S|-i\) 是 \(S\) 的 一个周期
定理1:若 \(p,q\) 是字符串 \(S\) 的周期,那么 \((p,q)\) 也是字符串 \(S\) 的周期
易证
定理2:字符串 \(S\) 的 border 长度排序后会形成一个等差数列
不讨论 border 长度小于 \(\lfloor\frac{|S|}{2} \rfloor\) 的情况。
设最长 \(border\) 的长度为 \(i\),那么 \(p=|S|-i\) 即 \(S\) 的最小周期。对于另一周期 \(q\),由定理 0 可知 \(|S|-q\) 是 \(S\) 的 border 的长度;由定理 1 可得 \((p,q)\) 也是 \(S\) 的周期,又因 \(p\) 是 \(S\) 的最小周期,有 \((p,q)=p\) ,即 \(q\) 是 \(p\) 的倍数。综上,\(S\) 的周期会形成公差为 \(p\) 的等差数列,border 长度也会形成公差为 \(p\) 的等差数列
定理3:字符串 \(S\) 的所有 border 排序后会形成 \(\log\) 个等差数列
按照 border 的最高位排序,对于 \([2^i,2^{i+1})\) 的最长 border \(p\),其余所有 border 也是 \(S_{[1,p]}\) 的 border。按照定理 2 ,这些 border 长度会形成等差数列。
可以把 KMP 的均摊复杂度变成带一个 log 但更为稳定的复杂度
Z函数(扩展 kmp)
在 Z 函数中, \(z\) 数组记录的是后缀 \(i\) 和原串的 LCP 长度(最长公共前缀)
代码:
void _Z(string str)
{
int l=0,r=0;
memset(z,0,sizeof z); z[1]=m;
for (int i=2;i<=m;i++)
{
if (i<=r) z[i]=min(z[i-l+1],r-i+1);
while (i+z[i]<=n&&str[z[i]+1]==str[i+z[i]]) z[i]++;
if (i+z[i]-1>r) l=i,r=i+z[i]-1;
}
}
若要对字符串 \(a,b\) 求 \(b\) 与 \(a\) 的每个后缀的 LCP,先对 \(a\) 跑 Z 函数再匹配即可
复杂度证明
每次 while 都会移动 \(r\) 指针,\(r\) 指针不会超过字符串长度,所以复杂度是 \(O(n)\) 的
P9576 「TAOI-2」Ciallo~(∠・ω< )⌒★
调题:一定要记得清空 \(z\) 数组啊。。
一个合法方案要么是 \(t\) 串原本就在 \(s\) 串出现过,要么是 \(t\) 的前缀、后缀在 \(s\) 中前后出现过通过删除子串 \([l,r]\) 得到 \(t\),第一种情况很好考虑,匹配后根据组合数计算即可,第二种情况按后缀考虑。
设 \(s[i,i+p-1]\) 与 \(t[1,p]\) 匹配,\(s[j-q+1,j]\) 与 \(t[m-q+1,m]\) 匹配,那么这两个前后缀能拼接在一起贡献当且仅当 \(j-i+1\geqslant m,p+q+1-m>0\),即 \(i\in[1,j+1-m],p\in[m-q,+\infty]\)。这个限制与贡献计算都可以用二维数点轻松解决,贡献的计算即为满足条件的 \(\sum{(p+1-m)}-cnt\times q\)。注意 \(q=m\) 的情况即可,不要重复计算
代码
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=4e7+5;
string s,t;
int n,m;
int z[N],len[3][N];
int ans;
struct BIT
{
int tr[N];
inline int lowbit(int x) { return x&-x; }
inline void add(int x,int y) { if (!x) return ; while (x<=n) { tr[x]+=y; x+=lowbit(x); } }
inline int sum(int x)
{
int res=0;
while (x) { res+=tr[x]; x-=lowbit(x); }
return res;
}
}bt1,bt2;
void _Z(string str)
{
int l=0,r=0;
for (int i=1;i<=m;i++) z[i]=0;
z[1]=m;
for (int i=2;i<=m;i++)
{
if (i<=r) z[i]=min(z[i-l+1],r-i+1);
while (i+z[i]<=n&&str[z[i]+1]==str[i+z[i]]) z[i]++;
if (i+z[i]-1>r) l=i,r=i+z[i]-1;
}
}
void init(string s1,string s2,int op)
{
_Z(s2);
int l=0,r=0;
for (int i=1;i<=n;i++)
{
if (i<=r) len[op][i]=min(r-i+1,z[i-l+1]);
while (i+len[op][i]<=n&&len[op][i]+1<=m&&s1[i+len[op][i]]==s2[len[op][i]+1]) len[op][i]++;
if (i+len[op][i]-1>r) l=i,r=i+len[op][i]-1;
}
}
int ea(int x,int y)
{
if (x<0) return 0;
int res=bt1.sum(x)+y*bt2.sum(x);
return res;
}
void calc()
{
for (int i=m;i<=n;i++)
{
int pres=ans;
if (len[1][i-m+1]==m) len[1][i-m+1]--;
bt1.add(len[1][i-m+1],len[1][i-m+1]-m+1),bt2.add(len[1][i-m+1],1);
if (len[2][i]==m) len[2][i]--,ans-=m-1;
ans+=ea(m,len[2][i])-ea(m-len[2][i]-1,len[2][i]);
}
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>s>>t;
n=s.size(),s=" "+s;
m=t.size(),t=" "+t;
if (n<=m) { cout<<0; return 0; }
init(s,t,1);
string tmp1=" ",tmp2=" ";
for (int i=n;i>=1;i--) tmp1+=s[i];
for (int i=m;i>=1;i--) tmp2+=t[i];
init(tmp1,tmp2,2);
for (int i=1;i<=n/2;i++) swap(len[2][i],len[2][n-i+1]);
for (int i=1;i<=n;i++) if (len[1][i]==m) ans+=(i-1)*i/2+(n-i-len[1][i]+1)*(n-i-len[1][i]+2)/2;
calc(),cout<<ans;
return 0;
}

浙公网安备 33010602011771号