扩展KMP
Z函数:对于一个长度为 n n n的字符串 S S S, z [ i ] z[i] z[i]表示 S S S与后缀 S [ i , n ] S[i,n] S[i,n]的最长公共前缀LCP的长度。
比如 S = a a a b a a a b c S=aaabaaabc S=aaabaaabc,其前缀有 a , a a , a a a , a a a b , a a a b a , a a a b a a , a a a b a a a , a a a b a a a b , a a a b a a a b c a,aa,aaa,aaab,aaaba,aaabaa,aaabaaa,aaabaaab,aaabaaabc a,aa,aaa,aaab,aaaba,aaabaa,aaabaaa,aaabaaab,aaabaaabc。设 S [ 2 , n ] = a a b a a a b c S[2,n]=aabaaabc S[2,n]=aabaaabc,其前缀有 a , a a , a a b , a a b a , a a b a a , a a b a a a , a a b a a a b , a a b a a a b c a,aa,aab,aaba,aabaa,aabaaa,aabaaab,aabaaabc a,aa,aab,aaba,aabaa,aabaaa,aabaaab,aabaaabc。发现 S S S与 S [ 2 , n ] S[2,n] S[2,n]的最长公共前缀为 a a aa aa,长度是 2 2 2,因此 z [ 2 ] = 2 z[2]=2 z[2]=2。
Z-Box:它是字符串 S S S中的一个区间 [ l , r ] [l,r] [l,r],满足 S [ l , r ] S[l,r] S[l,r]是 S S S的前缀(不一定要求是最长),会随着 i i i的变化而移动,当在位置 i i i时, [ l , r ] [l,r] [l,r]必须包含 i i i,并且 r r r要尽可能的大。
如下图所示:
这里有三个Z-Box。我们来看当 i = 8 i=8 i=8时, S [ i ] = c S[i]=c S[i]=c,它应该属于第二个Z-Box,因为第二个Z-Box是 b a c b bacb bacb满足是 S S S的前缀, c c c在这个Z-Box中,并且 r r r尽可能的大。当 i = 9 i=9 i=9时, S [ i ] = b S[i]=b S[i]=b,它应该属于第三个Z-Box而不是第二个Z-Box,因为假设它属于第二个Z-Box,那么虽然盒子长度大,但是它的 r = 9 r=9 r=9。而如果它属于第三个Z-Box,那么虽然盒子长度小,但是它的 r = 10 r=10 r=10尽可能的大。
我们可以在知道 i − 1 i-1 i−1位置Z-Box的情况下,求出 z [ i ] z[i] z[i]和 i i i位置的Z-Box。
假设我们知道某个 [ l , r ] [l,r] [l,r],那么我们可以对称找到其在 S S S中的对应位置。设区间 [ l , r ] [l,r] [l,r]的长度为 l e n = r − l + 1 len=r-l+1 len=r−l+1,对应点 l l l,其对称位置是起点 1 1 1。对于点 r r r,其对称位置是 1 + l e n − 1 = 1 + ( r − l + 1 ) − 1 = r − l + 1 1+len-1=1+(r-l+1)-1=r-l+1 1+len−1=1+(r−l+1)−1=r−l+1。设区间 [ l , i ] [l,i] [l,i]的长度为 L e n = i − l + 1 Len=i-l+1 Len=i−l+1,对于点 i i i,其对称位置是 1 + L e n − 1 = 1 + ( i − l + 1 ) − 1 = i − l + 1 1+Len-1=1+(i-l+1)-1=i-l+1 1+Len−1=1+(i−l+1)−1=i−l+1。于是我们可以得到这六个点。
算法流程:
计算完前 i − 1 i-1 i−1个Z函数,维护盒子 [ l , r ] [l,r] [l,r],则 S [ l , r ] = S [ 1 , r − l + 1 ] S[l,r]=S[1,r-l+1] S[l,r]=S[1,r−l+1]。
-
如果 i ≤ r i\leq r i≤r(在盒内),则有 S [ i , r ] = S [ i − l + 1 , r − l + 1 ] S[i,r]=S[i-l+1,r-l+1] S[i,r]=S[i−l+1,r−l+1]。
(1)若 z [ i − l + 1 ] < r − i + 1 z[i-l+1]<r-i+1 z[i−l+1]<r−i+1,则 z [ i ] = z [ i − l + 1 ] z[i]=z[i-l+1] z[i]=z[i−l+1]。
(2)若 z [ i − l + 1 ] ≤ r − i + 1 z[i-l+1]\leq r-i+1 z[i−l+1]≤r−i+1,则 z [ i ] = r − i + 1 z[i]=r-i+1 z[i]=r−i+1,从 r + 1 r+1 r+1往后开始暴力枚举。
对于在盒内的,即当 i ≤ r i\leq r i≤r时,我们可以取 z [ i ] = m i n ( z [ i − l + 1 ] , r − i + 1 ) z[i]=min(z[i-l+1],r-i+1) z[i]=min(z[i−l+1],r−i+1)。
-
如果 i > r i>r i>r(在盒外),则从 i i i开始暴力枚举。
-
求出 z [ i ] z[i] z[i]后,如果 i + z [ i ] − 1 > r i+z[i]-1>r i+z[i]−1>r,则更新 l = i , r = i + z [ i ] − 1 l=i,r=i+z[i]-1 l=i,r=i+z[i]−1
如果找到了尽可能右的盒子,那么我们就要更改这个新盒子的左端点 l l l和右端点 r r r。
代码:
void get_z(char *s, int n)
{
z[1] = n;
for (int i = 2, l, r = 0; i <= n; i ++ )
{
// 如果在盒内 整合第一种情况中的(1)和(2)
if (i <= r) z[i] = min(z[i - l + 1], r - i + 1);
// 无论i是在盒内还是在盒外,我们都先进行预判
while (i + z[i] <= n && 1 + z[i] <= n && s[i + z[i]] == s[1 + z[i]]) z[i] ++ ;
// 更新盒子的左右端点
if (i + z[i] - 1 > r)
{
l = i;
r = i + z[i] - 1;
}
}
}
内层while循环的总次数决定了总的执行次数。对于while循环,如果条件成立则z++,即盒子的右端点 r r r就会后移若干位。而 r ≤ n r\leq n r≤n,如 r r r右移总步数不会超过 n n n。所以时间复杂度是 O ( n ) O(n) O(n)。
题目描述
算法思路
算法实现
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 2e7 + 10;
typedef long long LL;
// a是文本串 b是模式串
char a[N], b[N];
int z[N], p[N];
// n是模式串s的长度 m是文本串t的长度
int n, m;
// 求出模式串s的Z函数
void Z(char *s, int n)
{
z[1] = n;
for (int i = 2, l = 0, r = 0; i <= n; i ++ )
{
if (i <= r) z[i] = min(z[i - l + 1], r - i + 1);
while (1 + z[i] <= n && i + z[i] <= n && s[1 + z[i]] == s[i + z[i]]) z[i] ++ ;
if (i + z[i] - 1 > r)
{
l = i;
r = i + z[i] - 1;
}
}
}
// s是模式串,n是模式串s的长度
// t是文本串,m是文本串t的长度
void exKMP(char *s, int n, char *t, int m)
{
Z(s, n); // 求出模式串s的Z函数
for (int i = 1, l = 0, r = 0; i <= m; i ++ )
{
if (i <= r) p[i] = min(z[i - l + 1], r - i + 1);
while (1 + p[i] <= n && i + p[i] <= m && s[1 + p[i]] == t[i + p[i]]) p[i] ++ ;
if (i + p[i] - 1 > r)
{
l = i;
r = i + p[i] - 1;
}
}
}
int main()
{
scanf("%s%s", a + 1, b + 1);
m = strlen(a + 1), n = strlen(b + 1); // m是文本串 n是模式串
exKMP(b, n, a, m);
LL ans = 0;
for (int i = 1; i <= n; i ++ ) ans ^= 1ll * i * (z[i] + 1);
printf("%lld\n", ans);
ans = 0;
for (int i = 1; i <= m; i ++ ) ans ^= 1ll * i * (p[i] + 1);
printf("%lld\n", ans);
return 0;
}