// 注:// string substr (size_t pos = 0, size_t len = npos) const;vector<int> prefix_function(string s) { int n = (int)s.length(); vector<int> pi(n); for (int i = 1; i < n; i++) for (int j = i; j >= 0; j--) if (s.substr(0, j) == s.substr(i - j + 1, j)) { pi[i] = j; break; } return pi;}
Python
def prefix_function(s): n = len(s) pi = [0] * n for i in range(1, n): for j in range(i, -1, -1): if s[0:j] == s[i - j + 1 : i + 1]: pi[i] = j break return pi
Java
static int[] prefix_function(String s) { int n = s.length(); int[] pi = new int[n]; for (int i = 1; i < n; i++) { for (int j = i; j >= 0; j--) { if (s.substring(0, j).equals(s.substring(i - j + 1, i + 1))) { pi[i] = j; break; } } } return pi;}
vector<int> prefix_function(string s) { int n = (int)s.length(); vector<int> pi(n); for (int i = 1; i < n; i++) for (int j = pi[i - 1] + 1; j >= 0; j--) // improved: j=i => j=pi[i-1]+1 if (s.substr(0, j) == s.substr(i - j + 1, j)) { pi[i] = j; break; } return pi;}
Python
def prefix_function(s): n = len(s) pi = [0] * n for i in range(1, n): for j in range(pi[i - 1] + 1, -1, -1): if s[0:j] == s[i - j + 1 : i + 1]: pi[i] = j break return pi
Java
static int[] prefix_function(String s) { int n = s.length(); int[] pi = new int[n]; for (int i = 1; i < n; i++) { for (int j = pi[i - 1] + 1; j >= 0; j--) { if (s.substring(0, j).equals(s.substring(i - j + 1, i + 1))) { pi[i] = j; break; } } } return pi;}
vector<int> prefix_function(string s) { int n = (int)s.length(); vector<int> pi(n); for (int i = 1; i < n; i++) { int j = pi[i - 1]; while (j > 0 && s[i] != s[j]) j = pi[j - 1]; if (s[i] == s[j]) j++; pi[i] = j; } return pi;}
Python
def prefix_function(s): n = len(s) pi = [0] * n for i in range(1, n): j = pi[i - 1] while j > 0 and s[i] != s[j]: j = pi[j - 1] if s[i] == s[j]: j += 1 pi[i] = j return pi
Java
static int[] prefix_function(String s) { int n = s.length(); int[] pi = new int[n]; for (int i = 1; i < n; i++) { int j = pi[i - 1]; while (j > 0 && s.charAt(i) != s.charAt(j)) { j = pi[j - 1]; } if (s.charAt(i) == s.charAt(j)) { j++; } pi[i] = j; } return pi;}
给定一个文本 t 和一个字符串 s,我们尝试找到并展示 s 在 t 中的所有出现(occurrence)。
为了简便起见,我们用 n 表示字符串 s 的长度,用 m 表示文本 t 的长度。
我们构造一个字符串 s+#+t,其中 # 为一个既不出现在 s 中也不出现在 t 中的分隔符。接下来计算该字符串的前缀函数。现在考虑该前缀函数除去最开始 n+1 个值(即属于字符串 s 和分隔符的函数值)后其余函数值的意义。根据定义,π[i] 为右端点在 i 且同时为一个前缀的最长真子串的长度,具体到我们的这种情况下,其值为与 s 的前缀相同且右端点位于 i 的最长子串的长度。由于分隔符的存在,该长度不可能超过 n。而如果等式 π[i]=n 成立,则意味着 s 完整出现在该位置(即其右端点位于位置 i)。注意该位置的下标是对字符串 s+#+t 而言的。
因此如果在某一位置 i 有 π[i]=n 成立,则字符串 s 在字符串 t 的 i−(n−1)−(n+1)=i−2n 处出现。下图所示为索引的示意图。
正如在前缀函数的计算中已经提到的那样,如果我们知道前缀函数的值永远不超过一特定值,那么我们不需要存储整个字符串以及整个前缀函数,而只需要二者开头的一部分。在我们这种情况下这意味着只需要存储字符串 s+# 以及相应的前缀函数值即可。我们可以一次读入字符串 t 的一个字符并计算当前位置的前缀函数值。
vector<int> find_occurrences(string text, string pattern) { string cur = pattern + '#' + text; int sz1 = text.size(), sz2 = pattern.size(); vector<int> v; vector<int> lps = prefix_function(cur); for (int i = sz2 + 1; i <= sz1 + sz2; i++) { if (lps[i] == sz2) v.push_back(i - 2 * sz2); } return v;}
Python
def find_occurrences(t, s): cur = s + "#" + t sz1, sz2 = len(t), len(s) ret = [] lps = prefix_function(cur) for i in range(sz2 + 1, sz1 + sz2 + 1): if lps[i] == sz2: ret.append(i - 2 * sz2) return ret
Java
static List<Integer> find_occurrences(String text, String pattern) { String cur = pattern + '#' + text; int sz1 = text.length(), sz2 = pattern.length(); List<Integer> v = new ArrayList<>(); int[] lps = prefix_function(cur); for (int i = sz2 + 1; i <= sz1 + sz2; i++) { if (lps[i] == sz2) { v.add(i - 2 * sz2); } } return v;}
字符串的周期
对字符串 s 和 0<p≤∣s∣,若 s[i]=s[i+p] 对所有 i∈[0,∣s∣−p−1] 成立,则称 p 是 s 的周期。
对字符串 s 和 0≤r<∣s∣,若 s 长度为 r 的前缀和长度为 r 的后缀相等,就称 s 长度为 r 的前缀是 s 的 border。
由 s 有长度为 r 的 border 可以推导出 ∣s∣−r 是 s 的周期。
根据前缀函数的定义,可以得到 s 所有的 border 长度,即 π[n−1],π[π[n−1]−1],…。2
所以根据前缀函数可以在 O(n) 的时间内计算出 s 所有的周期。其中,由于 π[n−1] 是 s 最长 border 的长度,所以 n−π[n−1] 是 s 的最小周期。
统计每个前缀的出现次数
在该节我们将同时讨论两个问题。给定一个长度为 n 的字符串 s,在问题的第一个变种中我们希望统计每个前缀 s[0…i] 在同一个字符串的出现次数,在问题的第二个变种中我们希望统计每个前缀 s[0…i] 在另一个给定字符串 t 中的出现次数。
首先让我们来解决第一个问题。考虑位置 i 的前缀函数值 π[i]。根据定义,其意味着字符串 s 一个长度为 π[i] 的前缀在位置 i 出现并以 i 为右端点,同时不存在一个更长的前缀满足前述定义。与此同时,更短的前缀可能以该位置为右端点。容易看出,我们遇到了在计算前缀函数时已经回答过的问题:给定一个长度为 j 的前缀,同时其也是一个右端点位于 i 的后缀,下一个更小的前缀长度 k<j 是多少?该长度的前缀需同时也是一个右端点为 i 的后缀。因此以位置 i 为右端点,有长度为 π[i] 的前缀,有长度为 π[π[i]−1] 的前缀,有长度为 π[π[π[i]−1]−1] 的前缀,等等,直到长度变为 0。故而我们可以通过下述方式计算答案。
实现
[list2tab]
C++
vector<int> ans(n + 1);for (int i = 0; i < n; i++) ans[pi[i]]++;for (int i = n - 1; i > 0; i--) ans[pi[i - 1]] += ans[i];for (int i = 0; i <= n; i++) ans[i]++;
Python
ans = [0] * (n + 1)for i in range(0, n): ans[pi[i]] += 1for i in range(n - 1, 0, -1): ans[pi[i - 1]] += ans[i]for i in range(0, n + 1): ans[i] += 1
解释
在上述代码中我们首先统计每个前缀函数值在数组 π 中出现了多少次,然后再计算最后答案:如果我们知道长度为 i 的前缀出现了恰好 ans[i] 次,那么该值必须被叠加至其最长的既是后缀也是前缀的子串的出现次数中。在最后,为了统计原始的前缀,我们对每个结果加 1。
现在考虑第二个问题。我们应用来自 Knuth–Morris–Pratt 的技巧:构造一个字符串 s+#+t 并计算其前缀函数。与第一个问题唯一的不同之处在于,我们只关心与字符串 t 相关的前缀函数值,即 i≥n+1 的 π[i]。有了这些值之后,我们可以同样应用在第一个问题中的算法来解决该问题。
一个字符串中本质不同子串的数目
给定一个长度为 n 的字符串 s,我们希望计算其本质不同子串的数目。
我们将迭代的解决该问题。换句话说,在知道了当前的本质不同子串的数目的情况下,我们要找出一种在 s 末尾添加一个字符后重新计算该数目的方法。
令 k 为当前 s 的本质不同子串数量。我们添加一个新的字符 c 至 s。显然,会有一些新的子串以字符 c 结尾。我们希望对这些以该字符结尾且我们之前未曾遇到的子串计数。
给定一个长度为 n 的字符串 s,我们希望找到其最短的「压缩」表示,也即我们希望寻找一个最短的字符串 t,使得 s 可以被 t 的一份或多份拷贝的拼接表示。
显然,我们只需要找到 t 的长度即可。知道了该长度,该问题的答案即为长度为该值的 s 的前缀。
让我们计算 s 的前缀函数。通过使用该函数的最后一个值 π[n−1],我们定义值 k=n−π[n−1]。我们将证明,如果 k 整除 n,那么 k 就是答案,否则不存在一个有效的压缩,故答案为 n。
假定 n 可被 k 整除。那么字符串可被划分为长度为 k 的若干块。根据前缀函数的定义,该字符串长度为 n−k 的前缀等于其后缀。但是这意味着最后一个块同倒数第二个块相等,并且倒数第二个块同倒数第三个块相等,等等。作为其结果,所有块都是相等的,因此我们可以将字符串 s 压缩至长度 k。
证明
诚然,我们仍需证明该值为最优解。实际上,如果有一个比 k 更小的压缩表示,那么前缀函数的最后一个值 π[n−1] 必定比 n−k 要大。因此 k 就是答案。
现在假设 n 不可以被 k 整除,我们将通过反证法证明这意味着答案为 n3。假设其最小压缩表示 r 的长度为 p(p 整除 n),字符串 s 被划分为 n/p≥2 块。那么前缀函数的最后一个值 π[n−1] 必定大于 n−p(如果等于则 n 可被 k 整除),也即其所表示的后缀将部分的覆盖第一个块。现在考虑字符串的第二个块。该块有两种解释:第一种为 r0r1…rp−1,另一种为 rp−krp−k+1…rp−1r0r1…rp−k−1。由于两种解释对应同一个字符串,因此可得到 p 个方程组成的方程组,该方程组可简写为 r(i+k)modp=rimodp,其中 ⋅modp 表示模 p 意义下的最小非负剩余。
因此,即使没有字符串 t,我们同样可以应用构造转移表的算法构造一个转移表 ( old π,c)→ new −π:
实现
void compute_automaton(string s, vector<vector<int>>& aut) { s += '#'; int n = s.size(); vector<int> pi = prefix_function(s); aut.assign(n, vector<int>(26)); for (int i = 0; i < n; i++) { for (int c = 0; c < 26; c++) { int j = i; while (j > 0 && 'a' + c != s[j]) j = pi[j - 1]; if ('a' + c == s[j]) j++; aut[i][c] = j; } }}
void compute_automaton(string s, vector<vector<int>>& aut) { s += '#'; int n = s.size(); vector<int> pi = prefix_function(s); aut.assign(n, vector<int>(26)); for (int i = 0; i < n; i++) { for (int c = 0; c < 26; c++) { if (i > 0 && 'a' + c != s[i]) aut[i][c] = aut[pi[i - 1]][c]; else aut[i][c] = i + ('a' + c == s[i]); } }}
最终我们可在 O(∣Σ∣n) 的时间复杂度内构造该自动机。
该自动机在什么时候有用呢?首先,记得大部分时候我们为了一个目的使用字符串 s+#+t 的前缀函数:寻找字符串 s 在字符串 t 中的所有出现。
因此使用该自动机的最直接的好处是 加速计算字符串 s+#+t 的前缀函数。
通过构建 s+# 的自动机,我们不再需要存储字符串 s 以及其对应的前缀函数值。所有转移已经在表中计算过了。
但除此以外,还有第二个不那么直接的应用。我们可以在字符串 t 是 某些通过一些规则构造的巨型字符串 时,使用该自动机加速计算。Gray 字符串,或者一个由一些短的输入串的递归组合所构造的字符串都是这种例子。
出于完整性考虑,我们来解决这样一个问题:给定一个数 k≤105,以及一个长度 ≤105 的字符串 s,我们需要计算 s 在第 k 个 Gray 字符串中的出现次数。回想起 Gray 字符串以下述方式定义:
g1g2g3g4=a=aba=abacaba=abacabadabacaba
由于其天文数字般的长度,在这种情况下即使构造字符串 t 都是不可能的:第 k 个 Gray 字符串有 2k−1 个字符。然而我们可以在仅仅知道开头若干前缀函数值的情况下,有效计算该字符串末尾的前缀函数值。
Knuth, Donald E., James H. Morris, Jr, and Vaughan R. Pratt. “Fast pattern matching in strings.” SIAM journal on computing 6.2 (1977): 323-350.doi: 10.1137/0206024↩