Skip to Content

Template

Sea SS el string.

Pista 1

Una plantilla de SS es a la vez un prefijo y un sufijo de SS. Esto sugiere que deberíamos usar KMP.

Pista 2

Si TT es una plantilla de SS y UU es una plantilla de TT, entonces UU también es una plantilla de SS.

Demostración

Como UU es una plantilla de TT, podemos construir TT usando UU. Como TT es una plantilla de SS, podemos construir SS usando TT.

Por lo tanto, también podemos construir SS usando UU (es decir, es una plantilla de SS).

Pista 3

Primero, usamos KMP para computar la función prefijo π\pi de SS. La plantilla más corta de SS es o bien SS mismo o la plantilla más corta de S[0:π[S1]]S[0 : \pi[|S| - 1]].

Demostración

En su lugar demostraremos que cualquier plantilla de SS que no es SS mismo también es una plantilla de S[0:π[S1]]S[0 : \pi[|S| - 1]], ya que es equivalente.

Como S[0:π[S1]]S[0 : \pi[|S| - 1]] es el prefijo propio más largo de SS que también es un sufijo, sabemos que el único string más largo que puede ser plantilla de SS es SS mismo. Sea TT alguna plantilla de SS que no es SS mismo.

A continuación, afirmamos que podemos construir al menos la mitad del prefijo de S[0:π[S1]]S[0 : \pi[|S| - 1]] usando TT. Si Tπ[S1]2|T| \geq \frac{\pi[|S| - 1]}{2}, entonces hemos terminado. En caso contrario, en el proceso de construir SS usando TT de izquierda a derecha, debe haber algún punto en el que pasamos de (construido menos de la mitad de S[0:π[S1]]S[0 : \pi[|S| - 1]]) a (construido al menos la mitad pero no más que S[0:π[S1]]S[0 : \pi[|S| - 1]]).

De forma similar, podemos construir al menos la mitad del sufijo de S[0:π[S1]]S[0 : \pi[|S| - 1]] usando TT. Luego podemos combinar estos dos fragmentos para construir S[0:π[S1]]S[0 : \pi[|S| - 1]] mismo, y por tanto TT también es una plantilla de S[0:π[S1]]S[0 : \pi[|S| - 1]].

Pista 4

¡No hay que tener miedo de usar KMP otra vez!

Solución 1

Complejidad temporal: O(NlogN)\mathcal O(N \log N).

Primero computamos la función prefijo π\pi de SS usando KMP.

Si 2π[i1]i2\pi[i - 1] \geq i, entonces S[0:i]S[0 : i] nunca será la plantilla más corta de SS porque sabemos que S[0:π[i1]]S[0 : \pi[i - 1]] es una plantilla de S[0:i]S[0 : i].

¡Esto significa que solo tenemos O(logN)\mathcal O(\log N) plantillas candidatas que comprobar! Luego podemos usar KMP de nuevo sobre cada candidata y verificar si el “hueco” máximo entre coincidencias es suficientemente pequeño.

#include <bits/stdc++.h> using namespace std; string s; int p[500000]; int shortest_template(int n) { if (!p[n - 1]) return n; if (p[n - 1] * 2 >= n) return shortest_template(p[n - 1]); int cand = shortest_template(p[n - 1]); for (int i = 0, j = 0, curr = -1; i < n; i++) { while (j && (j == cand || s[i] != s[j])) j = p[j - 1]; if (s[i] == s[j]) j++; if (j == cand) curr = i; if (i - curr >= cand) return n; } return cand; } int main() { cin.tie(0)->sync_with_stdio(0); cin >> s; int n = s.size(); for (int i = 1; i < n; i++) { p[i] = p[i - 1]; while (p[i] && s[i] != s[p[i]]) p[i] = p[p[i] - 1]; if (s[i] == s[p[i]]) p[i]++; } cout << shortest_template(n); return 0; }
Solución 2

Complejidad temporal: O(N)\mathcal O(N).

Primero computamos la función prefijo π\pi de SS usando KMP. Mientras hacemos esto, también construimos un árbol donde el padre del nodo (i+1)(i + 1) es π[i]\pi[i] para cada 0i<N0 \leq i < N.

