今天写了一遍KMP \text{KMP} KMP ,看来也没有想象中那么难写嘛……
首先是next \text{next} next 数组的求法。很多博客里说是“自匹配求next \text{next} next ”,但是我完全不懂。直到看到了知乎上的这篇文章,才终于掌握了求next \text{next} next 的方法。这个回答中的“次优”二字很妙,点破了求next \text{next} next 的真谛。如何更好的理解和掌握 KMP 算法? - 逍遥行的回答 - 知乎
下面对next \text{next} next 数组的求法给予简单说明。
本文中next[i] \text{next[i]} next[i] 指的是s[0...i] \text{s[0...i]} s[0...i] 这一段中,既是真 前缀又是真 后缀的最长子串的长度,如在字符串"aaaaa"中,next[4] = 4 \text{next[4]}=4 next[4] = 4 。
我们假设现在已经求出了next[0] ∼ next[i-1] \text{next[0]}\sim\text{next[i-1]} next[0] ∼ next[i-1] ,现在想求next[i] \text{next[i]} next[i] 。 我们已知s[0] ∼ s[next[i-1]-1] \text{s[0]}\sim \text{s[next[i-1]-1]} s[0] ∼ s[next[i-1]-1] 和s[i-next[i-1]] ∼ s[i-1] \text{s[i-next[i-1]]}\sim \text{s[i-1]} s[i-next[i-1]] ∼ s[i-1] 这两段是一样的,如果s[next[i-1]] \text{s[next[i-1]]} s[next[i-1]] 和s[i] \text{s[i]} s[i] 也是一样的,那岂不是很好?如果是这样,next[i] \text{next[i]} next[i] 便等于next[i-1]+1 \text{next[i-1]+1} next[i-1]+1 。 然而有时候~~(大多数时候)~~事情并没有这么简单。我们悲哀地发现s[next[i-1]] ≠ s[i] \text{s[next[i-1]]} \neq \text{s[i]} s[next[i-1]] = s[i] 。这时我们怎么去求next[i] \text{next[i]} next[i] 呢? 我们经过思考可以发现,s[0] ∼ s[next[i]-2] \text{s[0]}\sim \text{s[next[i]-2]} s[0] ∼ s[next[i]-2] 和s[i-next[i]] ∼ s[i-1] \text{s[i-next[i]]}\sim \text{s[i-1]} s[i-next[i]] ∼ s[i-1] 这两段是一样的。那么,这两个公共的前缀及后缀应该也是next[i-1] \text{next[i-1]} next[i-1] 的一个次优解。
我们经过探索知道,next[i] \text{next[i]} next[i] 的次优解正是next[next[i]] \text{next[next[i]]} next[next[i]] 。
因此,我们的方法是,设一个变量t t t ,若s[i]!=s[t] \text{s[i]!=s[t]} s[i]!=s[t] 且t > 0 t>0 t > 0 ,则使t t t 不断等于next[ t − 1 ] \text{next[}t-1\text{]} next[ t − 1 ] 。直到跳出循环条件,则把next[i] \text{next[i]} next[i] 设为t + 1 t+1 t + 1 或0 0 0 。
求next[i] \text{next[i]} next[i] 的代码如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 COPY void get_next (string &s) { int l=s.size(); nxt[0 ]=0 ; for (int i=1 ;i<l;i++){ int t=nxt[i-1 ]; while (t && s[i]!=s[t]) t=nxt[t-1 ]; if (s[t]==s[i]) nxt[i]=t+1 ; else nxt[i]=0 ; } }
求next \text{next} next 数组这一最艰巨的任务~~(大雾)已经完成,下面就是 比较简单的~~匹配过程了。
我们用i i i 指针代表当前扫描到的主字符串的位置,j j j 指针代表当前扫描到的模式串的位置。需要注意的有两点,一是出现失配、j j j 指针回退时,一定要注意判断j = 0 j=0 j = 0 的边界情况,防止死循环;二是如果恰好出现i = len ( a ) , j = len ( b ) i=\text{len}(a),j=\text{len}(b) i = len ( a ) , j = len ( b ) 的情况,应在循环结束后额外判断。
最后指出一点,洛谷的模板题实质上是MP \text{MP} MP 算法而不是KMP \text{KMP} KMP 算法,真正的KMP \text{KMP} KMP 还要对next \text{next} next 数组做一些小优化(见这篇题解 ),但是两者在复杂度上没有差别,因此不再深究。
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 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 COPY #include <iostream> #include <cstdio> #include <string> #define MAXN 1000005 using namespace std ;int nxt[MAXN]={0 };void get_next (string &s) ;int main (void ) { string a,b; int i,j; int la,lb; cin >> a >> b; get_next(b); la=a.size(),lb=b.size(); i=j=0 ; while (i<la){ if (j==lb){ cout << i-lb+1 << endl ; j=nxt[j-1 ]; } if (a[i]==b[j]) i++,j++; else { if (j) j=nxt[j-1 ]; else i++; } } if (i==la && j==lb) cout << i-lb+1 << endl ; for (int i=0 ;i<lb;i++) cout << nxt[i] << ' ' ; cout << endl ; return 0 ; } void get_next (string &s) { int l=s.size(); nxt[0 ]=0 ; for (int i=1 ;i<l;i++){ int t=nxt[i-1 ]; while (t && s[i]!=s[t]) t=nxt[t-1 ]; if (s[t]==s[i]) nxt[i]=t+1 ; else nxt[i]=0 ; } }
等一下,这篇文章还没完!如果KMP \text{KMP} KMP 只能用于字符串匹配,那我还学它干什么!(大雾)KMP \text{KMP} KMP 还有一个重要的应用:求解与恋爱 循环有关的问题。
假设我们想要求一个字符串的最小循环节,如"abcabcabcabc"的最小循环节是"abc",“aaaaaaaa"的最小循环节是"a”,朴素的方法应该是进行O ( n 2 ) \text{O}(n^{2}) O ( n 2 ) 的枚举,但是有了KMP \text{KMP} KMP (的next \text{next} next 数组),O ( n ) \text{O}(n) O ( n ) 就可以解决问题!
来看一个定理。
定理 22.33 22.33 2 2 . 3 3 恋爱循环定理
设一个字符串s [ 0... n − 1 ] \text{s}[0...n-1] s [ 0 . . . n − 1 ] 的长度为n n n ,如果next [ n − 1 ] ≠ 0 \text{next}[n-1]\neq 0 next [ n − 1 ] = 0 ,且( n − next [ n − 1 ] ) ∣ n (n-\text{next}[n-1])\mid n ( n − next [ n − 1 ] ) ∣ n ,即n ≡ 0 ( m o d n − next [ n − 1 ] ) n\equiv 0\pmod{n-\text{next}[n-1]} n ≡ 0 ( m o d n − next [ n − 1 ] ) ,那么这个字符串是真 循环的(即循环次数大于1 1 1 ),且最小循环节的长度是n − next [ n − 1 ] n-\text{next}[n-1] n − next [ n − 1 ] ,循环次数是n n − next [ n − 1 ] \frac{n}{n-\text{next}[n-1]} n − next [ n − 1 ] n 。 逆命题亦成立,即若一个真循环的字符串s [ 0... n − 1 ] \text{s}[0...n-1] s [ 0 . . . n − 1 ] 的最小循环节长度为t t t ,则next [ n − 1 ] = n − t \text{next}[n-1]=n-t next [ n − 1 ] = n − t ,且有t ∣ n t\mid n t ∣ n 。
证明: 不妨设l = next[ n − 1 ] l=\text{next[}n-1\text{]} l = next[ n − 1 ] ,设t = n − l t=n-l t = n − l ,设n = k t ( k ∈ N ) n=kt(k\in \mathbb{N}) n = k t ( k ∈ N ) 。
由于l ≠ 0 l\neq 0 l = 0 ,又由next \text{next} next 数组中元素的非负性(即l ≥ 0 l\ge 0 l ≥ 0 ),知t < n t < n t < n 。
我们又有t ∣ n t\mid n t ∣ n ,由整除的性质,我们可以得到n ≥ 2 t n\ge 2t n ≥ 2 t ,即k ≥ 2 k\ge 2 k ≥ 2 。
由于l = n − t = k t − t = ( k − 1 ) t l=n-t=kt-t=(k-1)t l = n − t = k t − t = ( k − 1 ) t ,我们知道t ∣ l t\mid l t ∣ l 。
由于t t t 是l l l 和n n n 的公因数,我们可以把s [ 0... n − 1 ] \text{s}[0...n-1] s [ 0 . . . n − 1 ] 均分成k k k 节,每一节的长度都是t t t ;也可以把s [ 0... l − 1 ] \text{s}[0...l-1] s [ 0 . . . l − 1 ] (最长公共前缀)和s [ n − l . . . n − 1 ] \text{s}[n-l...n-1] s [ n − l . . . n − 1 ] (最长公共后缀)均分成k − 1 k-1 k − 1 节,每一节的长度也都是t t t 。
下面我们把s [ 0... n − 1 ] \text{s}[0...n-1] s [ 0 . . . n − 1 ] 被均分成的k k k 节称作a 1 , a 2 , ⋯ , a k a_{1},a_{2},\cdots,a_{k} a 1 , a 2 , ⋯ , a k ,把s [ 0... l − 1 ] \text{s}[0...l-1] s [ 0 . . . l − 1 ] 被均分成的k − 1 k-1 k − 1 节称作b 1 , b 2 , ⋯ , b k − 1 b_{1},b_{2},\cdots,b_{k-1} b 1 , b 2 , ⋯ , b k − 1 ,把s [ n − l . . . n − 1 ] \text{s}[n-l...n-1] s [ n − l . . . n − 1 ] 被均分成的k − 1 k-1 k − 1 节称作c 1 , c 2 , ⋯ , c k − 1 c_{1},c_{2},\cdots,c_{k-1} c 1 , c 2 , ⋯ , c k − 1 。
由next \text{next} next 数组的定义,我们知道b 1 = c 1 , b 2 = c 2 , ⋯ , b k − 1 = c k − 1 b_{1}=c_{1},b_{2}=c_{2},\cdots,b_{k-1}=c_{k-1} b 1 = c 1 , b 2 = c 2 , ⋯ , b k − 1 = c k − 1 。
经过仔细比对下标范围~~(其实不那么仔细也可以)~~,我们又发现a 1 a_{1} a 1 和b 1 b_{1} b 1 是同一个字符串(即它们代表着s \text{s} s 字符串中同一个字串,不只是内容相同,位置也完全相同)。
经过归纳得出a i a_{i} a i 和b i b_{i} b i 是同一个字符串,其中i ∈ { i ∈ N ∣ 1 ≤ i ≤ k − 1 } i\in \{i\in \mathbb{N} \mid 1\le i \le k-1\} i ∈ { i ∈ N ∣ 1 ≤ i ≤ k − 1 } 。同理,a i + 1 a_{i+1} a i + 1 和c i c_{i} c i 是同一个字符串,其中i ∈ { i ∈ N ∣ 1 ≤ i ≤ k − 1 } i\in \{i\in \mathbb{N} \mid 1\le i \le k-1\} i ∈ { i ∈ N ∣ 1 ≤ i ≤ k − 1 } 。
由b 1 = c 1 b_{1}=c_{1} b 1 = c 1 我们知道a 1 = a 2 a_{1}=a_{2} a 1 = a 2 ,由b 2 = c 2 b_{2}=c_{2} b 2 = c 2 我们知道a 2 = a 3 a_{2}=a_{3} a 2 = a 3 ,由b k − 1 = c k − 1 b_{k-1}=c_{k-1} b k − 1 = c k − 1 我们知道a k − 1 = a k a_{k-1}=a_{k} a k − 1 = a k ,因此a 1 = a 2 = ⋯ = a k a_{1}=a_{2}=\cdots=a_{k} a 1 = a 2 = ⋯ = a k 。即s [ 0... t − 1 ] \text{s}[0...t-1] s [ 0 . . . t − 1 ] 是s \text{s} s 的一个循环节。
如何证明这是最小循环节呢?我们考察一个循环字符串,设它的最小循环节为"a",设"a"的长度为t ′ t' t ′ ,那么整个循环字符串可以表示为"aaaa…aa"(k k k 个"a"),则n = k t ′ n=kt' n = k t ′ 。容易发现,这个字符串的最长公共前后缀是"aaaa…a"(k − 1 k-1 k − 1 个"a"),即l = ( k − 1 ) t ′ l=(k-1)t' l = ( k − 1 ) t ′ ,此时有t = n − l = t ′ t=n-l=t' t = n − l = t ′ ,因此我们按照上面的方法计算出的循环节长度t t t 和实际最小循环节长度t ′ t' t ′ 是相等的,这就说明我们计算出的t t t 正是最小循环节长度。
循环次数显然为n t \frac{n}{t} t n 。
整理可知,原命题和逆命题都已证毕。
下面附一张图以方便理解。
现在我们再来解决一个问题。如果一个字符串现在不是真循环字符串,我们现在可以在它的末尾补上若干个字符以使它成为真循环字符串,那么我们至少要补多少个字符呢?
这个问题其实也不难。要让它成为真循环字符串,只要让它满足恋爱 循环定理的条件即可,即让( n − next [ n − 1 ] ) ∣ n (n-\text{next}[n-1])\mid n ( n − next [ n − 1 ] ) ∣ n 成立。很显然,我们应该努力让next [ n − 1 ] \text{next}[n-1] next [ n − 1 ] 尽量增大,因此我们补的字符应该是最大公共前缀以后的字符,例如"abcabcefgabcabc"的最大公共前缀是"abcabc",那么我们就应该给它补上"efg"。这么补的结果就是,n n n 和l l l 同时增大相同的数值,于是t t t 的值不变。
现在问题就转化成:求一个最小的正整数x x x ,使得t ∣ ( n + x ) t\mid (n+x) t ∣ ( n + x ) 。答案很显然,x = t − n % t x=t-n\%t x = t − n % t 。于是我们就知道,至少要补上t − n % t t-n\%t t − n % t 个字符,才能使这个字符串成为真循环字符串。问题解决。
总之,KMP \text{KMP} KMP 算法能够快速进行字符串的匹配,而且next \text{next} next 数组也有许多神奇的功能,因此我们应该很好地掌握KMP \text{KMP} KMP (大雾)。
练习
KMP \text{KMP} KMP 模板求周期模板题 (数据范围比较小,但是可以作为练习)next \text{next} next 数组的应用1 (此题使用倍增算法也可以勉强过,这里涉及到一个倍增数组(二维数组)的维数优化技巧。)next \text{next} next 数组的应用2 (此题中的周期和上文所述的周期不同。记得开long long \text{long long} long long ! )
上述两个next \text{next} next 数组的应用主要是用next \text{next} next 数组遍历所有公共前后缀,即可以把next \text{next} next 数组的内容理解为一棵“公共前后缀树”。
v1.4.18