26 Aug 2023, 9:11 AM
26 Aug 2023, 9:11 AM

Stringhe con Dynamic Programming

Sottosequenza comune massimale

Problema

Date due sequenze T e U, trovare la più lunga sottosequenza comune di T e U.
Vediamo la definizione di sottosequenza, sottosequenza comune e sottosequenza comune massimale

Sottosequenza

  • Una sequenza P è una sottosequenza di T se P è ottenuto da T rimuovendo uno o più dei suoi elementi
  • Alternativamente: P è definito come il sottoinsieme degli indici 1,...,n degli elementi di T che compaiono anche in P
  • I rimanenti elementi sono elencati nello stesso ordine, senza essere necessariamente contigui
  • La sequenza vuota ∅ è sottosequenza di ogni altra sequenza

Esempio: P = "AAATA", T = "AAAATTGA"

Sottosequenza comune

Una sequenza X è una sottosequenza comune (common subsequence) di due sequenze T, U, se è sottosequenza sia di T che di U.
Scriviamo XCS(T,U)

Esempio: P = "AAATA", T = "AAAATTGA", X = "ATA"

Sottosequenza comune massimale

Una sequenza XCS(T,U) è una sottosequenza comune massimale (longest common subsequence) di due sequenze T, U, se non esiste altra sottosequenza comune YCS(T,U) tale che Y sia più lunga di X(|Y|>|X|). Scriviamo XLCS(T,U).

Esempio: P = "AAAATTGGA", T = "TAACGATATGGA", X = "AAATTGGA"

Soluzione

Prima di procedere, definiamo il prefisso di T. Data una sequenza T composta dai caratteri t1 t2 ...tn, T(i) denota il prefisso di T dato dai primi i caratteri.

Soluzione ricorsiva

Distinguiamo i vari casi che possono presentarsi quando vogliamo trovare la LCS di due stringhe T e U.

Caso 1 - Gli ultimi caratteri coincidono

Gli ultimi caratteri di T(i) e di U(j) coincidono: ti=uj .
Allora quel carattere è una sottosequenza e possiamo tranquillamente tenercela da parte per dopo e calcolare la LCS(T(i1),U(j1)).
Dunque:

LCS(T(i),U(j))=LCS(T(i1),U(j1))+ti

Esempio:
LCS("ALBERTO", "PIERO")=LCS("ALBERT", "PIER")+"O"

Caso 2 - Gli ultimi caratteri non coincidono

Gli ultimi caratteri di T(i) e di U(j) non coincidono: tiuj .
Allora bisogna controllare entrambi i casi T(i1),U(j) e T(i),U(j1).

Dunque:

LCS(T(i),U(j))=longest(LCS(T(i1),U(j)),LCS(T(i),U(j1)))

Esempio:
LCS("ALBERT", "PIER")=longest(LCS("ALBER", "PIER"),LCS("ALBERT", "PIE"))

Caso base

La più lunga sottosequenza di T(i) e U(j), quando uno dei prefissi è vuoto, i.e. se i=0 or j=0, è .

Esempio:
LCS("ALBERT",)=

Equazione di ricorrenza

