Border
废话
波论好难,学不懂。
一点一点推罢。
疯狂写博客捏,但是可能有很多笔误(青蛙在我旁边随便一眼都能看到),如果发现了请在评论区中提出!
当然也不排除哪里有些推导过程什么的错了。
Border 的定义
一个字符串的最长真公共前后缀就叫 Border(这个「真」就表示其不和原字符串相同)。
刨开 Border,剩下的一部分被称作 Period。
Border 是可以有重叠部分的:
为什么剩下的部分要叫 Period?因为它是这个字符串的循环节,Border 没有重叠部分的情况是很显然的。
在有重叠的情况下,利用相等关系的传递性,下面三个青色部分是相同的。
所以 Period 是这个串的循环节。 如果还有重叠部分可以一直递归下去。
Border 的性质和定理
因为 Border 和 Period 关联比较大,所以也会有很多关于 Period 的性质和定理。
Border 的 Border 是 Border
令 \(\operatorname{Border}(S)\) 表示字符串 \(S\) 的所有 Border 组成的集合,令 \(\operatorname{maxBorder}(S)\) 表示字符串 \(S\) 长度最大的 Border。
那么有:
就是一个字符串所有的 Border 是由它最大的 Border 和这个 Border 的所有 Border 组成的。
换句话说就是:Border 的 Border 是 Border。
(有重叠的情况懒得画了。)
Border 的前半部分的两段 Border 相同,而 Border 本身的两段也相同,从而前半段的 Border 和后半段的 Border 相同,所以是原字符串的 Border。
由此可见,Border 的 Border 可以利用相等关系的传递性,来证明是原字符串的另一个 Border。
而且并不存在一个 Border,使得它不是 \(\operatorname{maxBorder}(S)\) 且不属于 \(\operatorname{Border}(S)\)。
若存在,则它必定比 \(\operatorname{maxBorder}(S)\) 短,而它又是原串的 Border,所以它分别和 \(\operatorname{maxBorder}(S)\) 的左右两段相同,所以它又是 \(\operatorname{maxBorder}(S)\) 的 Border,矛盾。
弱周期定理(Weak Periodicity Lemma)
若字符串 \(S\) 存在两个长度分别为 \(p\) 和 \(q\) 的 Period,且 \(p + q \leqslant |S|\),那么 \(S\) 还存在长度为 \(\gcd(p, q)\) 的 Period。
证明:更相减损术。
若一个字符串有长度为 \(len\) 的 Period,那么对于所有的 \(i(1 \leqslant i \leqslant |S| - len)\),都有 \(S_{i} = S_{i + len}\),或是对于所有的 \(i(len < i \leqslant |S|)\),有 \(S_{i - len} = S_{i}\)(这两个是等价的)。反推也成立。
不妨设 \(p > q\),令 \(d = p - q\),那么有:
- \(i \leqslant q\) 时,\(S_{i} = S_{i + p} = S_{i + p - q} = S_{i + d}\)。
- \(q < i \leqslant p\) 时,\(S_{i} = S_{i - q} = S_{i - q + p} = S_{i - d}\)。
- \(p < i\) 时,\(S_{i} = S_{i - np} = S_{i - mq}\),可以转化为上面两种情况。
所以 \(S\) 存在长度为 \(d\) 的循环节,这个过程可以一直递归直至 \(p = q\)。
同时还有一个周期定理(Periodicity Lemma),它的条件变为了 \(p + q - \gcd(p, q) \leqslant |S|\),其余不变。但是证明起来困难很多并且用处不大,就暂时没学。
前缀的 Period 是 Period
(当然是有条件的。)
若字符串 \(S\) 存在长度为 \(p\) 的周期和一个前缀 \(T\),且 \(T\) 存在长度为 \(q\) 的周期。若 \(|T| \geqslant p\) 且 \(q | p\),那么 \(S\) 也存在长度为 \(q\) 的周期。
青色部分(\(T\) 的 Period)正好可以填充满一个蓝色部分(\(S\) 的 Period),而蓝色部分又是 \(S\) 的 Period,所以每一个蓝色部分都是由若干个完整的青色部分组成的(除了最后一部分可能不完整),所以青色部分也是 \(S\) 的 Period。
Border 中的等差数列
- 若字符串 \(S, T\) 满足 \(|T| \geqslant \dfrac{|S|}{2}\),则 \(T\) 在 \(S\) 中的匹配位置可以排成一个等差数列。
首先不考虑 \(S\) 中没有被任何匹配子串覆盖到位置,相当于把开始和结束的一段丢掉(因为 \(|T| \geqslant \dfrac{|S|}{2}\) 所以不可能存在中间一段没有被覆盖而两旁被覆盖的情况)。
考虑第一个匹配位置,第二个匹配位置以及最后一个匹配位置。
(若没有三个及以上的匹配位置那么结论显然成立,就不讨论了。)
令第一个匹配位置和第二个匹配位置之间的距离为 \(p\),第二个匹配位置和最后一个匹配位置之间的距离为 \(q\)。
考虑第一个匹配子串和第二个匹配子串的「并集」, \(T\) 是它的 Border,所以它存在长度为 \(p\) 的 Period。
而 \(T\) 也是这个「并集」的前缀,所以 \(T\) 也拥有长度为 \(p\) 的 Period。
(上面这一点感觉很显然就没打算单独讲。)
同理可得 \(T\) 还存在长度为 \(q\) 的 Period。
而因为 \(p + q + |T| = |S| \Leftrightarrow p + q = |S| - |T| \leqslant 2|T| - |T| = |T|\)。
由弱周期定理(WPL)可知,\(T\) 还存在长度为 \(\gcd(p, q)\) 的 Period。
令 \(g = \gcd(p, q)\),\(T\) 的最短 Period 长度为 \(l\)。
那么肯定有 \(l | p\),否则可以利用弱周期定理继续构造更短的 \(l\)。
若 \(l < p\),那么显然可以构造出比第二个匹配位置更靠前的匹配,或者说是更小的 \(p\)。
所以 \(p = l\),进而有 \(p = l = g\),所以 \(p | q\)。
又因为 \(S\) 被 \(T\) 完全覆盖了,所以对于任意的 \(i(i \leqslant |S| - p)\),都有 \(S_{i} = S_{i + p}\),所以 \(S\) 也有长度为 \(p\) 的 Period 且这是最短的 Period。
所以 \(S\) 中所有的 \(T\) 的匹配位置可以排成一个等差数列,其公差为 \(p\)。
- 字符串 \(S\) 中所有长度大于等于 \(\dfrac{|S|}{2}\) 的 Border 的长度可以排成一个等差数列。
考虑 \(\operatorname{maxBorder}(S)\) 和任意一个非 \(\operatorname{maxBorder}(S)\) 的长度大于等于 \(\dfrac{|S|}{2}\) 的 Border。
(若不存在两个及以上的 Border 那么显然成立,不讨论。)
令它们对应的 Period 长度分别为 \(p\) 和 \(q\)。
因为两个 Border 的长度都大于 \(\dfrac{|S|}{2}\),所以 \(p, q \leqslant |S| - \dfrac{|S|}{2} = \dfrac{|S|}{2}\),所以 \(p + q \leqslant |S|\)。
利用弱周期定理(WPL)可知,\(\gcd(p, q)\) 也是 \(S\) 的 Period,若 \(\gcd(p, q) < p < q\),那么 \(S\) 应有更长的 Border,与我们开始所取的 \(\operatorname{maxBorder}(S)\) 矛盾,所以 \(\gcd(p, q) = p\),所以 \(p | q\)。
而这个结论是对所有长度大于等于 \(\dfrac{|S|}{2}\) 的 Border 成立的,所以这些 Border 对应的 Period 的长度显然都是 \(p\) 的倍数,进而得到这些 Border 的长度可以排成公差为 \(p\) 的等差数列的结论。
实际上,这个结论可以之际推到长度大于等于 \(|S| - p\) 的 Border,因为这些 Border 都满足弱周期定理的使用条件。
拓展结论:一个字符串 \(S\) 的所有 Border 的长度可以划分成 \(\mathcal{O}(\log |S|)\) 个等差数列。
这个拓展结论其实挺显然的,不证了。
如何求每个前缀的 maxBorder
详见 「BJWC2018 Border 的四种求法」一题。
朴素的求法是 \(\mathcal{O}(n^{2})\) 的。
注意到小标题「前缀」这一限定,令 \(nxt_{i}\) 表示 \(\operatorname{maxBorder}(S[1 .. i])\) 的长度(也对应着靠前的 Border 的结束位置)。若我们已知 \(nxt_{1 \sim i - 1}\),那么可以快速求出 \(nxt_{i}\)。
KMP 包含了这一部分,这里不细讲,贴一个远古博客的链接。
而这个 \(nxt_{i}\) 常常会被称为 \(fail\) 指针,因为在字符串匹配失败时就是利用它快速跳转的。
若把每一对 \((fail_{i}, i)\) 看作一条树上的边,那么所有的边就组成了一棵失配树,总共有 \(n + 1\) 个点和 \(n\) 条边,有时候将题目放到失配树上来做会直观许多。
一个点到根的路径就包含了它所有的 Border。
每一个前缀在 \(S\) 中的出现次数就是其子树大小。
应该还有很多性质,但是还不了解。
习题
做一题写一题,咕咕咕。
Luogu P3193 [HNOI2008] GT考试
考虑对匹配的过程 dp。
观察到 \(M\) 最大只有 \(20\),所以可以直接以当前匹配长度来作为状态。
令 \(dp_{i, j}\) 表示以第 \(i\) 个字符结束的后缀的最长匹配长度。
令 \(cnt_{i, j}\) 表示在匹配长度为 \(i\) 的后缀后加一个字符使得它最长匹配后缀的长度变为 \(j\) 的方案数。
那么有转移式:
对于每一个 \(i\),转移的方式都没变,所以可以用矩阵来加速。
如何求 \(cnt_{i, j}\)?直接枚举 \(i\) 和下一个字符,按照 KMP 的方式来进行匹配,假设最后的匹配长度为 \(k\),那么令 \(cnt_{i, k} \leftarrow cnt_{i, k} + 1\)。
Code:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int n, m, mod, now, ans, nxt[25];
string s;
struct matrix {
int val[20][20];
matrix(int v = 0) {
for(int i = 0; i < m; ++i) {
for(int j = 0; j < m; ++j) {
val[i][j] = (i == j ? v : 0);
}
}
}
void operator *= (const matrix& _) {
static matrix res;
for(int i = 0; i < m; ++i) {
for(int j = 0; j < m; ++j) {
res.val[i][j] = 0;
for(int k = 0; k < m; ++k) {
res.val[i][j] += val[i][k] * _.val[k][j] % mod;
}
res.val[i][j] %= mod;
}
}
memcpy(val, res.val, sizeof(val));
}
} a, res;
matrix ksm(matrix& v, int y) {
matrix ret(1), x = v;
while(y) {
if(y & 1) ret *= x;
x *= x;
y >>= 1;
}
return ret;
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> m >> mod >> s;
s = "V" + s;
for(int i = 2, j = 0; i <= m; ++i) {
while(j && s[j + 1] != s[i]) j = nxt[j];
if(s[j + 1] == s[i]) ++j;
nxt[i] = j;
}
for(int i = 0; i < m; ++i) {
for(char j = '0'; j <= '9'; ++j) {
now = i;
while(now && s[now + 1] != j) now = nxt[now];
if(s[now + 1] == j) ++now;
if(now < m) ++a.val[i][now];
}
}
res = ksm(a, n);
for(int i = 0; i < m; ++i) ans += res.val[0][i];
cout << ans % mod;
return 0;
}
Luogu P3435 [POI2006] OKR-Periods of Words
题目就是让你求每个前缀的最大真循环节长度之和(如不存在则为 \(0\))。
循环节长度显然可以转化为 Border 长度。
但是题目还有一个条件:\(a\) 是 \(Q + Q\) 的前缀。
其实这个条件没什么用,它等价于 Border 长度小于等于前缀长度的一半。
如果有 Border 长度大于前缀长度的一半,那么两端 Border 的重合部分显然也是一个 Border:
所以这一点就不需要考虑了,现在我们要求:每个前缀的最短 Border 长度。
因为「Border 的 Border 是 Border」,所以可以用 KMP 求出 \(nxt\) 再不停地跳,直到跳到最短的 Border 为止。
这个过程会比较慢,可以用记忆化。
另外一个方法是构建失配树,发现「一个前缀的最短 Border」就是它的最浅非根祖先。
所以可以在失配树上搜索,记录子树的最浅非根祖先,便搜索边统计答案即可。
Code:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int n, nxt[1000005], siz[1000005];
ll ans;
vector<int> g[1000005];
string s;
void dfs(int now, int rt) {
if(!rt) rt = now;
if(now) ans += now - rt;
for(const auto& i : g[now]) {
dfs(i, rt);
}
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> s;
s = "V" + s;
g[0].push_back(1);
for(int i = 2, j = 0; i <= n; ++i) {
while(j && s[j + 1] != s[i]) j = nxt[j];
if(s[j + 1] == s[i]) ++j;
nxt[i] = j;
g[nxt[i]].push_back(i);
}
dfs(0, 0);
cout << ans;
return 0;
}
Luogu P2375 [NOI2014] 动物园
感受失配树的强大!
显然 \(num_{i}\) 就是 \(i\) 到根的路径上(根除外)编号小于等于 \(\dfrac{i}{2}\) 的点的数量(Border 不重叠那么肯定长度小于字符串的一半)。
那么可以用树状数组来维护结点数量的前缀和,具体地,进入结点 \(i\) 时将其在树状数组中的值 \(+1\),退出时 \(-1\),统计答案则直接 \(ask(i / 2)\) 即可。
Code:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
constexpr int mod = 1000000007;
int t, n, nxt[1000005], num[1000005], c[1000005];
ll ans;
vector<int> g[1000005];
string s;
int lowbit(int x) {return x & -x;}
void update(int x, int v) {
while(x <= 1000000) {
c[x] += v;
x += lowbit(x);
}
}
int ask(int x) {
int ret = 0;
while(x) {
ret += c[x];
x -= lowbit(x);
}
return ret;
}
void dfs(int now) {
num[now] = ask(now >> 1);
if(now) update(now, 1);
for(const auto& i : g[now]) {
dfs(i);
}
if(now) update(now, -1);
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> t;
while(t--) {
cin >> s;
n = s.length();
s = "V" + s;
for(int i = 0; i <= n; ++i) g[i].clear();
for(int i = 2, j = 0; i <= n; ++i) {
while(j && s[j + 1] != s[i]) j = nxt[j];
if(s[j + 1] == s[i]) ++j;
nxt[i] = j;
}
for(int i = 1; i <= n; ++i) g[nxt[i]].push_back(i);
dfs(0);
ans = 1;
for(int i = 1; i <= n; ++i) ans = ans * (num[i] + 1ll) % mod;
cout << ans << '\n';
}
return 0;
}
Luogu P3426 [POI2005] SZA-Template
如果这个印章能覆盖整个字符串,那么肯定在字符串的开头结尾都要印一次,所以它肯定是原串的一个 Border!
现在我们随便截取一个 Border,怎么判断它是否合法呢?
首先在原串中找到所有的这个 Border 的匹配串,在上面印个章,如果最后原串每个位置都被盖到了,那么它就是合法的。
注意到每个匹配串都代表着一个前缀,这个 Border 也是这个前缀的 Border。
把这一关系放到失配树上,就是看子树内的所有相邻位置的距离最大值。
用 set
可以轻松做到 \(\mathcal{O}(n \log n)\) 的复杂度。
但是如果只删不加,就可以改用链表,从而做到 \(\mathcal{O}(n)\)。
每次搜索儿子先删掉不在点 \(n\) 到根上的链的子树,再访问链上的儿子即可。
Code:
#include <bits/stdc++.h>
using namespace std;
int n, mx, l[500005], r[500005], nxt[500005];
bool flag[500005];
string s;
vector<int> g[500005];
int dfs(int now) {
if(flag[now] && mx <= now) return now;
for(const auto& i : g[now]) {
if(!flag[i]) {
dfs(i);
}
}
mx = max(mx, r[now] - l[now]);
r[l[now]] = r[now], l[r[now]] = l[now];
for(const auto& i : g[now]) {
if(flag[i]) {
auto res = dfs(i);
if(res) return res;
}
}
return 0;
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> s;
n = s.length();
s = "V" + s;
for(int i = 2, j = 0; i <= n; ++i) {
while(j && s[j + 1] != s[i]) j = nxt[j];
if(s[j + 1] == s[i]) ++j;
nxt[i] = j;
}
for(int i = 1; i <= n; ++i) {
g[nxt[i]].push_back(i);
l[i] = i - 1, r[i] = i + 1;
}
for(int now = n; now; now = nxt[now]) {
flag[now] = true;
}
cout << dfs(0);
return 0;
}
本文来自博客园,作者:A_box_of_yogurt,转载请注明原文链接:https://www.cnblogs.com/A-box-of-yogurt/p/18032741