Observemos que el subárbol del nodo ii es el conjunto de todas las posiciones jj donde S[0:i]=S[ji:j]S[0 : i] = S[j - i : j].

Esto significa que podemos recorrer el árbol hacia arriba desde el nodo NN y llevar la cuenta de los nodos en el subárbol del nodo actual (usando una lista enlazada).

Para comprobar si el nodo actual funciona, simplemente verificamos si los huecos entre posiciones en el subárbol son suficientemente pequeños. Podemos llevar la cuenta de estos huecos manteniendo un máximo global que actualizamos cada vez que borramos un nodo de la lista enlazada.

#include <bits/stdc++.h> using namespace std; const int MX = 5e5 + 5; string s; int p[MX], mx_gap = 1, ans, prv[MX], nxt[MX]; int graph[MX]; bool visited[MX]; int num_child[MX]; int n; void dfs(int node) { assert(node <= n); visited[node] = true; if (node && !visited[p[node - 1]]) { dfs(p[node - 1]); if (mx_gap <= node) ans = min(ans, node); } mx_gap = max(mx_gap, nxt[node] - prv[node]); nxt[prv[node]] = nxt[node]; prv[nxt[node]] = prv[node]; for (int ind = num_child[node]; ind < num_child[node + 1]; ++ind) { int i = graph[ind]; if (!visited[i]) dfs(i); } } int main() { cin.tie(0)->sync_with_stdio(0); cin >> s; n = s.size(); ++num_child[0]; for (int i = 1; i < n; i++) { p[i] = p[i - 1]; while (p[i] && s[i] != s[p[i]]) p[i] = p[p[i] - 1]; if (s[i] == s[p[i]]) p[i]++; ++num_child[p[i]]; } for (int i = 1; i <= n + 1; ++i) num_child[i] += num_child[i - 1]; graph[--num_child[0]] = 1; for (int i = 1; i < n; i++) graph[--num_child[p[i]]] = i + 1; prv[0] = 0, nxt[0] = 1, prv[n] = n - 1, nxt[n] = n; for (int i = 1; i < n; i++) prv[i] = i - 1, nxt[i] = i + 1; ans = n; dfs(n); cout << ans; return 0; }
Solución 3

Complejidad temporal: O(N)\mathcal O(N).

Primero computamos la función prefijo π\pi de SS usando KMP.

Luego resolvemos el siguiente problema de forma recursiva: dado un string, ¿cuál es la longitud de su plantilla más corta? Podemos hacerlo así:

  1. Hallar la respuesta para el prefijo propio más largo del string que también es un sufijo.
  2. Comprobar si esta respuesta también funciona para el string actual.
  3. Si lo hace, devolver esa longitud; si no, devolver la longitud del string actual.

Resolveremos cada paso 2 usando DP y otro KMP en tiempo amortizado O(N)\mathcal O(N).

Si sabemos dónde coincide una plantilla candidata en un string dado, entonces también podemos comprobar si es una plantilla en tiempo lineal. Sin embargo, esto correrá en tiempo cuadrático en total si no tenemos cuidado.

La observación clave es que, como sabemos que la plantilla candidata también es una plantilla del prefijo propio más largo del string dado, solo necesitamos hacer la DP sobre la parte restante del string. ¡Esto significa que solo necesitamos procesar cada carácter de SS en la DP a lo sumo una vez!

Para hallar las coincidencias con la misma eficiencia, ejecutamos KMP sobre el mismo string parcial en cada paso recursivo.

#include <bits/stdc++.h> using namespace std; string s; int p[500000]; int shortest_template(int n) { if (!p[n - 1]) return n; int cand = shortest_template(p[n - 1]); for (int i = p[n - 1], j = cand, curr = p[n - 1] - 1; i < n; i++) { while (j && (j == cand || s[i] != s[j])) j = p[j - 1]; if (s[i] == s[j]) j++; if (j == cand) curr = i; if (i - curr >= cand) return n; } return cand; } int main() { cin.tie(0)->sync_with_stdio(0); cin >> s; int n = s.size(); for (int i = 1; i < n; i++) { p[i] = p[i - 1]; while (p[i] && s[i] != s[p[i]]) p[i] = p[p[i] - 1]; if (s[i] == s[p[i]]) p[i]++; } cout << shortest_template(n); return 0; }