LCS(T(i),U(j))={i=0 or j=0LCS(T(i1),U(j1))+tii>0 and j>0 and ti=ujlongest(LCS(T(i1),U(j)),LCS(T(i),U(j1)))i>0 and j>0 and tiuj

Implementazione
string LCS(const string& t, const string& u) {
    return LCSHelper(t, t.size() - 1, u, u.size() - 1);
}

string LCSHelper(const string& t, int i, const string& u, int j) {
    if(i < 0 || j < 0)
        return "";
    else if(t.at(i) == u.at(j))
        return LCSHelper(t, i - 1, u, j - 1) + t.at(i);
    else
        return max(LCSHelper(t, i, u, j - 1), LCSHelper(t, i - 1, u, j));
}

Soluzione iterativa con dynamic programming

Date due sequenze T e U di lunghezza n e m, DP[n][m] contiene la lunghezza della LCS del problema originale.

Equazione di ricorrenza

DP[i][j]={0i=0 or c=0DP[i1][j1]+1i>0 and j>0 and ti=ujmax(DP[i1][j],DP[i][j1])i>0 and j>0 and tiuj

Implementazione
int LCS(const string& t, int n, const string& u, int m) {
    vector<vector<int>> DP(n + 1, vector<int>(m + 1));

    for(int i = 0; i < m + 1; i++)
        DP[0][i] = 0;
    for(int i = 0; i < n + 1; i++)
        DP[i][0] = 0;

    for(int i = 1; i < n + 1; i++) {
        for(int j = 1; j < m + 1; j++) {
            if(t.at(i - 1) == u.at(j - 1))
                DP[i][j] = DP[i - 1][j - 1] + 1;
            else
                DP[i][j] = max(DP[i - 1][j], DP[i][j - 1]);
        }
    }

    return DP[n][m];
}

LCS-DP.png|800

Ricostruzione della soluzione

Visto che tramite dynamic programming possiamo solo ottenere la lunghezza della soluzione, è necessario fare un'altra passata sulla tabella DP per ricostruirci la soluzione.

Implementazione
string LCS(const string& t, int n, const string& u, int m) {
    vector<vector<int>> DP(n + 1, vector<int>(m + 1));

    for(int i = 0; i < m + 1; i++)
        DP[0][i] = 0;
    for(int i = 0; i < n + 1; i++)
        DP[i][0] = 0;

    for(int i = 1; i < n + 1; i++) {
        for(int j = 1; j < m + 1; j++) {
            if(t.at(i - 1) == u.at(j - 1))
                DP[i][j] = DP[i - 1][j - 1] + 1;
            else
                DP[i][j] = max(DP[i - 1][j], DP[i][j - 1]);
        }
    }

    return solution(t, n - 1, u, m - 1, DP);
}

string solution(const string& t, int i, const string& u, int j, const vector<vector<int>>& DP) {
    if(i < 0 || j < 0)
        return "";
    else if(t.at(i) == u.at(j)) {
        return solution(t, i - 1, u, j - 1, DP) + t.at(i);
    }
    else if(DP[i - 1][j] > DP[i][j - 1])
        return solution(t, i - 1, u, j, DP);
    else
        return solution(t, i, u, j - 1, DP);
}

Se invece si vuole solo misurare la lunghezza della LCS senza ricostruire la soluzione, è possibile conservare solo la riga precedente, ottimizzando così lo spazio a O(m).

Edit distance (string matching approssimato)

Problema

Siano:

Trovare un’occorrenza k-approssimata di P in T con k minimo (0km).

Occorrenza k-approssimata

Un’occorrenza k-approssimata di P in T è una copia di P in T in cui sono ammessi k "errori" tra P e T, del seguente tipo:

  1. I corrispondenti caratteri in P, T sono diversi (sostituzione)
  2. Un carattere in P non è incluso in T (inserimento)
  3. Un carattere in T non è incluso in P (cancellazione)

Esempio: T = "questoèunoscempio", P = "unesempio", Output = "2"
Devo cioè inserire una 'c' tra 's' ed 'e', e sostituire la 'e' in una 'o'.

Nota: Questo problema è praticamente identico al problema "edit distance" (detta anche distanza di Levenshtein), solo che non è necessario trasformare P in T, ma è sufficiente che P sia una sottostringa di T. Dunque nel nostro caso, dati T = "questoèunoscempio" e P = "unesempio" l'output sarà 2, nell'edit distance l'output è 2+10=12 (perchè bisogna aggiungere anche "questoèuno"). Vedremo che l'unica differenza è che nel nostro problema basta fare un ulteriore passata sull'ultima riga di DP.

Soluzione

Soluzione ricorsiva

Disclaimer

Come accennato sopra, questa soluzione ritorna il minimo numero di operazione per trasformare P in T, quindi non risolve proprio il nostro problema. Tuttavia è utile per capire la soluzione con dynamic progrramming.

Caso 1 - Gli ultimi caratteri coincidono

Gli ultimi caratteri di P(i) e di T(i) coincidono: pi=tj (usiamo i per scorrere P e j per scorrere T).
Dunque non dobbiamo fare nessuna operazione tra le tre elencate all'inizio, e possiamo chiamare 0 + editDistance(P,T,i1,j1). Sommiamo 0 perchè non è stata fatta alcuna operazione.

Esempio:
editDistance("ABA", "ACA")=editDistance("AB", "AC")+0

Caso 2 - Gli ultimi caratteri non coincidono - sostituzione

Gli ultimi caratteri di P(i) e di T(i) non coincidono: tiuj. Assumiamo allora che sia stata fatta una sostituzione del carattere e chiamiamo 1 + editDistance(P,T,i1,j1). Sommiamo 1 perchè assumiamo sia stato fatta una sostituzione.

Esempio:
editDistance("CA", "CC")=editDistance("C", "C")+1

Caso 3 - Gli ultimi caratteri non coincidono - inserimento

Gli ultimi caratteri di P(i) e di T(i) non coincidono: pi=tj. Assumiamo allora che sia stata fatta un inserimento del carattere e chiamiamo 1 + editDistance(P,T,i,j1). Sommiamo 1 perchè assumiamo sia stato fatto un inserimento.

Esempio:
editDistance("AB", "ABB")=editDistance("AB", "AB")+1

Caso 4 - Gli ultimi caratteri non coincidono - rimozione

Gli ultimi caratteri di P(i) e di T(i) non coincidono: pi=tj. Assumiamo allora che sia stata fatta una rimozione del carattere e chiamiamo 1 + editDistance(P,T,i1,j). Sommiamo 1 perchè assumiamo sia stato fatto una rimozione.

Esempio:
editDistance("ABB", "AB")=editDistance("AB", "AB")+1

Caso base

Se i=0 or j=0, devo fare o sizeof(P) rimozioni o sizeof(T) inserimenti.

Esempio:
editDistance("ABC",)=sizeof("ABC")=sizeof(P)=3
Ossia, devo effettuare 3 rimozioni per trasformare "ABC" in "".

Esempio:
editDistance(,"ABC")=sizeof("ABC")=sizeof(T)=3
Ossia, devo effettuare 3 inserimenti per trasformare "" in "ABC".

Implementazione
int min(int x, int y, int z) { return min(min(x, y), z); }

int editDistance(const string& p, const string& t) {
    // We pass size() so we can return i or j and not ++i or ++j (they would be -1)
    return editDistance(p, p.size(), t, t.size());
}

int editDistance(const string& p, int i, const string& t, int j) {
    // If first string is empty, the only option is to
    // insert all characters of second string into first
    if(i == 0)
        return j;
    // If second string is empty, the only option is to
    // remove all characters of first string
    else if(j == 0)
        return i;
    // If last characters of two strings are same, nothing much to do. 
    // Ignore last characters and get count for remaining strings.
    else if(p.at(i - 1) == t.at(j - 1))
        return editDistance(p, i - 1, t, j - 1) + 0;
    // If last characters are not the same, consider all three
    // operations on last character of first string,
    // recursively compute minimum cost for all three
    // operations and take minimum of three values.
    else return 1 + min(
                editDistance(p, i - 1, t, j - 1), // Replace
                editDistance(p, i, t, j - 1), // Insert
                editDistance(p, i - 1, t, j) // Remove
            );
}

Soluzione iterativa con dynamic programming

Sia DP[0 ... m][0 ... n] una tabella di programmazione dinamica tale che DP[i][j] sia il minimo valore k per cui esiste un’occorrenza k-approssimata di P(i) in T(j) che termina nella posizione j.

Equazione di ricorrenza

DP[i][j]={0i=0ij=0min(DP[i1][j1]+0,DP[i1][j1]+1,DP[i][j1]+1,DP[i1][j]+1)altrimenti

Implementazione
int editDistance(const string& p, int n, const string& t, int m) {
    vector<vector<int>> DP(n + 1, vector<int>(m + 1));

    for(int i = 0; i < m + 1; i++)
        DP[0][i] = 0; // with DP[0][i] = i we would resolve the original edit distance
    for(int i = 1; i < n + 1; i++)
        DP[i][0] = i;

    for(int i = 1; i < n + 1; i++) {
        for(int j = 1; j < m + 1; j++) {
            if(p.at(i - 1) == t.at(j - 1))
                DP[i][j] = DP[i - 1][j - 1] + 0; // Nothing to do
            else
                DP[i][j] = 1 + min(DP[i - 1][j - 1], // Replace
                                   DP[i][j - 1], // Insert
                                   DP[i - 1][j]); // Remove
        }
    }
    return *min_element(DP[n].begin(), DP[n].end());
}

Dunque non è detto che la "soluzione finale" si trovi nella casella "in basso a destra". È invece possibile che la soluzione debba essere ricercata essa stessa nella tabella DP.