Distancia de edición (Levenshtein)
El mínimo de letras que hay que sustituir, borrar o insertar para convertir una palabra en otra. Se calcula con una tabla de prefijos y es la base de los correctores y del diff.
nivel avanzadoTambién: Levenshtein, edit distance, distancia de Levenshtein, corrector ortográfico
Visualízalo paso a paso
Cambia los datos, dale a reproducir y sigue cada paso en el dibujo, en la línea de Java que se ejecuta y en sus variables.
Distancia de edición
Escribe dos palabras: la tabla calcula cuántas operaciones (sustituir, borrar o insertar una letra) hacen falta como mínimo para convertir una en la otra.
Paso 1
dp[i][j] = las operaciones para convertir las i primeras letras de «casa» en las j primeras de «cesta». La primera columna y la primera fila son los casos fáciles: convertir algo en la palabra vacía es borrarlo todo, y al revés, insertarlo todo.
1static int distancia(String a, String b) {
2 int[][] dp = new int[a.length() + 1][b.length() + 1];
3 for (int i = 0; i <= a.length(); i++) dp[i][0] = i; // a = casa, b = cesta
4 for (int j = 0; j <= b.length(); j++) dp[0][j] = j;
5 for (int i = 1; i <= a.length(); i++)
6 for (int j = 1; j <= b.length(); j++) {
7 if (a.charAt(i - 1) == b.charAt(j - 1))
8 dp[i][j] = dp[i - 1][j - 1];
9 else // sustituir, borrar o insertar
10 dp[i][j] = 1 + Math.min(dp[i - 1][j - 1],
11 Math.min(dp[i - 1][j], dp[i][j - 1]));
12 }
13 return dp[a.length()][b.length()];
14}Variables
- a
- casa
- b
- cesta
Atajos con el foco dentro del visualizador: ← → paso a paso, Espacio reproducir o pausar, Inicio/Fin ir al principio o al final.
La idea
La distancia de edición (o de Levenshtein) entre dos palabras es el número mínimo de operaciones de una letra (sustituir, borrar o insertar) para convertir una en la otra. De «casa» a «cesta» hay 2: cambiar la a por una e e insertar una t. Cuanto más pequeña, más se parecen.
Se piensa desde el final. Si las dos palabras acaban en la misma letra, esa letra no cuesta nada y queda el problema de los prefijos sin ella. Si acaban en letras distintas, hay tres opciones: sustituir la última (y resolver los dos prefijos), borrar la última de la primera o insertar al final la última de la segunda. Se coge la más barata y se suma 1. Escrito tal cual con recursividad, repite los mismos prefijos una y otra vez: exponencial.
Con una tabla de (n + 1) × (m + 1) cada pareja de prefijos se calcula una sola vez. dp[i][j] es la distancia entre las i primeras letras de una palabra y las j primeras de la otra. La primera fila y la primera columna son triviales (pasar de nada a j letras son j inserciones) y cada casilla sale de tres vecinas: la diagonal (sustituir o dejar), la de arriba (borrar) y la de la izquierda (insertar). La respuesta queda en la esquina.
Recorriendo la tabla al revés, desde la esquina, se recupera la lista de operaciones. La misma idea, con otra regla para rellenar las casillas, da la subsecuencia común más larga, que es lo que usa diff (y Git) para comparar ficheros línea a línea.
Cuándo usarlo
- Correctores ortográficos y búsquedas tolerantes a erratas («¿quisiste decir…?»).
- Detectar registros duplicados con pequeñas diferencias: «Jose Garcia» y «José García», «c/ Mayor 3» y «C. Mayor, 3».
- Comparar secuencias en bioinformática (ADN, proteínas), con costes distintos para cada operación.
- Comparar versiones de un texto o de código (con la variante de la subsecuencia común más larga).
Cuándo no
- Con textos enormes: n · m casillas para dos libros es demasiado. Los diff reales trabajan por líneas y con algoritmos más listos (Myers).
- Si las dos cadenas tienen la misma longitud y solo puede haber sustituciones: la distancia de Hamming se calcula en O(n).
- Si importan los intercambios de letras vecinas («algoritmo» / «algorimto»): Levenshtein cuenta 2; la variante de Damerau cuenta 1.
Paso a paso
- Bordes. dp[i][0] = i (borrar las i letras) y dp[0][j] = j (insertar las j letras).
- Letras iguales. Si a[i − 1] == b[j − 1], dp[i][j] = dp[i − 1][j − 1]: esa letra no cuesta nada.
- Letras distintas. dp[i][j] = 1 + el mínimo de la diagonal (sustituir), la de arriba (borrar) y la de la izquierda (insertar).
- Respuesta y operaciones. La distancia está en dp[n][m]. Para saber qué operaciones son, se vuelve desde esa esquina por las casillas de las que salió cada valor.
El código
Distancia y operaciones
Cuatro parejas de palabras con su distancia y la lista de operaciones reconstruida desde la esquina de la tabla (→ sustituir, - borrar, + insertar).
1import java.util.*;
2
3public class Main {
4 static int[][] tabla(String a, String b) {
5 int[][] dp = new int[a.length() + 1][b.length() + 1];
6 for (int i = 0; i <= a.length(); i++) dp[i][0] = i;
7 for (int j = 0; j <= b.length(); j++) dp[0][j] = j;
8 for (int i = 1; i <= a.length(); i++)
9 for (int j = 1; j <= b.length(); j++)
10 dp[i][j] = a.charAt(i - 1) == b.charAt(j - 1) ? dp[i - 1][j - 1]
11 : 1 + Math.min(dp[i - 1][j - 1], Math.min(dp[i - 1][j], dp[i][j - 1]));
12 return dp;
13 }
14
15 /** Vuelve desde la esquina eligiendo de qué casilla salió cada valor: así salen las operaciones. */
16 static List<String> operaciones(String a, String b, int[][] dp) {
17 LinkedList<String> ops = new LinkedList<>();
18 int i = a.length(), j = b.length();
19 while (i > 0 || j > 0) {
20 if (i > 0 && j > 0 && a.charAt(i - 1) == b.charAt(j - 1) && dp[i][j] == dp[i - 1][j - 1]) {
21 i--;
22 j--;
23 } else if (i > 0 && j > 0 && dp[i][j] == dp[i - 1][j - 1] + 1) {
24 ops.addFirst(a.charAt(i - 1) + "→" + b.charAt(j - 1)); // sustituir
25 i--;
26 j--;
27 } else if (i > 0 && dp[i][j] == dp[i - 1][j] + 1) {
28 ops.addFirst("-" + a.charAt(i - 1)); // borrar
29 i--;
30 } else {
31 ops.addFirst("+" + b.charAt(j - 1)); // insertar
32 j--;
33 }
34 }
35 return ops;
36 }
37
38 public static void main(String[] args) {
39 String[][] parejas = {{"casa", "cesta"}, {"gato", "pato"}, {"kitten", "sitting"}, {"algoritmo", "logaritmo"}};
40 for (String[] p : parejas) {
41 int[][] dp = tabla(p[0], p[1]);
42 System.out.println(p[0] + " → " + p[1] + ": " + dp[p[0].length()][p[1].length()] + " (" + String.join(", ", operaciones(p[0], p[1], dp)) + ")");
43 }
44 }
45}def tabla(a, b):
dp = [[0] * (len(b) + 1) for _ in range(len(a) + 1)]
for i in range(len(a) + 1):
dp[i][0] = i
for j in range(len(b) + 1):
dp[0][j] = j
for i in range(1, len(a) + 1):
for j in range(1, len(b) + 1):
dp[i][j] = dp[i - 1][j - 1] if a[i - 1] == b[j - 1] else 1 + min(dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1])
return dp
def operaciones(a, b, dp):
"""Vuelve desde la esquina eligiendo de qué casilla salió cada valor: así salen las operaciones."""
ops = []
i, j = len(a), len(b)
while i > 0 or j > 0:
if i > 0 and j > 0 and a[i - 1] == b[j - 1] and dp[i][j] == dp[i - 1][j - 1]:
i -= 1
j -= 1
elif i > 0 and j > 0 and dp[i][j] == dp[i - 1][j - 1] + 1:
ops.insert(0, a[i - 1] + "→" + b[j - 1]) # sustituir
i -= 1
j -= 1
elif i > 0 and dp[i][j] == dp[i - 1][j] + 1:
ops.insert(0, "-" + a[i - 1]) # borrar
i -= 1
else:
ops.insert(0, "+" + b[j - 1]) # insertar
j -= 1
return ops
for x, y in [("casa", "cesta"), ("gato", "pato"), ("kitten", "sitting"), ("algoritmo", "logaritmo")]:
dp = tabla(x, y)
print(f"{x} → {y}: {dp[len(x)][len(y)]} ({', '.join(operaciones(x, y, dp))})")function tabla(a, b) {
const dp = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
for (let i = 0; i <= a.length; i++) dp[i][0] = i;
for (let j = 0; j <= b.length; j++) dp[0][j] = j;
for (let i = 1; i <= a.length; i++)
for (let j = 1; j <= b.length; j++)
dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1]
: 1 + Math.min(dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]);
return dp;
}
/** Vuelve desde la esquina eligiendo de qué casilla salió cada valor: así salen las operaciones. */
function operaciones(a, b, dp) {
const ops = [];
let i = a.length, j = b.length;
while (i > 0 || j > 0) {
if (i > 0 && j > 0 && a[i - 1] === b[j - 1] && dp[i][j] === dp[i - 1][j - 1]) {
i--;
j--;
} else if (i > 0 && j > 0 && dp[i][j] === dp[i - 1][j - 1] + 1) {
ops.unshift(a[i - 1] + "→" + b[j - 1]); // sustituir
i--;
j--;
} else if (i > 0 && dp[i][j] === dp[i - 1][j] + 1) {
ops.unshift("-" + a[i - 1]); // borrar
i--;
} else {
ops.unshift("+" + b[j - 1]); // insertar
j--;
}
}
return ops;
}
for (const [x, y] of [["casa", "cesta"], ["gato", "pato"], ["kitten", "sitting"], ["algoritmo", "logaritmo"]]) {
const dp = tabla(x, y);
console.log(`${x} → ${y}: ${dp[x.length][y.length]} (${operaciones(x, y, dp).join(", ")})`);
}using System;
using System.Collections.Generic;
class Program {
static int[,] Tabla(string a, string b) {
int[,] dp = new int[a.Length + 1, b.Length + 1];
for (int i = 0; i <= a.Length; i++) dp[i, 0] = i;
for (int j = 0; j <= b.Length; j++) dp[0, j] = j;
for (int i = 1; i <= a.Length; i++)
for (int j = 1; j <= b.Length; j++)
dp[i, j] = a[i - 1] == b[j - 1] ? dp[i - 1, j - 1]
: 1 + Math.Min(dp[i - 1, j - 1], Math.Min(dp[i - 1, j], dp[i, j - 1]));
return dp;
}
/// Vuelve desde la esquina eligiendo de qué casilla salió cada valor: así salen las operaciones.
static List<string> Operaciones(string a, string b, int[,] dp) {
var ops = new List<string>();
int i = a.Length, j = b.Length;
while (i > 0 || j > 0) {
if (i > 0 && j > 0 && a[i - 1] == b[j - 1] && dp[i, j] == dp[i - 1, j - 1]) {
i--;
j--;
} else if (i > 0 && j > 0 && dp[i, j] == dp[i - 1, j - 1] + 1) {
ops.Insert(0, a[i - 1] + "→" + b[j - 1]); // sustituir
i--;
j--;
} else if (i > 0 && dp[i, j] == dp[i - 1, j] + 1) {
ops.Insert(0, "-" + a[i - 1]); // borrar
i--;
} else {
ops.Insert(0, "+" + b[j - 1]); // insertar
j--;
}
}
return ops;
}
static void Main() {
string[][] parejas = { new[] { "casa", "cesta" }, new[] { "gato", "pato" }, new[] { "kitten", "sitting" }, new[] { "algoritmo", "logaritmo" } };
foreach (var p in parejas) {
int[,] dp = Tabla(p[0], p[1]);
Console.WriteLine(p[0] + " → " + p[1] + ": " + dp[p[0].Length, p[1].Length] + " (" + string.Join(", ", Operaciones(p[0], p[1], dp)) + ")");
}
}
}<?php
function tabla(string $a, string $b): array {
$dp = array_fill(0, strlen($a) + 1, array_fill(0, strlen($b) + 1, 0));
for ($i = 0; $i <= strlen($a); $i++) $dp[$i][0] = $i;
for ($j = 0; $j <= strlen($b); $j++) $dp[0][$j] = $j;
for ($i = 1; $i <= strlen($a); $i++)
for ($j = 1; $j <= strlen($b); $j++)
$dp[$i][$j] = $a[$i - 1] === $b[$j - 1] ? $dp[$i - 1][$j - 1]
: 1 + min($dp[$i - 1][$j - 1], $dp[$i - 1][$j], $dp[$i][$j - 1]);
return $dp;
}
/** Vuelve desde la esquina eligiendo de qué casilla salió cada valor: así salen las operaciones. */
function operaciones(string $a, string $b, array $dp): array {
$ops = [];
$i = strlen($a);
$j = strlen($b);
while ($i > 0 || $j > 0) {
if ($i > 0 && $j > 0 && $a[$i - 1] === $b[$j - 1] && $dp[$i][$j] == $dp[$i - 1][$j - 1]) {
$i--;
$j--;
} elseif ($i > 0 && $j > 0 && $dp[$i][$j] == $dp[$i - 1][$j - 1] + 1) {
array_unshift($ops, $a[$i - 1] . "→" . $b[$j - 1]); // sustituir
$i--;
$j--;
} elseif ($i > 0 && $dp[$i][$j] == $dp[$i - 1][$j] + 1) {
array_unshift($ops, "-" . $a[$i - 1]); // borrar
$i--;
} else {
array_unshift($ops, "+" . $b[$j - 1]); // insertar
$j--;
}
}
return $ops;
}
foreach ([["casa", "cesta"], ["gato", "pato"], ["kitten", "sitting"], ["algoritmo", "logaritmo"]] as [$x, $y]) {
$dp = tabla($x, $y);
echo "$x → $y: " . $dp[strlen($x)][strlen($y)] . " (" . implode(", ", operaciones($x, $y, $dp)) . ")\n";
}Salida al ejecutarlo (la misma en los 5 lenguajes)
casa → cesta: 2 (a→e, +t) gato → pato: 1 (g→p) kitten → sitting: 3 (k→s, e→i, +g) algoritmo → logaritmo: 3 (a→l, l→o, o→a)
Un corrector: la palabra más parecida
Para cada palabra mal escrita se busca la más cercana del diccionario, con la tabla reducida a dos filas. Fíjate en «algorimto»: cambiar dos letras de sitio cuesta 2.
1import java.util.*;
2
3public class Main {
4 /** Con dos filas basta: cada fila solo necesita la anterior. */
5 static int distancia(String a, String b) {
6 int[] prev = new int[b.length() + 1], cur = new int[b.length() + 1];
7 for (int j = 0; j <= b.length(); j++) prev[j] = j;
8 for (int i = 1; i <= a.length(); i++) {
9 cur[0] = i;
10 for (int j = 1; j <= b.length(); j++)
11 cur[j] = a.charAt(i - 1) == b.charAt(j - 1) ? prev[j - 1] : 1 + Math.min(prev[j - 1], Math.min(prev[j], cur[j - 1]));
12 int[] t = prev;
13 prev = cur;
14 cur = t;
15 }
16 return prev[b.length()];
17 }
18
19 public static void main(String[] args) {
20 List<String> diccionario = List.of("casa", "cosa", "caso", "clase", "programa", "algoritmo", "variable", "funcion");
21 for (String palabra : List.of("progama", "algorimto", "clace", "kasa", "varaible", "zzz")) {
22 String mejor = null;
23 int d = Integer.MAX_VALUE;
24 for (String w : diccionario) {
25 int x = distancia(palabra, w);
26 if (x < d) { // con empate se queda la primera del diccionario
27 d = x;
28 mejor = w;
29 }
30 }
31 System.out.println(d <= 2 ? palabra + " → ¿quisiste decir «" + mejor + "»? (distancia " + d + ")" : palabra + " → sin sugerencias (la más cercana está a " + d + ")");
32 }
33 }
34}def distancia(a, b):
"""Con dos filas basta: cada fila solo necesita la anterior."""
prev = list(range(len(b) + 1))
for i in range(1, len(a) + 1):
cur = [i] + [0] * len(b)
for j in range(1, len(b) + 1):
cur[j] = prev[j - 1] if a[i - 1] == b[j - 1] else 1 + min(prev[j - 1], prev[j], cur[j - 1])
prev = cur
return prev[len(b)]
diccionario = ["casa", "cosa", "caso", "clase", "programa", "algoritmo", "variable", "funcion"]
for palabra in ["progama", "algorimto", "clace", "kasa", "varaible", "zzz"]:
mejor, d = None, float("inf")
for w in diccionario:
x = distancia(palabra, w)
if x < d: # con empate se queda la primera del diccionario
d, mejor = x, w
if d <= 2:
print(f"{palabra} → ¿quisiste decir «{mejor}»? (distancia {d})")
else:
print(f"{palabra} → sin sugerencias (la más cercana está a {d})")/** Con dos filas basta: cada fila solo necesita la anterior. */
function distancia(a, b) {
let prev = Array.from({ length: b.length + 1 }, (_, j) => j);
let cur = new Array(b.length + 1).fill(0);
for (let i = 1; i <= a.length; i++) {
cur[0] = i;
for (let j = 1; j <= b.length; j++)
cur[j] = a[i - 1] === b[j - 1] ? prev[j - 1] : 1 + Math.min(prev[j - 1], prev[j], cur[j - 1]);
[prev, cur] = [cur, prev];
}
return prev[b.length];
}
const diccionario = ["casa", "cosa", "caso", "clase", "programa", "algoritmo", "variable", "funcion"];
for (const palabra of ["progama", "algorimto", "clace", "kasa", "varaible", "zzz"]) {
let mejor = null, d = Infinity;
for (const w of diccionario) {
const x = distancia(palabra, w);
if (x < d) { // con empate se queda la primera del diccionario
d = x;
mejor = w;
}
}
console.log(d <= 2 ? `${palabra} → ¿quisiste decir «${mejor}»? (distancia ${d})` : `${palabra} → sin sugerencias (la más cercana está a ${d})`);
}using System;
class Program {
/// Con dos filas basta: cada fila solo necesita la anterior.
static int Distancia(string a, string b) {
int[] prev = new int[b.Length + 1], cur = new int[b.Length + 1];
for (int j = 0; j <= b.Length; j++) prev[j] = j;
for (int i = 1; i <= a.Length; i++) {
cur[0] = i;
for (int j = 1; j <= b.Length; j++)
cur[j] = a[i - 1] == b[j - 1] ? prev[j - 1] : 1 + Math.Min(prev[j - 1], Math.Min(prev[j], cur[j - 1]));
(prev, cur) = (cur, prev);
}
return prev[b.Length];
}
static void Main() {
string[] diccionario = { "casa", "cosa", "caso", "clase", "programa", "algoritmo", "variable", "funcion" };
foreach (string palabra in new[] { "progama", "algorimto", "clace", "kasa", "varaible", "zzz" }) {
string? mejor = null;
int d = int.MaxValue;
foreach (string w in diccionario) {
int x = Distancia(palabra, w);
if (x < d) { // con empate se queda la primera del diccionario
d = x;
mejor = w;
}
}
Console.WriteLine(d <= 2 ? quot;{palabra} → ¿quisiste decir «{mejor}»? (distancia {d})" : quot;{palabra} → sin sugerencias (la más cercana está a {d})");
}
}
}<?php
/** Con dos filas basta: cada fila solo necesita la anterior. */
function distancia(string $a, string $b): int {
$prev = range(0, strlen($b));
$cur = array_fill(0, strlen($b) + 1, 0);
for ($i = 1; $i <= strlen($a); $i++) {
$cur[0] = $i;
for ($j = 1; $j <= strlen($b); $j++)
$cur[$j] = $a[$i - 1] === $b[$j - 1] ? $prev[$j - 1] : 1 + min($prev[$j - 1], $prev[$j], $cur[$j - 1]);
[$prev, $cur] = [$cur, $prev];
}
return $prev[strlen($b)];
}
$diccionario = ["casa", "cosa", "caso", "clase", "programa", "algoritmo", "variable", "funcion"];
foreach (["progama", "algorimto", "clace", "kasa", "varaible", "zzz"] as $palabra) {
$mejor = null;
$d = PHP_INT_MAX;
foreach ($diccionario as $w) {
$x = distancia($palabra, $w);
if ($x < $d) { // con empate se queda la primera del diccionario
$d = $x;
$mejor = $w;
}
}
echo $d <= 2 ? "$palabra → ¿quisiste decir «{$mejor}»? (distancia $d)\n" : "$palabra → sin sugerencias (la más cercana está a $d)\n";
}Salida al ejecutarlo (la misma en los 5 lenguajes)
progama → ¿quisiste decir «programa»? (distancia 1) algorimto → ¿quisiste decir «algoritmo»? (distancia 2) clace → ¿quisiste decir «clase»? (distancia 1) kasa → ¿quisiste decir «casa»? (distancia 1) varaible → ¿quisiste decir «variable»? (distancia 2) zzz → sin sugerencias (la más cercana está a 4)
Traza: La tabla de «casa» → «cesta»
| — | c | e | s | t | a | |
|---|---|---|---|---|---|---|
| — | 0 | 1 | 2 | 3 | 4 | 5 |
| c | 1 | 0 | 1 | 2 | 3 | 4 |
| a | 2 | 1 | 1 | 2 | 3 | 3 |
| s | 3 | 2 | 2 | 1 | 2 | 3 |
| a | 4 | 3 | 3 | 2 | 2 | 2 |
La esquina (2) es la distancia. Volviendo desde ella: a se queda, se inserta la t, s se queda, a → e y c se queda.
Complejidad
| Método | Tiempo | Memoria |
|---|---|---|
| Recursividad sin tabla | O(3^(n + m)) | O(n + m) |
| Tabla completa | O(n · m) | O(n · m) |
| Dos filas (solo la distancia) | O(n · m) | O(min(n, m)) |
| Hamming (misma longitud, solo sustituir) | O(n) | O(1) |
Con la tabla completa se pueden reconstruir las operaciones; con dos filas solo queda el número.
- Mejor caso: O(n²)
- Caso medio: O(n²)
- Peor caso: O(n²)
O(n · m): una casilla por pareja de prefijos de las dos palabras. La recursividad sin tabla repetiría subproblemas y sería exponencial. Las curvas grises son las demás clases, para comparar.
En la práctica
- Los correctores de los móviles y de los procesadores de texto sugieren las palabras del diccionario más cercanas.
- Los buscadores y las bases de datos ofrecen búsqueda difusa (PostgreSQL tiene
levenshtein()en la extensión fuzzystrmatch). diff, Git y las herramientas de revisión de código usan la subsecuencia común más larga, prima hermana de esta tabla.- En bioinformática, el alineamiento de secuencias (Needleman-Wunsch) es esta misma tabla con costes por letra.
- La limpieza de datos (deduplicar clientes, normalizar direcciones) compara cadenas con distancias de edición.
Errores típicos
- Tabla de n × m en vez de (n + 1) × (m + 1): sin la fila y la columna de la cadena vacía no hay casos base.
- Comparar a[i] con b[j] en lugar de a[i − 1] con b[j − 1]: la casilla i corresponde a la letra i − 1.
- Sumar 1 también cuando las letras son iguales.
- Escribir la recursividad sin memoización: con palabras de 15 letras ya tarda muchísimo.
- En PHP,
strleny$s[$i]cuentan bytes: con tildes o eñes hay que usarmb_str_split.
Ejercicios
Cada ejercicio se corrige solo con sus pruebas (algunas ocultas). Escribe tu solución en el editor y pulsa Ejecutar o Comprobar; la solución explicada está debajo, por si te atascas.
1. ¿Quisiste decir…?
La primera línea es un diccionario de palabras. Para cada línea siguiente, di si la palabra está en el diccionario o sugiere la más parecida (la de menor distancia de edición). El main ya recorre el diccionario en orden alfabético y escribe la respuesta: completa distancia.
- Primera línea: las palabras del diccionario separadas por espacios. Después, una palabra por línea (se pasan a minúsculas).
- Si está:
casa: correcta. Si no:kasa → casa (1 cambio)(o2 cambios), con la palabra más cercana; si empatan, la primera en orden alfabético. - Si la más cercana está a más de 3:
xyzzy: sin sugerencias.
Ejemplo
casa cosa caso clase mesa programa casa kasa clace progama
casa: correcta kasa → casa (1 cambio) clace → clase (1 cambio) progama → programa (1 cambio)
Ver la solución explicada
1import java.util.*;
2
3public class Main {
4 /** Cuántas letras hay que sustituir, borrar o insertar, como mínimo, para pasar de a a b. */
5 static int distancia(String a, String b) {
6 int[][] dp = new int[a.length() + 1][b.length() + 1];
7 for (int i = 0; i <= a.length(); i++) dp[i][0] = i;
8 for (int j = 0; j <= b.length(); j++) dp[0][j] = j;
9 for (int i = 1; i <= a.length(); i++)
10 for (int j = 1; j <= b.length(); j++)
11 dp[i][j] = a.charAt(i - 1) == b.charAt(j - 1) ? dp[i - 1][j - 1]
12 : 1 + Math.min(dp[i - 1][j - 1], Math.min(dp[i - 1][j], dp[i][j - 1]));
13 return dp[a.length()][b.length()];
14 }
15
16 public static void main(String[] args) {
17 Scanner sc = new Scanner(System.in);
18 if (!sc.hasNextLine()) return;
19 TreeSet<String> diccionario = new TreeSet<>(Arrays.asList(sc.nextLine().trim().toLowerCase().split("\\s+")));
20 while (sc.hasNextLine()) {
21 String palabra = sc.nextLine().trim().toLowerCase();
22 if (palabra.isEmpty()) continue;
23 if (diccionario.contains(palabra)) {
24 System.out.println(palabra + ": correcta");
25 continue;
26 }
27 String mejor = null;
28 int d = Integer.MAX_VALUE;
29 for (String w : diccionario) { // en orden alfabético: con empate gana la primera
30 int x = distancia(palabra, w);
31 if (x < d) {
32 d = x;
33 mejor = w;
34 }
35 }
36 System.out.println(d <= 3 ? palabra + " → " + mejor + " (" + d + (d == 1 ? " cambio)" : " cambios)") : palabra + ": sin sugerencias");
37 }
38 }
39}Un corrector de verdad añade más cosas (frecuencia de uso de cada palabra, teclas vecinas, un índice para no comparar con todo el diccionario), pero el corazón es esta distancia.
Con un diccionario de 100.000 palabras se compararían 100.000 tablas por palabra: por eso se usan estructuras como los BK-trees, que descartan palabras sin calcular su distancia.
2. Un diff de dos versiones
Compara dos versiones de un texto línea a línea y escribe un diff: las líneas que se mantienen, las que se borran y las que se añaden, con el mínimo de cambios. La clave es la subsecuencia común más larga (LCS): las líneas que se mantienen son una subsecuencia común lo más larga posible. El main lee las dos versiones y cuenta los cambios: completa diff.
- Entrada: las líneas de la versión antigua, una línea
---y las de la nueva. - Salida: una línea por cada línea del diff,
= texto(se mantiene),- texto(se borra) o+ texto(se añade), y al final2 líneas borradas, 1 añadida. - Cuando haya varias formas igual de buenas, primero los borrados y luego las inserciones: recorre las dos versiones desde el principio con la tabla
l[i][j]= LCS dea[i..]yb[j..]; si las líneas son iguales,=; sil[i + 1][j] >= l[i][j + 1], borra; si no, añade. Sin la línea---:Falta la línea --- que separa las dos versiones.
Ejemplo
int a = 1; int b = 2; print(a + b); --- int a = 1; int b = 3; print(a + b);
= int a = 1; - int b = 2; + int b = 3; = print(a + b); 1 línea borrada, 1 añadida
Ver la solución explicada
1import java.util.*;
2
3public class Main {
4 /** El diff de a a b: "= línea" si se mantiene, "- línea" si se borra y "+ línea" si se añade. Se basa en
5 la subsecuencia común más larga: l[i][j] = la más larga entre a[i..] y b[j..]. */
6 static List<String> diff(List<String> a, List<String> b) {
7 int n = a.size(), m = b.size();
8 int[][] l = new int[n + 1][m + 1];
9 for (int i = n - 1; i >= 0; i--)
10 for (int j = m - 1; j >= 0; j--)
11 l[i][j] = a.get(i).equals(b.get(j)) ? l[i + 1][j + 1] + 1 : Math.max(l[i + 1][j], l[i][j + 1]);
12 List<String> res = new ArrayList<>();
13 int i = 0, j = 0;
14 while (i < n || j < m) {
15 if (i < n && j < m && a.get(i).equals(b.get(j))) {
16 res.add("= " + a.get(i));
17 i++;
18 j++;
19 } else if (i < n && (j == m || l[i + 1][j] >= l[i][j + 1])) {
20 res.add("- " + a.get(i++)); // borrar no empeora: primero los borrados
21 } else {
22 res.add("+ " + b.get(j++));
23 }
24 }
25 return res;
26 }
27
28 public static void main(String[] args) {
29 Scanner sc = new Scanner(System.in);
30 List<String> a = new ArrayList<>(), b = new ArrayList<>();
31 boolean segunda = false;
32 while (sc.hasNextLine()) {
33 String linea = sc.nextLine();
34 if (linea.trim().equals("---")) {
35 segunda = true;
36 continue;
37 }
38 (segunda ? b : a).add(linea.trim());
39 }
40 if (!segunda) {
41 System.out.println("Falta la línea --- que separa las dos versiones");
42 return;
43 }
44 int borradas = 0, nuevas = 0;
45 for (String l : diff(a, b)) {
46 System.out.println(l);
47 if (l.startsWith("- ")) borradas++;
48 if (l.startsWith("+ ")) nuevas++;
49 }
50 System.out.println(borradas + (borradas == 1 ? " línea borrada, " : " líneas borradas, ") + nuevas + (nuevas == 1 ? " añadida" : " añadidas"));
51 }
52}La LCS es la distancia de edición sin sustituciones: maximizar lo que se mantiene es minimizar lo que se borra y se añade. La tabla es la misma idea con otra regla.
Rellenar la tabla con sufijos (desde el final) permite escribir el diff en orden, de la primera línea a la última, sin tener que darle la vuelta.
Test
Test: Distancia de edición (Levenshtein)
0/5 respondidas · 0 aciertosElige una respuesta en cada pregunta: verás al momento si es correcta y por qué. Con un 80 % de aciertos se da por superada.
1.¿Cuál es la distancia de edición entre «gato» y «pato»?
2.En la tabla, si a[i − 1] == b[j − 1], ¿cuánto vale dp[i][j]?
3.¿Qué operación representa venir de la casilla de arriba, dp[i − 1][j]?
4.¿Qué valores tiene la primera fila de la tabla (prefijo vacío de la primera palabra)?
5.¿Qué coste tiene la distancia de edición con tabla para palabras de longitudes n y m?