Skip to Content

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.

Recursos
FuenteRecursoNotas
CFA short guide to suffix automata

Explicación de autómatas de sufijos

cp-algoSuffix Automaton

Excelente tutorial de autómata de sufijos

CFadamant - history of recurring problem

¡Más problemas!

Implementación

Recursos
FuenteRecursoNotas
BenqSuffix 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 O(N2)\mathcal{O}(N^2) de memoria, pero la compresión de caminos permite representarlo y calcularlo en memoria lineal.

Recursos
FuenteRecursoNotas
SOUkkonen's suffix tree algorithm in plain English

ejemplo + diagramas

CFSuffix Tree. Ukkonen's algorithm

explicación breve del algoritmo de Ukkonen + código

Implementación

Recursos
FuenteRecursoNotas
BenqSuffix Tree

basado en el de adamant de arriba

cp-algoSuffix 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

HechoFuenteNombreDificultadTagsSolución
PlatinumStanding Out from the HerdDifí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)

HechoFuenteNombreDificultadTagsSolución
CFSuffix ArrayFácilSuffix Structures
KattisString MultimatchingFácil
onlinejudge.orgI Love Strings!!Fácil
SPOJLexicographical String SearchFácilSuffix Structures
CFMike & FriendsFácil
HEPower of String 3NormalSuffix Structures
CFCyclical QuestNormalSuffix Structures
Balkan OI2015 - ClarksonNormalSuffix Structures, DPSolución
CFPaper TaskDifícilSuffix Structures
CFYet Another LCP ProblemDifícilSuffix Structures
CFIndie AlbumDifícilSuffix Structures
CFString JourneyMuy difícilSuffix Structures, DP
CFSecurityMuy difícilSuffix Tree
CFIncomparable PairsMuy difícilSuffix Structures

Extender el árbol palindrómico

Problemas

HechoFuenteNombreDificultadTagsSolución
CFPalindromic PartitionMuy difícilPalindromic Tree
CFPalindromic MagicInsanoPalindromic Tree