
引入
KMP算法可以求解出一个模式串在文本串中出现的位置。扩展KMP算法(Z函数)在这个思路上进行了延伸。对于文本串S的每一个后缀(如果文本串S长度为n的话,就指的是 S[i:n]
,其中 (0<=i<=n-1)
),找到这个后缀与模式串T的最长公共前缀的长度,存储在 extend[i]
中。设模式串T长度为m,不难发现,如果我们只看 extend[i] = m
的情况,相当于在文本串S找出模式串T出现的位置,就和原本的KMP算法一样了。
扩展KMP算法在国外一般叫做Z函数(Z function)。
计算extend数组
对于模式串T,我们需要预处理一个数组 next
。其第i项表示从i开始T的后缀与T的最长公共前缀的长度。next
数组的求解在下节可以看到,我们先来看看如何用 next
数组计算出 extend
数组。
和之前说的马拉车算法类似,拓展KMP算法也是利用之前的数据对暴力算法进行优化。维护一个当前所到达过的最右边的值 right
以及所对应的起点 left
(即 left + extend[left] - 1
的最大值为 right
)。此时在第 i
项,默认其前面的 extend[i-1], extend[i-2], ... extend[0]
都已经更新完毕。
如果 i > right
,说明此时第 i
项还没有更新。此时就用最简单的暴力算法向前推进,并且更新 left
和 right
的值。
left = i;while((i+extend[i])<s.length() && (extend[i])<t.length() && t[extend[i]] == s[i+extend[i]]){//思考题:为什么这里没有+1 extend[i]++;}right = left + extend[i] - 1
如果 i <= right
,就可以利用 left
和 right
的值来更新 extend[i]
了。
求解extend数组的最小值
从上图结合之前学的内容可知, S[left:right+1]
和 T[0:right-left+1]
的值是相同的。也就是 S[i:right+1]
与 T[i-left:right-left+1]
的值是相同的(图中红色区域和绿色区域是相同的)。next[i-left] = 2
可以求得 T[0:2] = T[i - left:i - left + 2 + 1]
也就是黄色区域等于绿色区域的前两个。又因为红色区域等于绿色区域,所以红色区域前两个等于黄色区域。也就是文本串S以第i项开头的后缀与模式串T的最长公共前缀为2,也就是黄色区域的长度 next[i - left]
。进而得到 extend[i] = next[i - left]
我们再来看一个例子。
根据上文所述的方法画出红色,绿色和黄色区域。我们会发现套用上一个结论行不通。因为黄色区域的长度已经超过了红色区域的长度。而我们只能保证红色区域与绿色区域相同,并不能保证红色区域的下一个值和绿色区域的下一个值相同,也就是 S[5]
和 T[4]
不能确定是否相同。此时就应该用红色区域的长度作为 extend[i]
的值。extend[i] = right - i + 1
。
综上,可以得出一个通用式子。
extend[i] = min(right - i + 1, next[i - left]);
拓展
如果 i + extend[i] - 1 = right
,说明本次更新已经到了 s[left:right+1]
所管辖的尽头,需要对其进行更新。此时就和暴力的时候一样就好了,拓展 extend[i]
的值并且更新 left
和 right
。
if(i + extend[i] - 1 > right){ left = i; while((i+extend[i])<s.length() && (extend[i])<t.length() && t[extend[i]] == s[i+extend[i]]){//思考题:为什么这里没有+1 extend[i]++; } right = i + extend[i] - 1}
next 数组
其实和上一节很相似,只不过是文本串和模式串都是T而已。这里就直接上代码
void calNextt(){ nextt[0] = t.length(); // 自己和自己的最长公共前缀就是自己本身 int left,right; left = right = 0; for(int i=1;i<t.length();i++){ if(i<=right) nextt[i] = min(right-i+1,nextt[i-left]); while((i+nextt[i])<t.length() && t[nextt[i]] == t[i+nextt[i]]){ nextt[i]++; } if(i+nextt[i]-1>right){ left = i; right = i+nextt[i]-1; } }}
代码实践
代码实践:洛谷P5410
不要忘了用long long!!!!(改了很久的愤怒)
//LICENSE: GPL v3.0//https://blog.haoye.plus
#include<iostream>#include<string>#define int long longusing namespace std;string s,t;int nextt[20000009];//next 为保留字,不能用int extend[20000009];
void calNextt();void calExtend();
signed main(){ cin>>s>>t;
calNextt(); //计算答案 int ans = nextt[0]+1; for(int i=1;i<t.length();i++) ans = ans xor ((i+1)*(nextt[i]+1)); cout<<ans<<endl;
calExtend(); //计算答案 ans = extend[0]+1; for(int i=1;i<s.length();i++){ ans = ans xor ((i+1)*(extend[i]+1)); } cout<<ans<<endl;
return 0;}
void calNextt(){ nextt[0] = t.length(); // 自己和自己的最长公共前缀就是自己本身 int left,right; left = right = 0; for(int i=1;i<t.length();i++){ if(i<=right) nextt[i] = min(right-i+1,nextt[i-left]); while((i+nextt[i])<t.length() && t[nextt[i]] == t[i+nextt[i]]){ nextt[i]++; } if(i+nextt[i]-1>right){ left = i; right = i+nextt[i]-1; } }}
void calExtend(){ extend[0]=0; while((extend[0])<s.length() && (extend[0])<t.length() && t[extend[0]] == s[extend[0]]){ extend[0]++; }
int left,right; left = 0; right = extend[0]-1; for(int i=1;i<s.length();i++){ if(i<=right) extend[i] = min(right-i+1,nextt[i-left]); while((i+extend[i])<s.length() && (extend[i])<t.length() && t[extend[i]] == s[i+extend[i]]){ extend[i]++; } if(i+extend[i]-1>right){ left = i; right = i+extend[i]-1; } }}