0x15_字符串

0x15字符串

KMP模式匹配

KMP算法,又称模式匹配算法,能够在线性时间内判定字符串 A[1N]A[1\sim N] 是否为字符串 B[1M]B[1\sim M] 的子串,并求出字符串 AA 在字符串 BB 中各次出现的位置。

首先,一个 O(NM)O(NM) 的朴素做法是,尝试枚举字符串 BB 中的每个位置 ii ,把字符串 AA 与字符串 BB 的后缀 B[iM]B[i \sim M] 对齐,向后扫描逐一比较 A[1]A[1]B[i]B[i]A[2]A[2]B[i+1]B[i + 1] \cdots 是否相等。我们把这种比较过程称为 AABB 尝试进行“匹配”。

其次,这个问题使用字符串 Hash 也能在线性时间内求解。通过上一节中的“兔子与兔子”这道例题,我们已经知道:可以在 O(N)O(N) 的时间内预处理一个字符串的所有前缀 Hash 值,并在 O(1) 的时间内查询该字符串任意一个子串的 Hash 值。所以,一个很直接的想法是:枚举字符串 AA 中的每个位置 i(MiN)i (M \leq i \leq N) ,检查字符串 BB 的 Hash 值与字符串 AA 的子串 A[iM+1i]A[i - M + 1 \sim i] 的 Hash 值是否相同,即可得到该问题的解。

KMP算法能够更高效、更准确地处理这个问题,并且能为我们提供一些额外的信息。详细地讲,KMP算法分为两步:

  1. 对字符串 AA 进行自我“匹配”,求出一个数组 nextnext ,其中 next[i]next[i] 表示“A中以 ii 结尾的非前缀子串”与“A的前缀”能够匹配的最长长度,即:

next[i]=max{j},其 中j<i并 且A[ij+1i]=A[1j]n e x t [ i ] = \max \{j \}, \text {其 中} j < i \text {并 且} A [ i - j + 1 \sim i ] = A [ 1 \sim j ]
  1. 对字符串 AABB 进行匹配,求出一个数组 ff ,其中 f[i]f[i] 表示“ BB 中以 ii 结尾的子串”与“ AA 的前缀”能够匹配的最长长度,即:

f[i]=max{j},其 中ji并 且B[ij+1i]=A[1j]f [ i ] = \max \{j \}, \text {其 中} j \leq i \text {并 且} B [ i - j + 1 \sim i ] = A [ 1 \sim j ]

i=7i = 7 为结尾的“非前缀子串”有6个,分别是 A[27],A[37],,A[77]A[2\sim 7], A[3\sim 7], \dots, A[7\sim 7] ,如下图所示(因为 A[17]A[1\sim 7] 是前缀,所以不算在内)。如果使用朴素算法求 next[7],那么根据 next 的定义,我们可以枚举下列几种情况:

A[27]="bababa"A[2\sim 7] = "\mathrm{bababa}" ,它与前缀 A[16]="ababab"A[1\sim 6] = "\mathrm{ababab}" 不匹配。

A[37]="ababa"A[3\sim 7] = "\mathrm{ababa}" ,它与前缀 A[15]="ababa"A[1\sim 5] = \text{"ababa"} 匹配,长度为5。

A[47]="baba"A[4\sim 7] = \text{"baba"} ,它与前缀 A[14]="abab"A[1\sim 4] = \text{"abab"} 不匹配。

A[57]="aba"A[5\sim 7] = \text{"aba"} ,它与前缀 A[13]="aba"A[1\sim 3] = \text{"aba"} 匹配,长度为3。

A[67]="ba"A[6\sim 7] = "ba" ,它与前缀 A[12]="ab"A[1\sim 2] = "ab" 不匹配。

A[77]="a"A[7\sim 7] = \text{"a"} ,它与前缀 A[11]="a"A[1\sim 1] = \text{"a"} 匹配,长度为1。

所以,以 i=7i = 7 结尾,最多与 AA 的前缀匹配到5,next[7] =5= 5

