Estructuras de sufijos de strings
Muchos problemas se pueden resolver con arreglos de sufijos, autómatas de sufijos o árboles de sufijos. La solución puede ser solo un poco más fácil/difícil con las distintas estructuras de datos.
Autómata de sufijos
El autómata de sufijos (Suffix Automaton) es un grafo de palabras acíclico dirigido (DAWG), tal que cada camino en el grafo recorre una subcadena distinta del string original.
| Fuente | Recurso | Notas |
|---|---|---|
| CF | A short guide to suffix automata | Explicación de autómatas de sufijos |
| cp-algo | Suffix Automaton | Excelente tutorial de autómata de sufijos |
| CF | adamant - history of recurring problem | ¡Más problemas! |
Implementación
| Fuente | Recurso | Notas |
|---|---|---|
| Benq | Suffix Automaton |
Árbol de sufijos
El árbol de sufijos (Suffix Tree) es un trie que contiene todos los sufijos de un string. De forma naive, esto ocuparía de memoria, pero la compresión de caminos permite representarlo y calcularlo en memoria lineal.
| Fuente | Recurso | Notas |
|---|---|---|
| SO | Ukkonen's suffix tree algorithm in plain English | ejemplo + diagramas |
| CF | Suffix Tree. Ukkonen's algorithm | explicación breve del algoritmo de Ukkonen + código |
Implementación
| Fuente | Recurso | Notas |
|---|---|---|
| Benq | Suffix Tree | basado en el de adamant de arriba |
| cp-algo | Suffix Tree. Ukkonen's Algorithm | implementación del algoritmo de Ukkonen |
Generar el arreglo de sufijos a partir del árbol de sufijos
Un arreglo de sufijos se puede generar a partir del árbol de sufijos tomando el recorrido DFS del árbol de sufijos.
int N, sa[MN], ctr; // longitud del string, arreglo de sufijos, contador
struct Edge {
public:
int n, l, r; // nodo, la arista cubre s[l..r]
explicit operator bool() const { return n != -1; }
} c[MN * 2][26]; // aristas de un árbol de sufijos
void dfs(int n = 0, int d = 0) {
bool c = 0; // Tiene hijo. Si es false, este nodo es una hoja
for (int i = 0; i < 26; ++i)
if (c[n][i]) {
c = 1;
dfs(c[n][i].n, d + c[n][i].r - c[n][i].l);
}
if (!c) sa[ctr++] = N - d;
}Generar el árbol de sufijos a partir del arreglo de sufijos
Por supuesto, la operación de arriba también se puede invertir. Cada elemento del arreglo de sufijos corresponde a una hoja del árbol de sufijos. El arreglo LCP guarda información sobre el ancestro común más bajo de dos elementos adyacentes del arreglo de sufijos. Usando estas dos piezas de información, podemos construir el árbol de sufijos a partir del arreglo de sufijos en tiempo lineal.
int N;
char s[MN];
int sa[MN]; // arreglo de sufijos
int lcp[MN]; // lcp[i] guarda el prefijo común más largo entre s[sa[i-1]..]
// y s[sa[i]..]
Edge c[MN * 2][MK]; // aristas del árbol de sufijos
int d[MN * 2]; // longitud del string correspondiente a un nodo del árbol
int q[MN * 2], Q, ctr,
rm[MN]; // q se usa como pila. ctr cuenta nodos del árbol
std::stack<int> ins[MN];
void build_tree() {
q[0] = N;
Q = 0;
for (int i = N - 1; i >= 1; --i) {
while (Q && lcp[q[Q]] > lcp[i]) --Q;
if (lcp[q[Q]] != lcp[i])
++rm[q[Q]]; // Cota derecha del rango donde lcp es el prefijo
// común más largo
q[++Q] = i;
}
q[0] = 0, Q = 0;
for (int i = 1; i < N; ++i) {
while (Q && lcp[q[Q]] > lcp[i]) --Q;
if (lcp[q[Q]] != lcp[i])
ins[q[Q]].push(i); // Cota izquierda del rango donde lcp se
// vuelve por primera vez un prefijo común más largo
q[++Q] = i;
}
// Las cotas izquierda y derecha calculadas arriba se pueden interpretar
// como el preorden y postorden DFS
q[0] = 0, Q = 0; // Este arreglo q ahora guarda la pila de ancestros de
// cada nodo nuevo creado
auto nn = [&](int l, int dd) {
++ctr;
d[ctr] = dd;
int p = q[Q];
// p es el padre de este nodo, por definición de la pila q
int r = l + dd;
// s[l..r] es el string correspondiente al nodo que estamos insertando
l += d[p];
// d[p] es la longitud del padre, así que s[l..l+d[p]] ya estaría
// cubierto por los ancestros del nodo
c[p][s[l]] = {ctr, l, r};
return ctr;
};
for (int i = 0; i < N; ++i) {
Q -= rm[i];
for (int x; !ins[i].empty(); ins[i].pop()) {
x = ins[i].top();
x = nn(sa[x], lcp[x]);
// sa[x+1] sería equivalente, por definición de lcp
q[++Q] = x;
}
nn(sa[i], N - sa[i]);
}
}Generar el árbol de sufijos a partir del autómata de sufijos
Algo interesante de los árboles de sufijos y los autómatas de sufijos es que el árbol de enlaces de un autómata de sufijos es equivalente al árbol de sufijos del string invertido. Como los autómatas de sufijos son mucho más fáciles de crear que los árboles de sufijos, podemos usar esto como método alternativo para construir un árbol de sufijos, ¡también en tiempo lineal!
char s[MN]; // string
int ord[MN]; // nodos que representan prefijos del string s
int u[MN * 2]; // si el nodo ya se creó
int l[MN * 2]; // enlace en el autómata de sufijos
Edge c[MN * 2][27]; // arista del árbol de sufijos (no del autómata; la
// estructura del autómata no es necesaria para construir
// el stree)
void build_tree() {
s[N] = 26; // terminador
for (int i = N; i >= 0; --i) ord[i] = append(ord[i + 1], s[i]);
for (int i = 0, x, r, l; i <= N; ++i) {
x = ord[i], r = N + 1;
for (; x && !u[x]; x = l[x]) {
l = r - d[x] + d[l[x]];
c[l[x]][s[l]] = {x, l, r};
r = l;
u[x] = 1;
}
}
}Ejemplo - Standing Out
| Hecho | Fuente | Nombre | Dificultad | Tags | Solución |
|---|---|---|---|---|---|
| Platinum | Standing Out from the Herd | Difícil | — |
Solución con autómata de sufijos - Standing Out
#include <cstdio>
#include <cstring>
#include <vector>
FILE *IN, *OUT;
typedef long long ll;
const int MN = 1e5 + 10, MM = MN * 2;
char s[MN];
std::vector<int> down[MM];
int N, v[MM], c[MM][26], l[MM], d[MM], topo[MM], T, X;
ll f[MN], cnt[MM];
bool u[MM];
/*
Variables clave:
s: strings de entrada
down: árbol de enlaces del autómata
v: información sobre a qué vaca pertenece cada nodo
c: arreglo de hijos del autómata
l: enlace (del autómata)
d: profundidad (del autómata)
topo: orden topológico (del autómata)
T, X: contadores para el orden topológico y el autómata
f: respuesta
cnt: número de formas de alcanzar un nodo desde la raíz
u: arreglo de visitados para el orden topológico
*/
// agregar la vaca b al valor a
// valor = -1: ninguna vaca asignada
// valor = -2: varias vacas asignadas
// valor = 0..N: id de vaca
void merge(int &a, int b) {
if (!~a) a = b;
else if (~b && a != b) a = -2;
}
// código plantilla de autómata
int append(int p, char x) {
if (~c[p][x]) {
int q = c[p][x];
if (d[q] == d[p] + 1) return q;
else {
++X;
for (int i = 0; i < 26; ++i) c[X][i] = c[q][i];
l[X] = l[q], d[X] = d[p] + 1;
l[q] = X;
for (; ~p && c[p][x] == q; p = l[p]) c[p][x] = l[q];
return l[q];
}
}
int n = ++X;
d[n] = d[p] + 1;
for (; ~p && !~c[p][x]; p = l[p]) c[p][x] = n;
if (!~p) l[n] = 0;
else {
int q = c[p][x];
if (d[q] == d[p] + 1) l[n] = q;
else {
++X;
for (int i = 0; i < 26; ++i) c[X][i] = c[q][i];
l[X] = l[q], d[X] = d[p] + 1;
l[n] = l[q] = X;
for (; ~p && c[p][x] == q; p = l[p]) c[p][x] = l[q];
}
}
return n;
}
// DFS a lo largo de los enlaces
void dfs2(int n = 0) {
for (int x : down[n]) {
dfs2(x);
merge(v[n], v[x]);
}
}
// DFS a lo largo del autómata de sufijos. Esto construye el orden topológico
void dfs(int n = 0) {
u[n] = 1;
for (int i = 0; i < 26; ++i) {
int y = c[n][i];
if (~y && !u[y]) dfs(y);
}
topo[T++] = n;
}
int main(void) {
IN = fopen("standingout.in", "r"), OUT = fopen("standingout.out", "w");
memset(v, -1, sizeof v);
memset(c, -1, sizeof c);
fscanf(IN, "%d", &N);
d[0] = 0, l[0] = -1;
for (int i = 0; i < N; ++i) {
fscanf(IN, " %s", s);
int n = 0;
for (int j = 0; s[j]; ++j) {
n = append(n, s[j] - 'a'); // construir el autómata
merge(v[n], i);
}
}
// construir el árbol de enlaces
for (int i = 1; i <= X; ++i) down[l[i]].push_back(i);
dfs(); // dfs del árbol de enlaces
dfs2(); // dfs del autómata
cnt[0] = 1;
for (int i = T - 1, x; i >= 0; --i) {
x = topo[i];
for (int j = 0; j < 26; ++j)
if (~c[x][j])
cnt[c[x][j]] += cnt[x]; // contar caminos de la raíz a un nodo
if (v[x] >= 0)
f[v[x]] += cnt[x]; // si este nodo se asocia a una vaca única,
// sumar a la respuesta
}
for (int i = 0; i < N; ++i) fprintf(OUT, "%lld\n", f[i]);
return 0;
}Problemas de estructuras de sufijos (arreglo, autómata, árbol)
| Hecho | Fuente | Nombre | Dificultad | Tags | Solución |
|---|---|---|---|---|---|
| CF | ★ Suffix Array | Fácil | Suffix Structures | — | |
| Kattis | String Multimatching | Fácil | — | ||
| onlinejudge.org | I Love Strings!! | Fácil | — | ||
| SPOJ | ★ Lexicographical String Search | Fácil | Suffix Structures | — | |
| CF | Mike & Friends | Fácil | — | ||
| HE | Power of String 3 | Normal | Suffix Structures | — | |
| CF | Cyclical Quest | Normal | Suffix Structures | — | |
| Balkan OI | 2015 - Clarkson | Normal | Suffix Structures, DP | Solución | |
| CF | Paper Task | Difícil | Suffix Structures | — | |
| CF | Yet Another LCP Problem | Difícil | Suffix Structures | — | |
| CF | Indie Album | Difícil | Suffix Structures | — | |
| CF | String Journey | Muy difícil | Suffix Structures, DP | — | |
| CF | Security | Muy difícil | Suffix Tree | — | |
| CF | Incomparable Pairs | Muy difícil | Suffix Structures | — |
Extender el árbol palindrómico
| Fuente | Recurso | Notas |
|---|---|---|
| CF | A bit more about palindromes | |
| CF | Palindromic tree: Behind the Scenes |
Problemas
| Hecho | Fuente | Nombre | Dificultad | Tags | Solución |
|---|---|---|---|---|---|
| CF | Palindromic Partition | Muy difícil | Palindromic Tree | — | |
| CF | Palindromic Magic | Insano | Palindromic Tree | — |