我们有两个字符串:
A=“abbaabbbabaa”
B=“abbaaba”
显然第一个位置不能匹配:
1 2 abbaabb(x)babaa abbaaba(x)
我们要不要暴力移动?
显然不是最优的!
考虑我们1~7位已经匹配了,说明A [ 1 → 7 ] = B [ 1 → 7 ] A[1 \to 7] = B[1 \to 7] A [ 1 → 7 ] = B [ 1 → 7 ]
我们需要找到一个位置i i i ,使得A [ 7 − i + 1 → 7 ] = B [ 1 → i ] A[7-i+1 \to 7] = B[1 \to i] A [ 7 − i + 1 → 7 ] = B [ 1 → i ]
这样我们直接跳到i i i 即可。
1 2 3 abbaabbbabaa abbaaba (i=3)
注意到A [ 1 → 7 ] = B [ 1 → 7 ] A[1 \to 7]=B[1 \to 7] A [ 1 → 7 ] = B [ 1 → 7 ] 。
即B [ 7 − i + 1 → 7 ] = B [ 1 → i ] B[7-i+1 \to 7]=B[1\to i] B [ 7 − i + 1 → 7 ] = B [ 1 → i ]
于是要找到i i i ,等于找到一个位置i i i ,使得B B B 的前i i i 位和后i i i 位相等。
(这样的字符串B [ 1 → i ] B[1 \to i] B [ 1 → i ] 称为B B B 的border)
怎么办?
可以搞出一个n e x t [ x ] next[x] n e x t [ x ] 数组,表示B B B 的前x x x 位的最长border的长度。
为什么是最大,明明所有满足前缀=后缀的i i i 都是合法的?
仔细考虑一件事情,考虑A A A 中最大的border,肯定是A [ 1 → n e x t [ n ] ] A[1 \to next[n]] A [ 1 → n e x t [ n ] ] (蓝色)
考虑A A A 中第二大的border,肯定是A [ 1 → n e x t [ n e x t [ n ] ] ] A[1 \to next[next[n]]] A [ 1 → n e x t [ n e x t [ n ] ] ] (红色)
为什么?
考虑一个比红色字符串长的字符串(绿色),如果它是A A A 的border,这样可以推出最左边和最右边的两个绿色字符串相等,又因为两个蓝色字符串相等,就可以推出中间两个绿色字符串和两边的绿色字符串相等,可以推出它是蓝色字符串的border,说明n e x t [ n e x t [ n ] ] next[next[n]] n e x t [ n e x t [ n ] ] 可以取得更大,推出矛盾。
于是我们可以顺着n e x t next n e x t 数组向前跳,肯定是从大到小遍历了A A A 的所有border。
顺便扯几句,如果对于所有的i ∈ [ 1 , n ] i \in [1,n] i ∈ [ 1 , n ] 我们把i i i 向n e x t [ i ] next[i] n e x t [ i ] 连一条边,就会形成一棵树,因为显然n e x t [ i ] < i next[i]<i n e x t [ i ] < i 。
这样就可以构成一棵fail树,有很多奇妙的性质。
对于字符串abaabaa,它的n e x t next n e x t 数组:
1
2
3
4
5
6
7
0
0
1
1
2
3
4
构建出来fail树。
fail树有什么性质呢?
假设我们现在有一个节点x x x ,那么对于x x x 的子树里面的节点y y y ,满足A [ 1 → x ] A[1 \to x] A [ 1 → x ] 是A [ 1 → y ] A[1 \to y] A [ 1 → y ] 的border。
对于x x x 的祖先z z z ,满足A [ 1 → z ] A[1 \to z] A [ 1 → z ] 是A [ 1 → x ] A[1 \to x] A [ 1 → x ] 的border。
不扯淡了,考虑如何构建n e x t next n e x t 数组。
考虑一个类似于d p dp d p 的做法,从大到小遍历n e x t next n e x t 数组,并且开一个指针j j j 维护现在匹配的哪里。
然后每次暴力跳(往n e x t [ j ] next[j] n e x t [ j ] 跳,即跳到父节点),跳到一个位置j j j ,满足a [ j + 1 ] = = a [ i ] a[j+1]==a[i] a [ j + 1 ] = = a [ i ] 即可。
为什么每次暴力跳完之后不用指针归位,考虑到我们找到的是最大的n e x t [ i ] next[i] n e x t [ i ] ,所以可以从上一个位置继续跳,这是保证KMP时间复杂度正确的主要因素。
求nex部分代码:
1 2 3 4 5 6 int j=0 ;for (register int i=2 ;i<=n;++i){ while (j&&a[j+1 ]!=a[i]) j=nex[j]; if (a[j+1 ]==a[i]) j++; nex[i]=j; }
如何证明时间复杂度,再从fail树的角度考虑。
每次求n e x t next n e x t ,就等于是从上次跳到的位置向上到某个祖先,然后向下挂一个节点,位置移动到哪个节点。
显然每个节点最多只会被跳到一次。
于是复杂度是O ( n ) O(n) O ( n ) 的。
求两个字符串之间的匹配也比较相似,不再赘述:
1 2 3 4 5 6 j=0 ; for (register int i=1 ;i<=m;++i){ while (j&&a[j+1 ]!=b[i]) j=nex[j]; if (a[j+1 ]==b[i]) j++; if (j==n) printf ("%d\n" ,i-n+1 ); }
模板题:
https://www.luogu.org/problem/P3375
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 #include <bits/stdc++.h> #define MAXN 1000005 using namespace std;inline int read () { int x=0 ,f=1 ; char ch=getchar (); while (ch<'0' ||ch>'9' ){ if (ch=='-' ) f=-1 ; ch=getchar (); } while (ch>='0' &&ch<='9' ){ x=(x<<3 )+(x<<1 )+(ch^'0' ); ch=getchar (); } return x*f; } int nex[MAXN];int main () { char a[MAXN],b[MAXN]; scanf ("%s" ,b+1 ); scanf ("%s" ,a+1 ); int n=strlen (a+1 ),m=strlen (b+1 ); memset (nex,0 ,sizeof (nex)); int cnt=0 ,j=0 ; for (register int i=2 ;i<=n;++i){ while (j&&a[j+1 ]!=a[i]) j=nex[j]; if (a[j+1 ]==a[i]) j++; nex[i]=j; } j=0 ; for (register int i=1 ;i<=m;++i){ while (j&&a[j+1 ]!=b[i]) j=nex[j]; if (a[j+1 ]==b[i]) j++; if (j==n) printf ("%d\n" ,i-n+1 ); } for (register int i=1 ;i<=n;++i){ printf ("%d " ,nex[i]); } }
咕咕咕