123456789A=a b a b a b a ca b a b a b a a cj123456789n e x t [ 7 ] = 5\begin{array}{r l} & 1 2 3 4 5 6 7 8 9 \\ A = \text {a b a b a b a c} \\ & \frac {\text {a b a b a b a a c}}{j} \\ & 1 2 3 4 5 6 7 8 9 \\ & \text {n e x t [ 7 ] = 5} \end{array}

能否更快地求出 next 呢?我们不妨假设 next[16]\text{next}[1 \sim 6] 已经被求出。按照上述定义, next[6]=4\text{next}[6] = 4 ,即 A[36]A[3 \sim 6]A[14]A[1 \sim 4] 匹配。

接下来 A[7]=A[5]="a"A[7] = A[5] = \text{"a"} ,在该字符上能够继续匹配,由next[6]匹配长度的最优性可知,在字符7处继续匹配,next[7] =5= 5 就是最优解。

我们继续考虑 next[8]next[8] 。此时发现 A[8]="a"A[8] = \text{"a"}A[6]="b"A[6] = \text{"b"} 二者不相等,不能继续把匹配长度从5增长到6。我们只好把匹配长度缩短。以 i=7i = 7 为结尾的匹配长度除了 j=5j = 5 之外, A[57]A[5\sim 7]A[13]A[1\sim 3] 还能进行长度为3的匹配, A[77]A[7\sim 7]A[11]A[1\sim 1] 还能进行长度为1的匹配。我们依次尝试这两种稍短一些的匹配能否延伸到 i=8i = 8

然而, A[8]A[8]A[4]A[4]A[8]A[8]A[2]A[2] 都不相等,无法延伸,所以我们只能让 i=8i = 8AA 字符串开头重新开始匹配, A[8]=A[1]A[8] = A[1] ,匹配长度为 1, next[8]=1\text{next}[8] = 1

123456789A=abababaacabababaacabababaacabababaacabababaacj123456789F a i l(next[7]=5)F a i l(next[5]=3)F a i l(next[3]=1)M a t c h(next[1]=0)\begin{array}{l} 1 2 3 4 5 6 7 8 9 \\ A = a b \underline {{a}} b a b a \underline {{a}} c \\ \underline {{a}} b a b a b a a c \\ \underline {{a}} b a b a b a a c \\ \underline {{a}} b a b a b a a c \\ \underline {{a}} b a b a b a a c \\ j \\ 1 2 3 4 5 6 7 8 9 \end{array} \quad \text {F a i l} \quad (n e x t [ 7 ] = 5) \quad \text {F a i l} \quad (n e x t [ 5 ] = 3) \quad \text {F a i l} \quad (n e x t [ 3 ] = 1) \quad \text {M a t c h} \quad (n e x t [ 1 ] = 0)

那么我们是如何得知要考虑5,3,1这些匹配长度的呢?首先, next[7]=5next[7] = 5 ,这说

明从7往前的5个字符与 A[15]A[1\sim 5] 是相等的。如果存在一个新的 jj ,使得从5往前的 jj 个字符与 A[1j]A[1\sim j] 是相等的,那么从7往前的 jj 个字符与 A[1j]A[1\sim j] 也是相等的。这样的 jj 最大是多少呢?当然就是以5为结尾能匹配的最长前缀长度,即next[5]!

同理,在考虑完 j=next[5]=3j = \text{next}[5] = 3 之后,下一个要考虑的匹配长度就是 next[3]。依此类推,我们得到了以下算法:

KMP算法 next数组的求法

  1. 初始化 next[1]=j=0next[1] = j = 0 ,假设 next[1i1]next[1 \sim i - 1] 已求出,下面求解 next[i]next[i]

  2. 不断尝试扩展匹配长度 jj ,如果扩展失败(下一个字符不相等),令 jj 变为 next[j],直至 jj 为 0(应该重新从头开始匹配)。

  3. 如果能够扩展成功,匹配长度 jj 就增加 1。next[i] 的值就是 jj

next[1] = 0;  
for (int i = 2, j = 0; i <= n; i++) {  
    while (j > 0 && a[i] != a[j+1]) j = next[j];  
    if (a[i] == a[j+1]) j++;  
    next[i] = j;  
}

因为定义的相似性,求解 ff 与求解 next 的过程是基本一致的。

