Border树

对于一个字符串S,n = |S|,它的Border 树(也叫next 树)共有n+1
个节点:0, 1, 2, . . . , n。
0 是这棵有向树的根。对于其他每个点1 ≤ i ≤ n,父节点为next[i]。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
struct KMP {
vector<int> nxt;
string tt;
int len;
KMP() {}
KMP(string t) {
len = t.size();
t = " " + t;
tt = t;
nxt.resize(len + 1);
nxt[1] = nxt[0] = 0;
init(tt);
}

void init(string t) {
for (int i = 2; i <= len; i++) {
nxt[i] = nxt[i - 1];
while (nxt[i] && t[i] != t[nxt[i] + 1]) nxt[i] = nxt[nxt[i]];
nxt[i] += (t[i] == t[nxt[i] + 1]);
}
}
void buildtree(auto& e) {
for (int i = 1; i <= len; i++) {
e[nxt[i]].emplace_back(i);
}
}
};

性质:next[i] 表示字符串 S 的前缀 Prefix[i] 的非平凡的最大 Border。

  1. 每个前缀Prefix[i] 的所有Border:节点i 到根的链。
  2. 哪些前缀有长度为x 的Border:x 的子树。前缀x出现了子树大小siz[x]次
  3. 求两个前缀的最长公共Border 等价于求LCA。

1.给定一个字符串 ss,定义它的 kk 前缀 prek\mathit{pre}_k 为字符串 s1ks_{1\dots k}kk 后缀 sufk\mathit{suf}_k 为字符串 ssk+1ss_{|s|-k+1\dots |s|},其中 1ks1 \le k \le |s|

定义 Border(s)\bold{Border}(s)对于 i[1,s)i \in [1, |s|),满足 prei=sufi\mathit{pre}_i = \mathit{suf}_i 的字符串 prei\mathit{pre}_i 的集合。Border(s)\bold{Border}(s) 中的每个元素都称之为字符串 ssborder\operatorname{border}

mm 组询问,每组询问给定 p,qp,q,求 ssp\boldsymbol{p} 前缀q\boldsymbol{q} 前缀最长公共 border\operatorname{border} 的长度。

Sol:建立border树,等价于求两者的lca。由于求的是非平凡,如果存在一个前缀是另一个前缀的border,则需要跳到父亲

debug:树剖的板子要加双向边并且注意坐标偏移从1-n+1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void solve() {
string s;cin >> s; KMP kmp(s);
int n = s.size();HLD hld(n + 1);// 字符串长度加上一个空节点
for (int i = 1; i <= kmp.len; i++) { // 一定要加双向边
hld.addEdge(kmp.nxt[i] + 1, i + 1);
hld.addEdge(i + 1, kmp.nxt[i] + 1);
}
hld.work(1);
int m;cin >> m;
for (int i = 1; i <= m; i++) {
int x, y;
cin >> x >> y;
x++;//进行下标偏移。考虑到dfs序问题(0-n)->(1-n+1)
y++;
int res = hld.lca(x, y);
if (x == res || y == res)//如果一个前缀是另一个前缀border,找父亲
cout << hld.parent[res] - 1 << endl;
else
cout << res - 1 << endl;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//倍增版本
void solve() {
string s;
cin >> s;
KMP kmp(s);
int n = s.size();
Blca lc(n + 1); // 字符串长度加上一个空节点
for (int i = 1; i <= kmp.len; i++) { // 一定要加双向边
lc.add(kmp.nxt[i] + 1, i + 1);
}
lc.dfs(1, 0);
int m;
cin >> m;
for (int i = 1; i <= m; i++) {
int x, y;
cin >> x >> y;
x++; // 进行下标偏移。考虑到dfs序问题(0-n)->(1-n+1)
y++;
int res = lc.lca(x, y);
if (x == res || y == res) // 如果一个前缀是另一个前缀border,找父亲
cout << lc.st[res][0] - 1 << endl;
else
cout << res - 1 << endl;
}
}

2.字符串S 长度不超过106,求一个最长的子串T,满足:

  • T 为S 的前缀。

  • T 为S 的后缀。

  • T 在S 中至少出现K次。

Sol:考虑T是border,出现k次等价于子树比k大,我们只需要把树建出来,然后dfs一遍求子树大小就可以了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void solve() {
int n, k;
cin >> n >> k;
string s;
cin >> s;
KMP kmp(s);
vector<vector<int>> e(n + 1);
kmp.buildtree(e);
vector<int> sz(n + 1);
auto dfs = [&](auto self, int u)->void {
sz[u] = 1;
for (auto v : e[u]) {
self(self, v);
sz[u] += sz[v];
}
};
dfs(dfs,0);
int ans = 0;
for (int i = n; i; i = kmp.nxt[i]) {
if (sz[i] >= k)
ans = max(ans, i);
}
if(ans==0)cout<<-1;else cout<<s.substr(0,ans);
}

3.字符串S 长度不超过200, 000,有Q ≤ 200, 000 次操作:

  1. 修改操作:向字符串末尾添加一个字符ch。

  2. 查询操作:求一个最长的子串T,满足:
    T 为S 的前缀。
    T 为S 的后缀。
    T 在S 中至少出现K次。

Sol:考虑离线询问,先把完整的fail树建立。然后每次询问的子树大小,我们动态维护标记,从当前结尾开始倍增每次用树状数组查询带标记子树大小。倍增需要特判一开始就满足条件的情况。

debug:两层for变量用重,导致RE。倍增需要特判一开始就满足条件的情况。由于树状数组下标不能为0,而fail树中存在0号节点,但dfs序我们又规定了从1开始标号,每次都是在dfs序的意义下修改树状数组,所以不需要偏移了。牢记Fwk初始化都传入节点数就可以了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
void solve() {
int n, m;
cin >> n >> m;
string s;
cin >> s;
int cnt = n;
vector<node> q(m + 1);
for (int i = 1; i <= m; i++) {
cin >> q[i].op;
if (q[i].op == 1) {
cin >> q[i].c;
s += q[i].c;
cnt += 1;
q[i].x = cnt;
} else {
cin >> q[i].x;
}
}
deb(s);
KMP kmp(s);
Blca lc(cnt + 1);
kmp.buildtree(lc.e);
lc.dfs(0, 0);
Fwk<int> fw(cnt + 1); // 树状数组下标偏移
for (int i = 1; i <= n; i++) fw.add(l[i], 1);
// 主要是建树
int p = n;
for (int i = 1; i <= m; i++) {
if (q[i].op == 1) {
p = q[i].x;
fw.add(l[p], 1);
} else {
int cur = p;
if (fw.rangeSum(l[cur] - 1, r[cur]) >= q[i].x) {
cout << cur << endl;
continue;
}
for (int j = lc.len; j >= 0; j--) {
int pos = lc.st[cur][j];
deb(pos, fw.rangeSum(l[pos] - 1, r[pos]));
if (fw.rangeSum(l[pos] - 1, r[pos]) < q[i].x)
cur = pos;
}
deb(cur);
if (lc.st[cur][0])
cout << lc.st[cur][0] << endl;
else
cout << -1 << endl;
}
deb(i, p);
}
}

可持久化KMP,nxt图(类比triw图

https://codeforces.com/contest/1721/problem/E

https://www.cnblogs.com/Blue233333/p/8241503.html

https://blog.csdn.net/ghu99999/article/details/118707728

https://www.luogu.com.cn/problem/CF808G