后缀平衡树
定义¶
后缀之间的大小由字典序定义,后缀平衡树就是一个维护这些后缀顺序的平衡树,即字符串
特别地,后缀平衡树的中序遍历即为后缀数组。
构造过程¶
对长度为
记后缀平衡树维护的集合为
这里使用期望树高为
做法 1¶
插入时,暴力比较两个后缀之间的大小关系,从而判断之后是往哪一个子树添加。这样子,单次插入至多比较
一共会插入
做法 2¶
注意到
假设当前要比较
一共会插入
做法 3¶
根据做法 2,如果能够
记
不妨令平衡树中每个节点对应一个实数区间,令根节点对应
由于使用了期望树高为
做法 4¶
其实可以先构建出后缀数组,然后再根据后缀数组构建后缀平衡树。这样做的复杂度瓶颈在于后缀数组的构建复杂度或者所用平衡树一次性插入
删除操作¶
假设当前添加的后缀为
类似于插入操作,借助平衡树的删除节点操作可以完成删除
后缀平衡树的优点¶
- 后缀平衡树的思路比较清晰,相比后缀自动机等后缀结构更好理解,会写平衡树就能写。
- 后缀平衡树的复杂度不依赖于字符集的大小
- 后缀平衡树支持在字符串开头删除一个字符
- 如果使用支持可持久化的平衡树,那么后缀平衡树也能可持久化
例题¶
P3809【模板】后缀排序¶
后缀数组的模板题,建出后缀平衡树之后,通过中序遍历得到后缀数组。
SGT版本的参考代码
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 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 | #include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 5;
const double INF = 1e18;
int n, m, sa[N];
char t[N];
// SuffixBST(SGT Ver)
// 顺序加入,查询时将询问串翻转
// 以i开始的后缀,对应节点的编号为i
const double alpha = 0.75;
int root;
int sz[N], L[N], R[N];
double tag[N];
int buffer_size, buffer[N];
bool cmp(int x, int y) {
if (t[x] != t[y]) return t[x] < t[y];
return tag[x + 1] < tag[y + 1];
}
void init() { root = 0; }
void new_node(int& rt, int p, double lv, double rv) {
rt = p;
sz[rt] = 1;
tag[rt] = (lv + rv) / 2;
L[rt] = R[rt] = 0;
}
void push_up(int x) {
if (!x) return;
sz[x] = sz[L[x]] + 1 + sz[R[x]];
}
bool balance(int rt) { return alpha * sz[rt] > max(sz[L[rt]], sz[R[rt]]); }
void flatten(int rt) {
if (!rt) return;
flatten(L[rt]);
buffer[++buffer_size] = rt;
flatten(R[rt]);
}
void build(int& rt, int l, int r, double lv, double rv) {
if (l > r) {
rt = 0;
return;
}
int mid = (l + r) >> 1;
double mv = (lv + rv) / 2;
rt = buffer[mid];
tag[rt] = mv;
build(L[rt], l, mid - 1, lv, mv);
build(R[rt], mid + 1, r, mv, rv);
push_up(rt);
}
void rebuild(int& rt, double lv, double rv) {
buffer_size = 0;
flatten(rt);
build(rt, 1, buffer_size, lv, rv);
}
void insert(int& rt, int p, double lv, double rv) {
if (!rt) {
new_node(rt, p, lv, rv);
return;
}
if (cmp(p, rt))
insert(L[rt], p, lv, tag[rt]);
else
insert(R[rt], p, tag[rt], rv);
push_up(rt);
if (!balance(rt)) rebuild(rt, lv, rv);
}
void remove(int& rt, int p, double lv, double rv) {
if (!rt) return;
if (rt == p) {
if (!L[rt] || !R[rt]) {
rt = (L[rt] | R[rt]);
} else {
// 找到rt的前驱来替换rt
int nrt = L[rt], fa = rt;
while (R[nrt]) {
fa = nrt;
sz[fa]--;
nrt = R[nrt];
}
if (fa == rt) {
R[nrt] = R[rt];
} else {
L[nrt] = L[rt];
R[nrt] = R[rt];
R[fa] = 0;
}
rt = nrt;
tag[rt] = (lv + rv) / 2;
}
} else {
double mv = (lv + rv) / 2;
if (cmp(p, rt))
remove(L[rt], p, lv, mv);
else
remove(R[rt], p, mv, rv);
}
push_up(rt);
if (!balance(rt)) rebuild(rt, lv, rv);
}
void inorder(int rt) {
if (!rt) return;
inorder(L[rt]);
sa[++m] = rt;
inorder(R[rt]);
}
void solve(int Case) {
scanf("%s", t + 1);
n = strlen(t + 1);
init();
for (int i = n; i >= 1; --i) {
insert(root, i, 0, INF);
}
// 后缀平衡树的中序遍历即为后缀数组
m = 0;
inorder(root);
for (int i = 1; i <= n; ++i) printf("%d ", sa[i]);
printf("\n");
}
int main() {
solve(1);
return 0;
}
|
P6164【模板】后缀平衡树¶
题意
给定初始字符串
- 在当前字符串的后面插入若干个字符。
- 在当前字符串的后面删除若干个字符。
- 询问字符串
t
题目 强制在线,字符串变化长度以及初始长度
对于操作 1 和操作 2,由于后缀平衡树维护头插和头删操作比较方便,所以想到把尾插和尾删操作搞成头插和头删。这里如果维护
对于操作 3,
现在要查询某一个串
SGT版本的参考代码
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 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 | #include <bits/stdc++.h>
using namespace std;
const int N = 8e5 + 5;
const double INF = 1e18;
void decode(char* s, int len, int mask) {
for (int i = 0; i < len; ++i) {
mask = (mask * 131 + i) % len;
swap(s[i], s[mask]);
}
}
int q, n, na;
char a[N], t[N];
// SuffixBST(SGT Ver)
// 顺序加入,查询时将询问串翻转
// 以i结束的前缀,对应节点的编号为i
// 注意:不能写懒惰删除,否则可能会破坏树的结构
const double alpha = 0.75;
int root;
int sz[N], L[N], R[N];
double tag[N];
int buffer_size, buffer[N];
bool cmp(int x, int y) {
if (t[x] != t[y]) return t[x] < t[y];
return tag[x - 1] < tag[y - 1];
}
void init() { root = 0; }
void new_node(int& rt, int p, double lv, double rv) {
rt = p;
sz[rt] = 1;
tag[rt] = (lv + rv) / 2;
L[rt] = R[rt] = 0;
}
void push_up(int x) {
if (!x) return;
sz[x] = sz[L[x]] + 1 + sz[R[x]];
}
bool balance(int rt) { return alpha * sz[rt] > max(sz[L[rt]], sz[R[rt]]); }
void flatten(int rt) {
if (!rt) return;
flatten(L[rt]);
buffer[++buffer_size] = rt;
flatten(R[rt]);
}
void build(int& rt, int l, int r, double lv, double rv) {
if (l > r) {
rt = 0;
return;
}
int mid = (l + r) >> 1;
double mv = (lv + rv) / 2;
rt = buffer[mid];
tag[rt] = mv;
build(L[rt], l, mid - 1, lv, mv);
build(R[rt], mid + 1, r, mv, rv);
push_up(rt);
}
void rebuild(int& rt, double lv, double rv) {
buffer_size = 0;
flatten(rt);
build(rt, 1, buffer_size, lv, rv);
}
void insert(int& rt, int p, double lv, double rv) {
if (!rt) {
new_node(rt, p, lv, rv);
return;
}
if (cmp(p, rt))
insert(L[rt], p, lv, tag[rt]);
else
insert(R[rt], p, tag[rt], rv);
push_up(rt);
if (!balance(rt)) rebuild(rt, lv, rv);
}
void remove(int& rt, int p, double lv, double rv) {
if (!rt) return;
if (rt == p) {
if (!L[rt] || !R[rt]) {
rt = (L[rt] | R[rt]);
} else {
// 找到rt的前驱来替换rt
int nrt = L[rt], fa = rt;
while (R[nrt]) {
fa = nrt;
sz[fa]--;
nrt = R[nrt];
}
if (fa == rt) {
R[nrt] = R[rt];
} else {
L[nrt] = L[rt];
R[nrt] = R[rt];
R[fa] = 0;
}
rt = nrt;
tag[rt] = (lv + rv) / 2;
}
} else {
double mv = (lv + rv) / 2;
if (cmp(p, rt))
remove(L[rt], p, lv, mv);
else
remove(R[rt], p, mv, rv);
}
push_up(rt);
if (!balance(rt)) rebuild(rt, lv, rv);
}
bool cmp1(char* s, int len, int p) {
for (int i = 1; i <= len; ++i, --p) {
if (s[i] < t[p]) return true;
if (s[i] > t[p]) return false;
}
}
int query(int rt, char* s, int len) {
if (!rt) return 0;
if (cmp1(s, len, rt))
return query(L[rt], s, len);
else
return sz[L[rt]] + 1 + query(R[rt], s, len);
}
void solve() {
n = 0;
scanf("%d", &q);
init();
scanf("%s", a + 1);
na = strlen(a + 1);
for (int i = 1; i <= na; ++i) {
t[++n] = a[i];
insert(root, n, 0, INF);
}
int mask = 0;
char op[10];
for (int i = 1; i <= q; ++i) {
scanf("%s", op);
//三种情况分别处理
if (op[0] == 'A') { // ADD
scanf("%s", a + 1);
na = strlen(a + 1);
decode(a + 1, na, mask);
for (int i = 1; i <= na; ++i) {
t[++n] = a[i];
insert(root, n, 0, INF);
}
} else if (op[0] == 'D') { // DEL
int x;
scanf("%d", &x);
while (x) {
remove(root, n, 0, INF);
--n;
--x;
}
} else if (op[0] == 'Q') { // QUERY
scanf("%s", a + 1);
na = strlen(a + 1);
decode(a + 1, na, mask);
reverse(a + 1, a + 1 + na);
a[na + 1] = 'Z' + 1;
a[na + 2] = 0;
int ans = query(root, a, na + 1);
--a[na];
ans -= query(root, a, na + 1);
printf("%d\n", ans);
mask ^= ans;
}
}
}
int main() {
solve();
return 0;
}
|
参考资料¶
-
陈立杰 -《重量平衡树和后缀平衡树在信息学奥赛中的应用》 ↩
build本页面最近更新:,更新历史
edit发现错误?想一起完善? 在 GitHub 上编辑此页!
people本页面贡献者:OI-wiki
copyright本页面的全部内容在 CC BY-SA 4.0 和 SATA 协议之条款下提供,附加条款亦可能应用