前缀函数的运用
前缀函数的运用
KMP
在字符串中查找子串,本质是对前缀函数的运用。
例题1
给定一个文本串 ttt,和一个模板串 sss,找出 sss 在 ttt 中出现的所有位置。
题解
构造一个字符串 h=s+#+th=s+\#+th=s+#+t,hhh 由 三个部分 组成,第一个部分是模板串 sss,第二个部分是一个在 sss 和 ttt 中都不会出现 的字符 #\##,第三部分是文本串 ttt。
可以对 hhh 跑一遍前缀函数,然后所有前缀函数大小为 s.size()
的位置就是 sss 完整在文本串中出现且 sss 的 最后一个字符 所处的位置。
由于 hhh 有偏移,所以得到的位置应该减去 2*s.size()-1
。
#include <bits/stdc++.h>
using namespace std;
//#pragma GCC optimize(2)
#define int long long
#define endl '\n'
#define PII pair<int,int>
#define INF 1e18
const int N = 1e6 + 7;struct PrifixFunction {int n;string s;vector <int> p;PrifixFunction (int _n, string _s) : s(_s), n(_n), p(_n + 1){}void getPrifixFunction () {p[0] = 0;for (int i = 1; i < n; i++) {int j = p[i - 1];while (j && s[j] != s[i]) {j = p[j - 1];}if (s[j] == s[i]) j ++;p[i] = j;}}
};void solve () {string t, s;cin >> t >> s;string h = s + "#" + t;PrifixFunction pp(h.size(), h);pp.getPrifixFunction();vector <int> ans;for (int i = 0; i < h.size(); i++) {if (pp.p[i] == s.size()) ans.push_back(i - 2*s.size() + 1);}for (auto i : ans) cout << i << endl;
}
signed main() {solve();
}
字符串的周期
对于字符串 sss,若 sss 存在周期 T(1≤T≤∣s∣)T(1\le T\le |s|)T(1≤T≤∣s∣),则对于所有的 i∈[0,∣s∣−T−1]i\in[0,|s|-T-1]i∈[0,∣s∣−T−1] 都有 s[i]=s[i+T]s[i]=s[i+T]s[i]=s[i+T]。
不难发现,若存在一个子串 rrr,既是 sss 的一个 真前缀,又是 sss 的一个 真后缀,那么 sss 一定有周期 t=∣s∣−∣r∣t= |s|-|r|t=∣s∣−∣r∣。
因为 i+t≤∣s∣−1i+t\le|s|-1i+t≤∣s∣−1,当 iii 取 最大值 的时候,该不等式取等号,而 iii 至多只能到 ∣r∣−1|r|-1∣r∣−1,所以 t=∣s∣−∣r∣t=|s|-|r|t=∣s∣−∣r∣。
又因为这样的 rrr 的长度最大不超过 π(∣s∣−1)\pi(|s|-1)π(∣s∣−1),所以我们就说 sss 的最小周期是 π(∣s∣−1)\pi(|s|-1)π(∣s∣−1)。
统计每个前缀的出现次数
例题1
给定一个长度为 nnn 的字符串 sss,统计 sss 的每个前缀在 sss 中的出现次数。
题解
若存在一个子串 rrr,既是 sss 的一个 真前缀,又是 sss 的一个 真后缀,那么我们就称 rrr 是 sss 的一个 borderborderborder。
设 xxx 是 sss 的真前缀,如果 xxx 在 sss 中出现了一次以上,那么必然能截取出一个子串 ttt,使得 xxx 是 ttt 的 borderborderborder。
所以我们对于每个子串 s[0...i]s[0...i]s[0...i],求出其所有的 borderborderborder 并进行累加即可。
如果一个长度为 lenlenlen 的 borderborderborder 出现了 xxx 次,那么不管子串多长 p[len−1]p[len-1]p[len−1] 都是仅次于 lenlenlen 的 borderborderborder。
所以一个长度为 lenlenlen 的 borderborderborder 的出现次数,也代表了一部分 p[len−1]p[len-1]p[len−1] 的贡献。
我们先记录每个子串最大长度的 borderborderborder 出现次数,然后倒序累加就行了。
随后别忘记加上前缀自身出现的一次。
vector<int> p(n);
vector<int> cnt(n + 1, 0); // cnt[i] 表示长度为 i 的前缀出现次数// 1. 计算前缀函数
for (int i = 1; i < n; i++) {int j = p[i - 1];while (j && s[j] != s[i]) j = p[j - 1];if (s[j] == s[i]) j++;p[i] = j;
}// 2. 统计每个长度的出现次数(不含自己作为前缀的那一次)
for (int i = 0; i < n; i++) cnt[p[i]]++;// 3. 把出现次数沿着 border 链上传递
for (int len = n; len > 0; len--) {cnt[p[len - 1]] += cnt[len];
}// 4. 每个前缀本身出现一次
for (int i = 1; i <= n; i++) cnt[i]++;
例题2
求 sss 中的每个前缀在 ttt 中出现的次数。
题解
构造一个字符串 h=s+#+th=s+\#+th=s+#+t,其中 #\## 是不在 sss 和 ttt 中存在的字符。
接下来我们就用例题 111 的求法,求出 hhh 的每个前缀在自身中出现的次数。
然后我们再对 sss 这个字符串单独计算一遍其每个前缀在自身中出现的次数。
将两个数组相减即可。
当我们需要用的时候,将它们封装到结构体里就行了。
求 sss 中本质不同子串的数量
例题1
求 sss 中本质不同子串的数量。
题解
考虑迭代的做法,设 kkk 是当前的 sss 中本质不同的子串的数量,若在 sss 后面增加一个字符 ccc,记作 s1s_1s1。
那么最多增加 ∣s∣+1|s|+1∣s∣+1 个本质不同的子串,且这些子串都是以 ccc 结尾的。
此时我们可以将当前字符串 s1s_1s1 反转,设为 s1Ts_{1}^{T}s1T,对其跑一遍前缀函数。
然后求出 s1Ts_1^{T}s1T 的每个前缀在自身中出现多少次,那些只出现一次的就是增加的本质不同的子串。
求出 s1Ts_1^{T}s1T 中只出现一次的前缀,就相当于找出最大的 π\piπ 值,最大的 π\piπ 值表示前缀的字符串到哪里开始不重复出现。
所以只出现一次的前缀数量就相当于 ∣s∣+1−πmax|s|+1-\pi_{max}∣s∣+1−πmax。