Di recente ho implementato l'algoritmo di distanza Damerau-Levenshtein dallo pseudocodice su Wikipedia. Non riuscivo a trovare alcuna spiegazione esattamente come funziona e lo pseudocodice utilizza nomi di variabili completamente uninformative come DA
, DB
, i1
, e j1
che mi ha lasciato graffiare la mia testa.
Ecco la mia implementazione in Python: https://gist.github.com/badocelot/5327337
L'implementazione di Python mi ha aiutato a seguire il programma e capire cosa stava succedendo, rinominando le variabili con nomi più utili. Conoscevo abbastanza l'approccio di Wagner-Fischer al calcolo della distanza di Levenshtein da avere un quadro di riferimento.
A rischio di essere troppo lungo, ecco come capisco Damerau-Levenshtein:
Le variabili misteriose:
DA
(last_row
nel mio codice) è una specie di mappa che contiene l'ultima riga su cui è stato visto ogni elemento; nel mio codice è un vero dizionario PythonDB
(last_match_col
) contiene l'ultima colonna in cui la letterab
corrisponde alla letteraa
della riga correntei1
(last_matching_row
) è il numero di riga diDA
per la lettera corrente inb
j1
è solo una copia del valore diDB
/last_match_col
prima che sia potenzialmente aggiornato; nel mio codice ho appena spostato dovelast_match_col
viene aggiornato ed eliminato questa variabile
Il costo di recepimento:
H[i1][j1] + (i-i1-1) + 1 + (j-j1-1)
sta calcolando il costo dello scambio del personaggio corrente b
con l'ultimo personaggio in cui b
si trova a
(l'ultima partita), trattando tutti i personaggi in mezzo come aggiunte o eliminazioni.
Componenti del costo:
H[i1][j1]
ripristina il costo base al punto nei calcoli prima del recepimento, poiché la ricerca di un recepimento invalida il lavoro precedente(i-i1-1)
è la distanza tra la riga corrente e l'ultima riga corrispondente al carattere corrente, che è il numero di eliminazioni che sarebbero richieste(j-j1-1)
è la distanza tra la colonna corrente e l'ultima colonna con una corrispondenza, che è il numero di aggiunte- Il supplemento
+ 1
è solo il costo della trasposizione stessa
Se questa analisi non è corretta, mi piacerebbe sapere dove ho sbagliato. Come ho detto, non sono riuscito a trovare alcuna spiegazione dettagliata di come funziona l'algoritmo online.
Versione migliorata?
Avendolo capito, però, mi ha colpito il fatto che calcolando il costo di entrambe le aggiunte e le eliminazioni tra lettere trasposte sembrava difettoso: un'aggiunta e una cancellazione equivalgono a una sostituzione, che non sta verificando.
Se tutto ciò che è corretto, la soluzione dovrebbe essere banale: il costo delle lettere tra le lettere trasposte dovrebbe essere il maggiore tra le aggiunte e le cancellazioni: convertire il maggior numero di sostituzioni possibile e aggiungere eventuali aggiunte o eliminazioni rimaste.
Quindi il costo sarebbe:
H[i1][j1] + max((i-i1-1), (j-j1-1)) + 1
Ecco il mio codice per questa versione: https://gist.github.com/badocelot/5327427
Da alcuni semplici test, questo sembra corretto. Ad esempio, "abcdef" -> "abcfad" fornisce una distanza di modifica di 2 (trasporre "d" e "f", cambia "e" in "a"), mentre l'algoritmo originale fornisce una distanza di 3 (le ultime tre le lettere sono sostituzioni o 1 trasposizione + 1 aggiunta + 1 cancellazione).
Ora, non posso essere la prima persona a pensarci. Quindi, perché non l'ho incontrato? Non ho cercato abbastanza a lungo? O c'è qualche sottile difetto che impedisce a questo di funzionare davvero?