字符串
KMP 模式匹配
K M P \mathrm{KMP} KMP 算法, 又称模式匹配算法, 能够在线性时间内判定字符串 A [ 1 ∼ N ] A[1 \sim N] A [ 1 ∼ N ] 是否为字符串 B [ 1 ∼ M ] B[1 \sim M] B [ 1 ∼ M ] 的子串, 并求出字符串 A A A 在字符串 B B B 中各次出现的位置。
首先, 一个 O ( N M ) \mathrm{O}(N M) O ( NM ) 的朴素做法是, 尝试枚举字符串 B B B 中的每个位置 i i i , 把字符串 A A A 与字符串 B B B 的后缀 B [ i ∼ M ] B[i \sim M] B [ i ∼ M ] 对齐, 向后扫描逐一比较 A [ 1 ] A[1] A [ 1 ] 与 B [ i ] , A [ 2 ] B[i], A[2] B [ i ] , A [ 2 ] 与 B [ i + 1 ] ⋯ B[i+1] \cdots B [ i + 1 ] ⋯ 是否相等。我们把这种比较过程称为 A A A 与 B B B 尝试进行 “匹配”。
其次, 这个问题使用字符串 Hash 也能在线性时间内求解。通过《Hash》一文中的 “兔子与兔子” 这道例题, 我们已经知道: 可以在 O ( N ) O(N) O ( N ) 的时间内预处理一个字符串的所有前缀 Hash 值, 并在 O ( 1 ) O(1) O ( 1 ) 的时间内查询该字符串任意一个子串的 Hash 值。所以, 一个很直接的想法是: 枚举字符串 B B B 中的每个位置 i ( N ≤ i ≤ M ) i(N \leq i \leq M) i ( N ≤ i ≤ M ) , 检查字符串 A A A 的 Hash 值与符串 B B B 的子串 B [ i − N + 1 ∼ i ] B[i-N+1 \sim i] B [ i − N + 1 ∼ i ] 的 Hash 值是否相同, 即可得到该问题的解。
KMP 算法能够更高效、更准确地处理这个问题, 并且能为我们提供一些额外的信息。详细地讲, KMP 算法分为两步:
对字符串 A A A 进行自我 “匹配”, 求出一个数组 n e x t next n e x t , 其中 n e x t [ i ] next[i] n e x t [ i ] 表示 “ A A A 中以 i i i 结尾的非前缀子串” 与 “ A A A 的前缀” 能够匹配的最长长度, 即:
next [ i ] = max { j } , 其中 j < i 并且 A [ i − j + 1 ∼ i ] = A [ 1 ∼ j ] \operatorname{next}[i]=\max \{j\}, \text { 其中 } j<i \text { 并且 } A[i-j+1 \sim i]=A[1 \sim j]
next [ i ] = max { j } , 其中 j < i 并且 A [ i − j + 1 ∼ i ] = A [ 1 ∼ j ]
特别地, 当不存在这样的 j j j 时, 令 next [ i ] = 0 \operatorname{next}[i]=0 next [ i ] = 0 。
2. 对字符串 A A A 与 B B B 进行匹配, 求出一个数组 f f f , 其中 f [ i ] f[i] f [ i ] 表示 “ B B B 中以 i i i 结尾的子串” 与 “ A A A 的前缀” 能够匹配的最长长度, 即:
f [ i ] = max { j } , 其中 j ≤ i 并且 B [ i − j + 1 ∼ i ] = A [ 1 ∼ j ] f[i]=\max \{j\}, \text{ 其中 } j \leq i \text{ 并且 } B[i-j+1 \sim i]=A[1 \sim j]
f [ i ] = max { j } , 其中 j ≤ i 并且 B [ i − j + 1 ∼ i ] = A [ 1 ∼ j ]
下面讨论 n e x t next n e x t 数组的计算方法。根据定义, n e x t [ 1 ] = 0 n e x t[1]=0 n e x t [ 1 ] = 0 。接下来我们按照 i = 2 ∼ N i=2 \sim N i = 2 ∼ N 的顺序依次计算 n e x t [ i ] next[i] n e x t [ i ] 。
假设 n e x t [ 1 ∼ i − 1 ] next[1 \sim i-1] n e x t [ 1 ∼ i − 1 ] 已经计算完毕, 当计算 n e x t [ i ] next[i] n e x t [ i ] 时, 根据定义, 我们需要找出所有满足 j < i j<i j < i 且 A [ i − j + 1 ∼ i ] = A [ 1 ∼ j ] A[i-j+1 \sim i]=A[1 \sim j] A [ i − j + 1 ∼ i ] = A [ 1 ∼ j ] 的整数, j j j 并取最大值。为了敘述方便, 我们称满足这两个条件的 j j j 为 n e x t [ i ] next[i] n e x t [ i ] 的 “候选项”。
使用朴素算法计算 n e x t next n e x t 数组
朴素的做法是枚举 j ∈ [ 1 , i − 1 ] j \in[1, i-1] j ∈ [ 1 , i − 1 ] , 并检查 A [ i − j + 1 ∼ i ] A[i-j+1 \sim i] A [ i − j + 1 ∼ i ] 与 A [ 1 ∼ j ] A[1 \sim j] A [ 1 ∼ j ] 是否相等。以下图中的字符串为例,当 i = 7 i=7 i = 7 时,需要枚举下列 6 种情况:
A [ 2 ∼ 7 ] = A[2 \sim 7]= A [ 2 ∼ 7 ] = “bababa”, 它与前缀 A [ 1 ∼ 6 ] = A[1 \sim 6]= A [ 1 ∼ 6 ] = “ababab” 不相等。
A [ 3 ∼ 7 ] = A[3 \sim 7]= A [ 3 ∼ 7 ] = “ababa”, 它与前缀 A [ 1 ∼ 5 ] = A[1 \sim 5]= A [ 1 ∼ 5 ] = “ababa” 相等, 长度为 5 。
A [ 4 ∼ 7 ] = A[4 \sim 7]= A [ 4 ∼ 7 ] = “baba”, 它与前缀 A [ 1 ∼ 4 ] = A[1 \sim 4]= A [ 1 ∼ 4 ] = “abab” 不相等。
A [ 5 ∼ 7 ] = A[5 \sim 7]= A [ 5 ∼ 7 ] = “aba”, 它与前缀 A [ 1 ∼ 3 ] = A[1 \sim 3]= A [ 1 ∼ 3 ] = “aba” 相等, 长度为 3 。
A [ 6 ∼ 7 ] = A[6 \sim 7]= A [ 6 ∼ 7 ] = “ba”, 它与前缀 A [ 1 ∼ 2 ] = A[1 \sim 2]= A [ 1 ∼ 2 ] = “ab” 不相等。
A [ 7 ∼ 7 ] = A[7 \sim 7]= A [ 7 ∼ 7 ] = “a”, 它与前缀 A [ 1 ∼ 1 ] = A[1 \sim 1]= A [ 1 ∼ 1 ] = “a” 相等, 长度为 1 。
所以, 以 i = 7 i=7 i = 7 结尾, 最多与 A A A 的前缀匹配到 5 , n e x t [ 7 ] = 5 next[7]=5 n e x t [ 7 ] = 5 。
该算法对每个 i i i 枚举了 i − 1 i-1 i − 1 个非前缀子串, 并检查与对应前缀的匹配情况, 时间复杂度不会低于 O ( N 2 ) O\left(N^{2}\right) O ( N 2 ) 。能否更快地求出 n e x t next n e x t 呢?
引理
若 j 0 j_{0} j 0 是 next [ i ] \operatorname{next}[i] next [ i ] 的一个 “候选项”, 即 j 0 < i j_{0}<i j 0 < i 且 A [ i − j 0 + 1 ∼ i ] = A [ 1 ∼ j 0 ] A\left[i-j_{0}+1 \sim i\right]=A\left[1 \sim j_{0}\right] A [ i − j 0 + 1 ∼ i ] = A [ 1 ∼ j 0 ] , 则小于 j 0 j_{0} j 0 的最大的 n e x t [ i ] n e x t[i] n e x t [ i ] 的 “候选项” 是 n e x t [ j 0 ] n e x t\left[j_{0}\right] n e x t [ j 0 ] 。换言之, n e x t [ j 0 ] + 1 ∼ j 0 − 1 n e x t\left[j_{0}\right]+1 \sim j_{0}-1 n e x t [ j 0 ] + 1 ∼ j 0 − 1 之间的数都不是 n e x t [ i ] next[i] n e x t [ i ] 的 “候选项”。
反证法证明:
假设存在 n e x t [ j 0 ] < j 1 < j 0 n e x t\left[j_{0}\right]<j_{1}<j_{0} n e x t [ j 0 ] < j 1 < j 0 使得 j 1 j_{1} j 1 为 n e x t [ i ] n e x t[i] n e x t [ i ] 的 “候选项”, 即 A [ i − j 1 + 1 ] = A [ 1 ∼ j 1 ] A[i-\left.j_{1}+1\right]=A\left[1 \sim j_{1}\right] A [ i − j 1 + 1 ] = A [ 1 ∼ j 1 ] 。如下图所示, 颜色相同的阴影部分代表相等的子串。
分别取 A [ i − j 0 + 1 ∼ i ] A\left[i-j_{0}+1 \sim i\right] A [ i − j 0 + 1 ∼ i ] 和 A [ 1 ∼ j 0 ] A\left[1 \sim j_{0}\right] A [ 1 ∼ j 0 ] 的后 j 1 j_{1} j 1 个字符, 显然也相等, 即 A [ i − j 1 + 1 ∼ i ] = A [ j 0 − j 1 + 1 ∼ j 0 ] A\left[i-j_{1}+\right. 1 \sim i]=A\left[j_{0}-j_{1}+1 \sim j_{0}\right] A [ i − j 1 + 1 ∼ i ] = A [ j 0 − j 1 + 1 ∼ j 0 ] 从而 A [ j 0 − j 1 + 1 ∼ j 0 ] = A [ 1 ∼ j 1 ] A\left[j_{0}-j_{1}+1 \sim j_{0}\right]=A\left[1 \sim j_{1}\right] A [ j 0 − j 1 + 1 ∼ j 0 ] = A [ 1 ∼ j 1 ] , 与 n e x t [ j 0 ] n e x t\left[j_{0}\right] n e x t [ j 0 ] “最大” 的性质矛盾。故假设不成立。
另一方面, n e x t [ j 0 ] next\left[j_{0}\right] n e x t [ j 0 ] 显然是 n e x t [ i ] n e x t[i] n e x t [ i ] 的“候选项”。
证毕。
使用优化的算法计算 n e x t next n e x t 数组
根据引理, 当 next [ i − 1 ] \operatorname{next}[i-1] next [ i − 1 ] 计算完毕时, 我们即可得知 next [ i − 1 ] \operatorname{next}[i-1] next [ i − 1 ] 的所有 “候选项” 从大到小依次是 next [ i − 1 ] \operatorname{next}[i-1] next [ i − 1 ] , next [ n e x t [ i − 1 ] ] \operatorname{next}[ next [i-1]] next [ n e x t [ i − 1 ]] , next [ n e x t [ n e x t [ i − 1 ] ] ] ⋯ \operatorname{next}[ next [ next [i-1]]] \cdots next [ n e x t [ n e x t [ i − 1 ]]] ⋯ 而如果一个整数 j j j 是 n e x t [ i ] next[i] n e x t [ i ] 的 “候选项”, 那么 j − 1 j-1 j − 1 显然也必须是 n e x t [ i − 1 ] next[i-1] n e x t [ i − 1 ] 的 “候选项” (两个字符串 A [ i − j + 1 ∼ i ] A[i-j+1 \sim i] A [ i − j + 1 ∼ i ] 和 A [ 1 ∼ j ] A[1 \sim j] A [ 1 ∼ j ] 相等的前提是 A [ i − j + 1 ∼ i − 1 ] A[i-j+1 \sim i-1] A [ i − j + 1 ∼ i − 1 ] 和 A [ 1 ∼ j − 1 ] A[1 \sim j-1] A [ 1 ∼ j − 1 ] 相等)。因此, 在计算 n e x t [ i ] next[i] n e x t [ i ] 时, 只需把 next [ i − 1 ] + 1 , n e x t [ n e x t [ i − 1 ] ] + 1 , next [ next [ next [ i − 1 ] ] ] + 1 ⋯ \operatorname{next}[i-1]+1, next [ next [i- 1]]+1, \operatorname{next}[\operatorname{next}[\operatorname{next}[i-1]]]+1 \cdots next [ i − 1 ] + 1 , n e x t [ n e x t [ i − 1 ]] + 1 , next [ next [ next [ i − 1 ]]] + 1 ⋯ 作为 j j j 的选项即可。
仍然使用朴素算法中的例子。假设 n e x t [ 1 ∼ 6 ] next[1 \sim 6] n e x t [ 1 ∼ 6 ] 已经被求出。按照定义, next [ 6 ] = \operatorname{next}[6]= next [ 6 ] = 4, 即 A [ 3 ∼ 6 ] A[3 \sim 6] A [ 3 ∼ 6 ] 与 A [ 1 ∼ 4 ] A[1 \sim 4] A [ 1 ∼ 4 ] 匹配。
接下来 A [ 7 ] = A [ 5 ] = ‘a’ A[7]=A[5]=\text{`a'} A [ 7 ] = A [ 5 ] = ‘a’ , 在该字符上能够继续匹配, 由 n e x t [ 6 ] next[6] n e x t [ 6 ] 匹配长度的最优性可知, 在字符 7 处继续匹配, n e x t [ 7 ] = 5 next[7] = 5 n e x t [ 7 ] = 5 就是最优解。
我们继续考虑 n e x t [ 8 ] next [8] n e x t [ 8 ] 。此时发现 A [ 8 ] = ‘a’ A[8]=\text{`a'} A [ 8 ] = ‘a’ 与 A [ 6 ] = ‘a’ A[6]=\text{`a'} A [ 6 ] = ‘a’ 二者不相等, 不能继续把匹配长度从 5 增长到 6 。我们只好把匹配长度缩短。由引理可知, 以 i = 7 i=7 i = 7 为结尾的匹配长度除了 j = n e x t [ 7 ] = 5 j=next[7]=5 j = n e x t [ 7 ] = 5 之外, 还有 n e x t [ 5 ] = 3 n e x t[5]=3 n e x t [ 5 ] = 3 和 n e x t [ 3 ] = 1 next[3]=1 n e x t [ 3 ] = 1 。我们依次尝试这两种稍短一些的匹配能否延伸到 i = 8 i=8 i = 8 。
然而, A [ 8 ] A[8] A [ 8 ] 与 A [ 4 ] , A [ 8 ] A[4], A[8] A [ 4 ] , A [ 8 ] 与 A [ 2 ] A[2] A [ 2 ] 都不相等, 无法延伸, 所以我们只能让 i = 8 i=8 i = 8 从 A A A 字符串开头重新开始匹配, A [ 8 ] = A [ 1 ] A[8]=A[1] A [ 8 ] = A [ 1 ] , 匹配长度为 1 , n e x t [ 8 ] = 1 next[8]=1 n e x t [ 8 ] = 1 。
在讨论优化后的算法的时间复杂度之前, 我们先写出算法实现的框架和代码。
KMP 算法 n e x t next n e x t 数组的求法
初始化 n e x t [ 1 ] = j = 0 next[1]=j=0 n e x t [ 1 ] = j = 0 , 假设 n e x t [ 1 ∼ i − 1 ] next[1 \sim i-1] n e x t [ 1 ∼ i − 1 ] 已求出, 下面求解 next [ i ] \operatorname{next}[i] next [ i ] 。
不断尝试扩展匹配长度 j j j , 如果扩展失败 (下一个字符不相等), 令 j j j 变为 n e x t [ j ] n e x t[j] n e x t [ j ] , 直至 j j j 为 0 (应该重新从头开始匹配)。
如果能够扩展成功, 匹配长度 j j j 就增加 1 。n e x t [ i ] next[i] n e x t [ i ] 的值就是 j j j 。
1 2 3 4 5 6 ne[0 ] = ne[1 ] = 0 ; for (int i = 2 , j = 0 ; i <= n; ++i) { while (j && a[i] != a[j + 1 ]) j = ne[j]; if (a[i] == a[j + 1 ]) ++j; ne[i] = j; }
KMP 算法 f f f 数组的求法
因为定义的相似性, 求解 f f f 与求解 n e x t next n e x t 的过程是基本一致的。
1 2 3 4 5 6 7 8 9 for (int i = 1 , j = 0 ; i <= m; ++i) { while (j && b[i] != a[j + 1 ]) j = ne[j]; if (b[i] == a[j + 1 ]) ++j; f[i] = j; if (f[i] == n) { j = ne[j]; } }
这就是 KMP 模式匹配算法。在上面代码的 while 循环中, j j j 的值不断减小, j = j= j = n e x t [ j ] n e x t[j] n e x t [ j ] 的执行次数不会超过每层 for 循环开始时 j j j 的值与 while 循环结束时 j j j 的值之差。而在每层 for 循环中, j j j 的值至多增加 1 。因为 j j j 始终非负, 所以在整个计算过程中, j j j 减小的幅度总和不会超过 j j j 增加的幅度总和。故 j j j 的总变化次数至多为 2 ( N + M ) 2(N+M) 2 ( N + M ) 。整个算法的时间复杂度为 O ( N + M ) O(N+M) O ( N + M ) 。
一个字符串的前缀是从第一个字符开始的连续若干个字符,例如 abaab
共有 5 5 5 个前缀,分别是 a
,ab
,aba
,abaa
,abaab
。
我们希望知道一个 N N N 位字符串 S S S 的前缀是否具有循环节。
换言之,对于每一个从头开始的长度为 i i i (i > 1 i>1 i > 1 )的前缀,是否由重复出现的子串 A A A 组成,即 A A A … A AAA…A AAA … A (A A A 重复出现 K K K 次,K > 1 K>1 K > 1 )。
如果存在,请找出最短的循环节对应的 K K K 值(也就是这个前缀串的所有可能重复节中,最大的 K K K 值)。
输入格式
输入包括多组测试数据,每组测试数据包括两行。
第一行输入字符串 S S S 的长度 N N N 。
第二行输入字符串 S S S 。
输入数据以只包括一个 0 0 0 的行作为结尾。
输出格式
对于每组测试数据,第一行输出 Test case #
和测试数据的编号。
接下来的每一行,输出具有循环节的前缀的长度 i i i 和其对应 K K K ,中间用一个空格隔开。
前缀长度需要升序排列。
在每组测试数据的最后输出一个空行。
数据范围
2 ≤ N ≤ 1000000 2 \le N \le 1000000 2 ≤ N ≤ 1000000
输入样例:
1 2 3 4 5 6 7 3 aaa 4 abcd 12 aabaabaabaab 0
输出样例:
1 2 3 4 5 6 7 8 9 10 11 Test case #1 2 2 3 3 Test case #2 Test case #3 2 2 6 2 9 3 12 4
算法分析
对字符串 S S S 自匹配求出 n e x t next n e x t 数组, 根据定义, 对于每个 i , S [ i − n e x t [ i ] + 1 ∼ i ] i, S[i-next[i]+1 \sim i] i , S [ i − n e x t [ i ] + 1 ∼ i ] 与 S [ 1 ∼ n e x t [ i ] ] S[1 \sim n e x t[i]] S [ 1 ∼ n e x t [ i ]] 是相等的, 并且不存在更大的 n e x t next n e x t 值满足这个条件。
引理:
S [ 1 ∼ i ] S[1 \sim i] S [ 1 ∼ i ] 具有长度为 l e n < i l e n<i l e n < i 的循环元的充要条件是 l e n len l e n 能整除 i i i 并且 S [ l e n + 1 ∼ i ] = S [ 1 ∼ i − l e n ] S[len+ 1\sim i]=S[1 \sim i-l e n] S [ l e n + 1 ∼ i ] = S [ 1 ∼ i − l e n ] (即 i − l e n i-l e n i − l e n 是 n e x t [ i ] n e x t[i] n e x t [ i ] 的 “候选项”, 在讲解 KMP 算法时我们提到了 “候选项” 的含义)。
证明:
先证必要性。设 S [ 1 ∼ i ] S[1 \sim i] S [ 1 ∼ i ] 具有长度为 l e n len l e n 的循环元, 显然 l e n len l e n 能整除 i i i , 并且 S [ 1 ∼ i − l e n ] S[1 \sim i-l e n] S [ 1 ∼ i − l e n ] 和 S [ l e n + 1 ∼ i ] S[l e n+1 \sim i] S [ l e n + 1 ∼ i ] 都是由 i / l e n − 1 i / l e n-1 i / l e n − 1 个循环元构成的。故 S [ 1 ∼ i − l e n ] = S [ l e n + 1 ∼ i ] S[1 \sim i- len ]=S[ len +1 \sim i] S [ 1 ∼ i − l e n ] = S [ l e n + 1 ∼ i ] 。
再证充分性。设 l e n len l e n 能整除 i i i 并且 S [ l e n + 1 ∼ i ] = S [ 1 ∼ i − l e n ] S[ len +1 \sim i]=S[1 \sim i- len ] S [ l e n + 1 ∼ i ] = S [ 1 ∼ i − l e n ] 。因为 l e n < i len < i l e n < i , 所以 S [ 1 ∼ i − l e n ] S[1 \sim i-len ] S [ 1 ∼ i − l e n ] 和 S [ l e n + 1 ∼ i ] S[len +1 \sim i] S [ l e n + 1 ∼ i ] 的长度不小于 l e n len l e n 且是 l e n len l e n 的倍数。二者各取前 l e n len l e n 个字符, 有 S [ 1 ∼ l e n ] = S [ l e n + 1 ∼ 2 ∗ l e n ] S[1 \sim l e n]=S[l e n+1 \sim 2 * l e n] S [ 1 ∼ l e n ] = S [ l e n + 1 ∼ 2 ∗ l e n ] 。依此类推, 不断向后取 l e n len l e n 个 字符, 可以发现 S [ 1 ∼ i − l e n ] S[1 \sim i-l e n] S [ 1 ∼ i − l e n ] 和 S [ l e n + 1 ∼ i ] S[l e n+1 \sim i] S [ l e n + 1 ∼ i ] 是以 l e n len l e n 为间隔错位对齐的, 如下图所示。故 S [ 1 ∼ l e n ] S[1 \sim l e n] S [ 1 ∼ l e n ] 是 S S S 的循环元。
证毕。
根据引理, 当 i − n e x t [ i ] i-n e x t[i] i − n e x t [ i ] 能整除 i i i 时, S [ 1 ∼ i − n e x t [ i ] ] S[1 \sim i-n e x t[i]] S [ 1 ∼ i − n e x t [ i ]] 就是 S [ 1 ∼ i ] S[1 \sim i] S [ 1 ∼ i ] 的最小循环元。它的最大循环次数就是 i / ( i − n e x t [ i ] ) i /(i-next[i]) i / ( i − n e x t [ i ]) 。其中 i − n e x t [ i ] i-next[i] i − n e x t [ i ] 能整除 i i i 的条件是为了保证循环元每次重复的完整性。例如上图中 i = 7 i=7 i = 7 时, 最小循环元 “ab” 的第 4 次重复就尚未完成。
进一步地, 如果 i − n e x t [ n e x t [ i ] ] i-next[ next [i]] i − n e x t [ n e x t [ i ]] 能整除 i i i , 那么 S [ 1 ∼ i − n e x t [ n e x t [ i ] ] ] S[1 \sim i-next[next[i]]] S [ 1 ∼ i − n e x t [ n e x t [ i ]]] 就是 S [ 1 ∼ i ] S[1 \sim i] S [ 1 ∼ i ] 的次小循环元。依此类推, 我们还可以找出 S [ 1 ∼ i ] S[1 \sim i] S [ 1 ∼ i ] 所有可能的循环元。
值得指出的一个性质是, 一个字符串的任意循环元的长度必然是最小循环元长度的倍数。
Solution
最小表示法
给定一个字符串 S [ 1 ∼ n ] S[1 \sim n] S [ 1 ∼ n ] , 如果我们不断把它的最后一个字符放到开头, 最终会得到 n n n 个字符串, 称这 n n n 个字符串是循环同构的。这些字符串中字典序最小的一个, 称为字符串 S S S 的最小表示。
例如 S = S= S = “abca”, 那么它的 4 个循环同构字符串为 “abca”, “aabc”, “caab”, “bcaa”, S S S 的最小表示为 “aabc”。与 S S S 循环同构的字符串可以用该字符串在 S S S 中的起始下标表示, 因此我们用 B [ i ] B[i] B [ i ] 来表示从 i i i 开始的循环同构字符串, 即 S [ i ∼ n ] + S [ 1 ∼ i − 1 ] S[i \sim n]+S[1 \sim i-1] S [ i ∼ n ] + S [ 1 ∼ i − 1 ] 。
如何求出一个字符串的最小表示? 最朴素的方法是:按照定义, 依次比较这 n n n 个循环同构的字符串, 找到其中字典序最小的一个。比较两个循环同构字符串 B [ i ] B[i] B [ i ] 与 B [ j ] B[j] B [ j ] 时, 我们也采用直接向后扫描的方式, 依次取 k = 0 , 1 , 2 , ⋯ k=0,1,2, \cdots k = 0 , 1 , 2 , ⋯ , 比较 B [ i + k ] B[i+k] B [ i + k ] 与 B [ j + k ] B[j+k] B [ j + k ] 是否相等, 直至找到一个不相等的位置, 从而确定 B [ i ] B[i] B [ i ] 与 B [ j ] B[j] B [ j ] 的大小关系。
实际上,一个字符串的最小表示可以在 O ( n ) O(n) O ( n ) 的线性时间内求出。我们首先把 S S S 复制一份接在它的结尾, 得到的字符串记为 S S S S SS 。显然, B [ i ] = S S [ i ∼ i + n − 1 ] B[i]=S S[i \sim i+n-1] B [ i ] = SS [ i ∼ i + n − 1 ] 。
对于任意的 i , j i, j i , j , 我们仔细观察 B [ i ] B[i] B [ i ] 与 B [ j ] B[j] B [ j ] 的比较过程:
如果在 i + k i+k i + k 与 j + k j+k j + k 处发现不相等, 假设 S S [ i + k ] > S S [ j + k ] S S[i+k]>S S[j+k] SS [ i + k ] > SS [ j + k ] , 那么我们当然可以得知 B [ i ] B[i] B [ i ] 不是 S S S 的最小表示(因为存在一个更小的循环同构串 B [ j ] B[j] B [ j ] )。除此之外, 我们还可以得知 B [ i + 1 ] , B [ i + 2 ] , ⋯ , B [ i + k ] B[i+1], B[i+2], \cdots, B[i+k] B [ i + 1 ] , B [ i + 2 ] , ⋯ , B [ i + k ] 也都不是 S S S 的最小表示。这是因为对于 1 ≤ p ≤ k 1 \leq p \leq k 1 ≤ p ≤ k , 存在一个比 B [ i + p ] B[i+p] B [ i + p ] 更小的循环同构串 B [ j + p ] B[j+p] B [ j + p ] (从 i + p i+p i + p 与 j + p j+p j + p 开始向后扫描, 同样会在 p = k p=k p = k 时发现不相等, 并且 S S [ i + k ] > S S [ j + k ] S S[i+k]>S S[j+k] SS [ i + k ] > SS [ j + k ] )。
同理, 如果 S S [ i + k ] < S S [ j + k ] S S[i+k]<S S[j+k] SS [ i + k ] < SS [ j + k ] , 那么 B [ j ] , B [ j + 1 ] , ⋯ , B [ j + k ] B[j], B[j+1], \cdots, B[j+k] B [ j ] , B [ j + 1 ] , ⋯ , B [ j + k ] 都不是 S S S 的最 小表示, 直接跳过这些位置, 一定不会遗漏最小表示。
最小表示法算法流程:
初始化 i = 1 , j = 2 i=1, j=2 i = 1 , j = 2 。
通过直接向后扫描的方法, 比较 B [ i ] B[i] B [ i ] 与 B [ j ] B[j] B [ j ] 两个循环同构串。
(1) 如果扫描了 n n n 个字符后仍然相等, 说明 S S S 有更小的循环元 (例如 catcat 有循环元 cat), 并且该循环元已扫描完成, B [ min ( i , j ) ] B[\min (i, j)] B [ min ( i , j )] 即为最小表示, 算法结束。
(2) 如果在 i + k i+k i + k 与 j + k j+k j + k 处发现不相等:
若 S S [ i + k ] > S S [ j + k ] S S[i+k]>S S[j+k] SS [ i + k ] > SS [ j + k ] , 令 i = i + k + 1 i=i+k+1 i = i + k + 1 。若此时 i = j i=j i = j , 再令 i = i + 1 i=i+1 i = i + 1 。
若 S S [ i + k ] < S S [ j + k ] S S[i+k]<S S[j+k] SS [ i + k ] < SS [ j + k ] , 令 j = j + k + 1 j=j+k+1 j = j + k + 1 。若此时 i = j i=j i = j , 再令 j = j + 1 j=j+1 j = j + 1 。
若 i > n i>n i > n 或 j > n j>n j > n , 则 B [ min ( i , j ) ] B[\min (i, j)] B [ min ( i , j )] 为最小表示; 否则重复第 2 步。
该算法通过两个指针不断向后移动的形式, 尝试比较每两个循环同构串的大小。利用上面的性质, 及时排除掉不可能的选项。当其中一个移动到结尾时, 就考虑过了所有可能的二元组 ( B [ i ] , B [ j ] ) (B[i], B[j]) ( B [ i ] , B [ j ]) , 从而得到了最小表示。
如果每次比较向后扫描了 k k k 的长度, 则 i i i 或 j j j 二者之一会向后移动 k k k , 而 i i i 和 j j j 合计一共最多向后移动 2 n 2 n 2 n 的长度, 因此该算法的复杂度为 O ( n ) \mathrm{O}(n) O ( n ) 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 int n = strlen (s + 1 );for (int i = 1 ; i <= n; i++) s[n+i] = s[i];int i = 1 , j = 2 , k;while (i <= n && j <= n) { for (k = 0 ; k < n && s[i+k] == s[j+k]; k++); if (k == n) break ; if (s[i+k] > s[j+k]) { i = i + k + 1 ; if (i == j) i++; } else { j = j + k + 1 ; if (i == j) j++; } } ans = min (i, j);