前缀函数与KMP
Steadywelkin · · 个人记录
前缀函数与KMP
前缀函数
定义
前缀函数数π的定义为:
1.如果子串s[0,...i]有一对真后缀与真前 缀:s[0,...k-1]和s[i-(k-1)...i]那么π[i]就是这个相等的真前缀的长度
2.如果不止有一对相等,则π[i]即最长的那一对
3.如果没有相等,那么π[i]等于0
例如:
对于字符串abcabcd有:
π[0]=0 无真后缀与真前缀
π[1]=0 无相等的真前缀与真后缀
π[2]=0 无相等的真前缀与真后缀
π[3]=1 相等的真前缀与真后缀为a
π[4]=2 相等的真前缀与真后缀为ab
π[5]=3 相等的真前缀与真后缀为abc
π[6]=0 无相等的真前缀与真后缀
算法与优化
优化1:
相邻的前缀函数值至多增加1,
移动到下一个位置时,前缀函数的值要么增加一,要么维持不变,要么减少
优化2:
如果我们找到了长度j,只需要比较a[i+1]和s[j]如果他们相等,那么就有π[i+1]=j+1
否则需要找到子串s[0,...j]的长度仅次于j的第二长度,使得前缀性质得到保存
优化后的完整代码
void function(string a)
{
int len=(int)a.length();
for(int i=1;i<len;i++)
{
int j=pi[i-1];
while(j>0 && a[i]!=a[j])
j=pi[j-1];
if(a[i]==a[j])
j++;
pi[i]=j;
}
return;
}
KMP
典型问题
给定一个文本t和一个字符串s,尝试找到并展示s在t中的所有出现
分析
用n表示字符串s的长度 用m表示文本t的长度
构造一个字符串s+"#"+t(“#”指代既不在s中出现又不在t中出现的字符)
计算该字符的前缀函数 根据定义如果有π[i]=n出现 说明s完整出现在该位置
位置的计算:i-(n-1)-(n+1)=i-2*n
时间复杂度:O(n+m) 空间复杂度:O(n)
字符串的周期
定义
周期: 对于字符串s和0<p<=|s|,若s[i]对于所有的i属于[0,|s|-p-1]成立,称p是s的周期
border: 对于字符串s和0<=r<|s|,若有s长度为r的前缀与后缀相等,称s长度为r的前缀是s的border
计算
|s|-r是s的周期
所有border的长度: π[n-1],π[π[n-1]]...
最小周期: n-π[n-1]
统计前缀出现次数
第一类问题
给定一个长度为n的字符串s,统计每个前缀s[0,...i]在同一字符串的出现次数
solution
首先让我们来解决第一个问题。考虑位置i 的前缀函数值π[i] 。根据定义,其意味着字符串s一个长度为 π[i] 的前缀在位置i出现并以i为右端点,同时不存在一个更长的前缀满足前述定义。与此同时,更短的前缀可能以该位置为右端点。容易看出,我们遇到了在计算前缀函数时已经回答过的问题:给定一个长度为j的前缀,同时其也是一个右端点位于i的后缀,下一个更小的前缀长度k<j 是多少?该长度的前缀需同时也是一个右端点为i的后缀。因此以位置i为右端点,有长度为π[π[i]-1]的前缀,有长度为π[π[π[i]-1]-1]的前缀,有长度为0的前缀,等等,直到长度变为 。故而我们可以通过下述方式计算答案。
vector<int> ans(n + 1);
for (int i = 0; i < n; i++) ans[pi[i]]++;
for (int i = n - 1; i > 0; i--) ans[pi[i - 1]] += ans[i];
for (int i = 0; i <= n; i++) ans[i]++;
第二类问题
统计每个前缀s[0,...i]在另一个给定字符串t中的出现次数。
solution
我们应用来自 Knuth-Morris-Pratt 的技巧:构造一个字符串s+"#"+t并计算其前缀函数。与第一个问题唯一的不同之处在于,我们只关心与字符串t相关的前缀函数值,即t>=n+1的π[i]。有了这些值之后,我们可以同样应用在第一个问题中的算法来解决该问题。