前缀函数和KMP算法
前缀函数(\(\pi\)函数)
定义
border :若字符串 \(s\) 存在某个真前缀和某个真后缀相同,则这个真前缀或真后缀称为 \(s\) 的一个 border。\
前缀函数 :前缀函数 \(\pi[i]\) 的值为字符串 \(s\) 的前缀 \(s[0,i]\) 的最长 border 的长度。特别地, \(\pi[0]=0\)。
实现
vector<int> prefix_function(string s) {
int n = (int)s.length();
vector<int> pi;
for (int i = 1; i < n; i++) {
int j = pi[i - 1];
while (j && s[i] != s[j]) j = pi[j - 1];
if (s[i] == s[j]) j++;
pi[i] = j;
}
return pi;
}
前缀函数的应用
一 、模式匹配(KMP算法)
给定一个文本串 \(t\) 和一个模式串 \(s\) ,找出 \(s\) 在 \(t\) 中出现的所有位置。
将模式串与文本串拼接,在中间加一个分隔符,变为字符串 \(s+\#+t\) ,然后对这个字符串求出前缀函数,每个前缀函数等于 \(|s|\) 的位置即为模式串出现的位置的末尾。
二 、字符串的周期
对字符串 \(s\) ,若存在 \(0<p< |s|\) ,使得 \(s[i]=s[i+p]\) 对所有 \(i\in[0,|s|−p−1]\) 成立,则称 \(p\) 为字符串 \(s\) 的周期。
若 \(s\) 有长度为 \(r\) 的 border ,则 \(|s| -r\) 为 \(s\) 的周期。
可以借助前缀函数得到 \(s\) 的所有 border ,即 \(\pi[n−1],\pi[\pi[n−1]−1],...\) 以此类推。于是可以得到 \(s\) 的所有周期,其中 \(s\) 的最小周期为 \(n−\pi[n−1]\) 。
三 、统计每个前缀的出现次数
考虑位置 \(i\) 的前缀函数值 \(\pi[i]\) 。根据定义,其意味着字符串 \(s\) 的一个长度为 \(\pi[i]\) 的前缀在位置 \(i\) 出现并以 \(i\) 为右端点,同时不存在一个更长的前缀满足前述定义。与此同时,更短的前缀可能以该位置为右端点。
所以我们可以通过以下方法计算答案 。
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]++;
四 、一个字符串中本质不同子串的数目
考虑向一个字符串 \(s\) 末尾添加一个字符 \(c\) 后出现的新的字串数目。
构造字符串 \(t=s+c\) ,并将其反转成为 \(t’\) ,问题变为求 \(t’\) 有多少前缀未在其他地方出现过。如果计算出 \(t’\) 的前缀函数最大值 \(\pi_{max}\) ,那么长度大于 \(\pi_{max}\) 的前缀都没有出现过,故添加了一个新字符后新出现的子串数目为 \(|s|+1−\pi_{max}\) 。
五 、字符串压缩(字符串的整周期)
给定一个长度为 \(n\) 的字符串 \(s\) ,我们希望找到其最短的“压缩”表示,也即我们希望寻找一个最短的字符串 \(t\) ,使得 \(s\) 可以被 \(t\) 的一份或多份拷贝的拼接表示。
计算 \(s\) 的前缀函数。我们定义值 \(k=n−\pi[n−1]\) 。如果 \(k\) 整除 \(n\) ,那么前缀 \(s[0,k−1]\) 就是答案,否则可以证明不存在一个有效的压缩,故答案为 \(s\) 。
六 、根据前缀函数构建一个自动机
重新考虑计算前缀函数的问题,我们先前使用构造字符串 \(s+\#+t\) 的方法。但如果字符串 \(t\) 是某种巨大的字符串,我们只知道它的构造方式,无法将它显式地构造出来,那么如何计算?
例如计算字符串 \(s\) 在 \(k\) 阶 Gray 字符串中出现的次数,Gray 字符串的定义为:
第 \(k\) 个字符串地长度达到了 \(2^k\) 的数量级,无法通过之前的方法计算。
先计算 \(s+\#\) 的前缀函数,因为 \(\#\) 不属于字符集,所以之后的前缀函数值一定不超过 \(|s|\) 。我们把前缀函数值当作状态,字符当作转移的条件,构造出自动机。
自动机部分代码
void compute_automaton(string s, vector<vector<int>> &aut) {
s += '#';
int n = (int)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]);
}
}
}
我们实际要计算的是有多少个位置的前缀函数值等于 \(|s|\) ,故可以使用 dp 。
设 \(G[i][j]\) 表示,从状态 \(j\) 开始处理完第 \(i\) 个字符串后所去到的自动机的状态。同时设 \(K[i][j]\) 表示从状态 \(j\) 开始处理完第 \(i\) 个字符串后,\(s\) 出现的次数。这两个数组不难计算。
初始条件
转移
最终答案就是 \(K[𝑘][0]\) 。

浙公网安备 33010602011771号