Definition
The order of suffixes is defined lexicographically. A suffix balanced tree is a balanced tree that maintains this suffix order; that is, the suffix balanced tree of a string is the ordered set of all suffixes of . Each node in the suffix balanced tree corresponds to one suffix of the original string.
In particular, the in-order traversal of a suffix balanced tree is the suffix array.
Construction
To build the suffix balanced tree for a string of length , insert its suffixes into the suffix balanced tree in reverse order.
Let the set maintained by the suffix balanced tree be , and let the suffix currently being added be . Adding the next suffix means adding to (equivalently, if the string maintained by the suffix balanced tree is , prepend a character to in the next step). This operation is simply an insertion into the balanced tree.
Use a balanced tree with expected height , such as a scapegoat tree or a Treap.
Method 1
During insertion, compare two suffixes by brute force to determine their order, and use that to decide which subtree to insert into. Each insertion performs at most comparisons, and each comparison takes at most time, for a total of per construction path.
There are insertions, so this method has an upper-bound time complexity of .
Method 2
Observe that and differ only in , and already belongs to , so we can use this to optimize insertion.
Suppose we need to compare and , where . At each comparison, first compare the first characters of the two strings. If they differ, their order is determined; if they are equal, we only need to compare the order of the two strings after removing their first characters. After removing the first character, both strings already belong to , so we can use the balanced tree's operation for finding a rank in time to complete the comparison. Thus, one insertion takes at most time.
There are insertions, so this method has an upper-bound time complexity of .
Method 3
According to Method 2, if we can determine the order of two nodes in the balanced tree in time, then the suffix balanced tree can be built in time.
Let denote the value of node . If, while building the balanced tree, we maintain an additional tag for each node such that , then the order of two nodes in the balanced tree can be determined in time from tags.
Associate each node in the balanced tree with a real interval, and let the root correspond to . For node , let its interval be , set , and let its left and right subtrees correspond to and respectively. It is easy to prove that has the required property.
Since the balanced tree has expected height , the precision is sufficiently reliable. In an actual implementation, we can also use a larger interval, such as making the root correspond to .
Method 4
One can first construct the suffix array and then construct the suffix balanced tree from the suffix array. The complexity bottleneck is then either the construction of the suffix array or the complexity of inserting elements into the chosen balanced tree at once.
Deletion
Suppose the suffix currently being added is and the suffix added previously was . The suffix balanced tree also supports deleting the suffix (equivalently, the string maintained by the suffix balanced tree is , and we remove the initial ).
As with insertion, the operation of deleting can be performed using the balanced tree's node-deletion operation.
Advantages of the suffix balanced tree
- The idea behind the suffix balanced tree is relatively clear. Compared with suffix structures such as the suffix automaton, it is easier to understand; if you can write a balanced tree, you can write it.
- Its complexity does not depend on the size of the character set.
- It supports deleting one character from the beginning of a string.
- If the balanced tree supports persistence, the suffix balanced tree can also be persistent.
Examples
P3809 - Template Suffix Sorting
This is a template problem for suffix arrays. After building the suffix balanced tree, obtain the suffix array by an in-order traversal.
Reference code for the SGT version
#include <algorithm>
#include <iostream>
#include <string>
using namespace std;
constexpr int N = 1e6 + 5;
constexpr double INF = 1e18;
int n, m, sa[N];
string t;
// SuffixBST(SGT Ver)
// Add in order; reverse the query string when querying
// The suffix beginning at i corresponds to node i
constexpr 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 inorder(int rt) {
if (!rt) return;
inorder(L[rt]);
sa[++m] = rt;
inorder(R[rt]);
}
void solve(int Case) {
cin >> t;
n = t.size();
t = " " + t;
init();
for (int i = n; i >= 1; --i) {
insert(root, i, 0, INF);
}
// The in-order traversal of the suffix balanced tree is the suffix array
m = 0;
inorder(root);
for (int i = 1; i <= n; ++i) cout << sa[i] << ' ';
cout << '\n';
}
int main() {
cin.tie(nullptr)->sync_with_stdio(false);
solve(1);
return 0;
}
P6164 - Template Suffix Balanced Tree
Problem statement
Given an initial string and operations:
- Insert several characters at the end of the current string.
- Delete several characters from the end of the current string.
- Ask how many times the string occurs as a contiguous substring in the current string.
The problem is forced online. The changing and initial string lengths are at most , , and the total length of all queries is at most .
For operations 1 and 2, since a suffix balanced tree conveniently supports inserting and deleting at the front, we can turn insertion and deletion at the end into insertion and deletion at the front. If we maintain the suffix balanced tree of the reverse of rather than that of , this conversion is possible. Insertion and deletion in a balanced tree both take time, so adding or deleting one character takes time. Let the total number of added and deleted characters be ; this part then takes time.
For operation 3, the number of occurrences of equals the number of suffixes having as a prefix, and the number of suffixes prefixed by , which equals the rank of its successor minus the rank of its predecessor. Appending a very large character to constructs a successor of . Decreasing the last character of by 1 constructs a predecessor of .
To query the rank of a string in the suffix balanced tree, we cannot guarantee that occurs in the tree, so each comparison of string order must be done by brute force. One comparison takes time, and each query performs at most comparisons, so one query takes time. Let the sum of the lengths of all query strings be ; this part then takes time in total.
Reference code for the SGT version
#include <algorithm>
#include <iostream>
#include <string>
using namespace std;
constexpr int N = 8e5 + 5;
constexpr double INF = 1e18;
void decode(string& 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;
string a;
char t[N];
// SuffixBST(SGT Ver)
// Add in order; reverse the query string when querying
// The prefix ending at i corresponds to node i
// Note: lazy deletion cannot be used, or it may damage the tree structure
constexpr 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]);
rebuild(rt, lv, rv);
} else {
// Find the predecessor of rt to replace rt
int nrt = L[rt];
while (R[nrt]) {
nrt = R[nrt];
}
remove(L[rt], nrt, lv, tag[rt]);
L[nrt] = L[rt];
R[nrt] = R[rt];
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(const string& 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;
}
return false;
}
int query(int rt, const string& 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() {
cin.tie(nullptr)->sync_with_stdio(false);
n = 0;
cin >> q;
init();
cin >> a;
na = a.size();
a = " " + a;
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) {
cin >> op;
// Handle the three cases separately
if (op[0] == 'A') { // ADD
cin >> a;
na = a.size();
decode(a, na, mask);
a = " " + a;
for (int i = 1; i <= na; ++i) {
t[++n] = a[i];
insert(root, n, 0, INF);
}
} else if (op[0] == 'D') { // DEL
int x;
cin >> x;
while (x) {
remove(root, n, 0, INF);
--n;
--x;
}
} else if (op[0] == 'Q') { // QUERY
cin >> a;
na = a.size();
decode(a, na, mask);
a = " " + a;
reverse(a.begin() + 1, a.begin() + 1 + na);
a.push_back('Z' + 1);
int ans = query(root, a, na + 1);
--a[na];
ans -= query(root, a, na + 1);
cout << ans << '\n';
mask ^= ans;
}
}
}
int main() {
solve();
return 0;
}
References
- Chen Lijie — Applications of Weight-Balanced Trees and Suffix Balanced Trees in the Informatics Olympiad