KMP算法  $f$  数组的求法  
for(int  $\mathrm{i} = 1$  ,  $\mathrm{j} = 0$  ;i<=m;i++){while  $(j > 0\& \& (j == n||b[i] != a[j + 1]))j = next[j];$  if  $(\mathtt{b}[i] == \mathtt{a}[j + 1])j + + ;$  (20 $f[i] = j;$  //if  $(f[i] == n)$  ,此时就是  $A$  在  $B$  中的某一次出现

这就是KMP模式匹配算法,其时间复杂度为 O(N+M)O(N + M) ,这里我们就不再证明了。

【例题】Period POJ1961

如果一个字符串 SS 是由一个字符串 TT 重复 KK 次形成的,则称 TTSS 的循环元。使 KK 最大的字符串 TT 称为 SS 的最小循环元,此时的 KK 称为最大循环次数。

现在给定一个长度为 NN 的字符串 SS ,对 SS 的每一个前缀 S[1i]S[1 \sim i] ,如果它的最大循环次数大于 1,则输出该前缀的最小循环元长度和最大循环次数。

对字符串 SS 自匹配求出 next 数组, 根据定义, 对于每个 ii , S[inext[i]+1i]S[i - next[i] + 1 \sim i]

S[1next[i]]S[1 \sim next[i]] 是相等的,并且不存在更大的 next 值满足这个条件。

12a b a b a b a ba b a b a b a b12345678inext[8]=6S[38]=S[16]S[12]=S[34]=S[56]=S[78]\begin{array}{l l} 1 & 2 \\ \text {a b a b a b a b} \\ \hline & \\ & \frac {\text {a b a b a b a b}}{1 2 3 4 5 6 7 8} \end{array} \quad \begin{array}{l l} i \\ n e x t [ 8 ] = 6 \\ \Rightarrow S [ 3 \sim 8 ] = S [ 1 \sim 6 ] \\ \Rightarrow S [ 1 \sim 2 ] = S [ 3 \sim 4 ] = S [ 5 \sim 6 ] = S [ 7 \sim 8 ] \end{array}

如上图所示,仔细分析 next 数组的含义与计算过程,可以发现:当 inext[i]i - \text{next}[i] 能整除 ii 时, S[1inext[i]]S[1 \sim i - \text{next}[i]] 就是 S[1i]S[1 \sim i] 的最小循环元。它的最大循环次数就是 i/(inext[i])i / (i - \text{next}[i])

在上面这个结论中, inext[i]i - next[i] 能整除 ii 的条件是为了保证循环元每次重复的完整性。例如上图中 i=7i = 7 时,最小循环元"ab"的第4次重复就尚未完成。

进一步地,如果 inext[next[i]]i - next \left[ next[i] \right] 能整除 ii ,那么 S[1inext[next[i]]]S[1 \sim i - next[next[i]]] 就是 S[1i]S[1 \sim i] 的次小循环元。依此类推,我们还可以找出 S[1i]S[1 \sim i] 所有可能的循环元(注意判断是否能整除以保证完整性)。

i a b a b a b a b 12345678 next[8] = 6 a b a b a b a b 12345678 next[6] = 4  $\Rightarrow S[1\sim 4] = S[3\sim 6] = S[5\sim 8]$  a b a b a b a b 12345678 next[4] = 2 a b a b a b a b 12345678 next[2] = 0 char a[1000010]; int next[1000010], n, T; void calc_next() { next[1] = 0; for (int i = 2, j = 0; i <= n; i++) { while (j > 0 && a[i] != a[j+1]) j = next[j]; if (a[i] == a[j+1]) j++; next[i] = j; } } int main() { while (cin >> n && n) { scanf("%s", a + 1); calc_next(); printf("Test case %d\n", ++T);
for (int i = 2; i <= n; i++) { if (i % (i - next[i]) == 0 && i / (i - next[i]) > 1) printf("%d%d\n", i, i / (i - next[i])); } puts(""); }

最小表示法

给定一个字符串 S[1n]S[1 \sim n] ,如果我们不断把它的最后一个字符放到开头,最终会得到 nn 个字符串,称这 nn 个字符串是循环同构的。这些字符串中字典序最小的一个,称为字符串 SS 的最小表示。

例如 S="abca"S = \text{"abca"} ,那么它的4个循环同构字符串为"abca","aabc","caab","bcaa”, SS 的最小表示为"aabc"。因为一个与 SS 循环同构的字符串可以用该字符串在 SS 中的起始下标表示,所以我们就用 B[i]B[i] 来表示从 ii 开始的循环同构字符串,即 S[in]+S[1i1]S[i\sim n] + S[1\sim i - 1]

如何求出一个字符串的最小表示?最朴素的方法是:按照定义,依次比较这 nn 个循环同构的字符串,找到其中字典序最小的一个。比较两个循环同构字符串 B[i]B[i]B[j]B[j] 时,我们也采用直接向后扫描的方式,依次取 k=0,1,2,k = 0,1,2,\dots ,比较 B[i+k]B[i + k]B[j+k]B[j + k] 是否相等,直至找到一个不相等的位置,从而确定 B[i]B[i]B[j]B[j] 的大小关系。

实际上,一个字符串的最小表示可以在 O(n)O(n) 的线性时间内求出。我们首先把 SS 复制一份接在它的结尾,得到的字符串记为 SSSS 。显然, B[i]=SS[ii+n1]B[i] = SS[i \sim i + n - 1]

对于任意的 i,ji,j ,我们仔细观察 B[i]B[i]B[j]B[j] 的比较过程:

$S =$  "bacacabc",  $i = 2,j = 4,k = 3$  i+k baca|cabcbacacabc baca|bcbacacabc jj+K

如果在 i+ki + kj+kj + k 处发现不相等,假设 SS[i+k]>SS[j+k]SS[i + k] > SS[j + k] ,那么我们当然可以得知 B[i]B[i] 不是 SS 的最小表示(因为存在一个更小的循环同构串 B[j]B[j] )。除此之外,我们还可以得知 B[i+1],B[i+2],,B[i+k]B[i + 1], B[i + 2], \dots, B[i + k] 也都不是 SS 的最小表示。这是因为对于 1pk1 \leq p \leq k ,存在一个比 B[i+p]B[i + p] 更小的循环同构串 B[j+p]B[j + p] (从 i+pi + pj+pj + p 开始向后扫描,同样会在 p=kp = k 时发现不相等,并且 SS[i+k]>SS[j+k]SS[i + k] > SS[j + k] )。

同理,如果 SS[i+k]<SS[j+k]SS[i + k] < SS[j + k] ,那么 B[j],B[j+1],,B[j+k]B[j], B[j + 1], \dots, B[j + k] 都不是 SS 的最小表示,直接跳过这些位置,一定不会遗漏最小表示。

根据这个性质,我们可以用这样的算法来求最小表示:

最小表示法

  1. 初始化 i=1,j=2i = 1, j = 2

  2. 通过直接向后扫描的方法,比较 B[i]B[i]B[j]B[j] 两个循环同构串。

(1) 如果扫描了 nn 个字符后仍然相等,说明 SS 只由 1 种字符构成,任意 B[i]B[i] 都是它的最小表示。
(2) 如果在 i+ki + kj+kj + k 处发现不相等:

SS[i+k]>SS[j+k]SS[i + k] > SS[j + k] ,令 i=i+k+1i = i + k + 1 。若此时 i=ji = j ,再令 i=i+1i = i + 1

SS[i+k]<SS[j+k]SS[i + k] < SS[j + k] ,令 j=j+k+1j = j + k + 1 。若此时 i=ji = j ,再令 j=j+1j = j + 1

  1. i>ni > nB[j]B[j] 为最小表示;若 j>nj > nB[i]B[i] 为最小表示;否则重复第2步。

该算法通过两个指针不断向后移动的形式,尝试比较每两个循环同构串的大小。利用上面的性质,及时排除掉不可能的选项。当其中一个移动到结尾时,就考虑过了所有可能的二元组 (B[i],B[j])(B[i], B[j]) ,从而得到了最小表示。

如果每次比较向后扫描了 kk 的长度,则 iijj 二者之一会向后移动 kk ,而 iijj 合计一共最多向后移动 2n2n 的长度,因此该算法的复杂度为 O(n)O(n)

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; //s只由一个字符构成,形如"aaaa"  
    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); // B[ans]是最小表示