Salta al contenuto
Note per Studenti Longest Common Subsequence - ricorrenza, tabella e ricostruzione

Longest Common Subsequence - ricorrenza, tabella e ricostruzione

In questa pagina 9

La LCS è il secondo grande esempio di programmazione dinamicaSchema in cinque passi: sottostruttura ottima, ricorrenza sui costi, informazione addizionale, calcolo bottom-up o memoizzato, ricostruzioneProgrammazione dinamica - Fibonacci, memoizzazione e schema del paradigma → ed è il modello di tutti i problemi su stringhe che seguono.

Stringhe, prefissi, sottostringhe, sottosequenze

Sia Σ\Sigma un alfabeto finito. Una stringa è una sequenza finita X=⟨x1,…,xm⟩X = \langle x_1, \dots, x_m \rangle di simboli di Σ\Sigma; ε\varepsilon è la stringa vuota e Σ∗\Sigma^* l'insieme di tutte le stringhe finite (stella di Kleene).

  • Prefisso Xi=⟨x1,…,xi⟩X_i = \langle x_1, \dots, x_i \rangle, per 0≤i≤m0 \le i \le m, con X0=εX_0 = \varepsilon.
  • Suffisso Xi=⟨xi,…,xm⟩X^i = \langle x_i, \dots, x_m \rangle, per 1≤i≤m+11 \le i \le m + 1, con Xm+1=εX^{m+1} = \varepsilon.
  • Sottostringa Xi..j=⟨xi,…,xj⟩X_{i..j} = \langle x_i, \dots, x_j \rangle per 1≤i≤j≤m1 \le i \le j \le m (simboli contigui); Xi..j=εX_{i..j} = \varepsilon se i>ji > j.
  • Sottosequenza: una stringa Z=⟨z1,…,zk⟩Z = \langle z_1, \dots, z_k \rangle per cui esiste una successione di indici strettamente crescente 1≤j1<j2<⋯<jk≤m1 \le j_1 < j_2 < \dots < j_k \le m con zs=xjsz_s = x_{j_s}. Si dice che gli indici realizzano ZZ in XX. Una stessa ZZ può avere più successioni di indici.

Esempio. X=⟨A,B,C,B,B,D⟩X = \langle A, B, C, B, B, D \rangle e Z=⟨A,C,B⟩Z = \langle A, C, B \rangle: ZZ è sottosequenza di XX, realizzata da ⟨1,3,4⟩\langle 1, 3, 4 \rangle e anche da ⟨1,3,5⟩\langle 1, 3, 5 \rangle. Non è una sottostringa. Ogni sottostringa è una sottosequenza (con indici i,i+1,…,ji, i+1, \dots, j), non viceversa.

Quante sono. Le sottostringhe sono Θ(m2)\Theta(m^2): ce ne sono mm di lunghezza 11, m−1m - 1 di lunghezza 22, …\dots, una di lunghezza mm, più ε\varepsilon:

1+∑j=1m(m−j+1)=1+∑j′=1mj′=1+m(m+1)2.1 + \sum_{j=1}^{m}(m - j + 1) = 1 + \sum_{j'=1}^{m} j' = 1 + \frac{m(m+1)}{2}.

Le sottosequenze sono molte di più. Se i caratteri di XX sono tutti distinti, ogni sottoinsieme di kk indici di {1,…,m}\{1, \dots, m\} dà una sottosequenza distinta (la si ottiene ordinando gli indici), e i sottoinsiemi di taglia kk sono (mk)\binom{m}{k} (Fattoriale e coefficienti binomialiFattoriale, permutazioni, disposizioni, combinazioni e coefficiente binomiale n su k, con il triangolo di Tartaglia.Fattoriale e coefficienti binomiali →). In totale

∑k=0m(mk)=2m\sum_{k=0}^{m} \binom{m}{k} = 2^m

per il binomio di Newton (Binomio di NewtonLa formula per sviluppare (a+b)^n con i coefficienti binomiali.Binomio di Newton →); contando anche ε\varepsilon (per k=0k = 0) sono 2m2^m. Per questo un approccio che enumera le sottosequenze di XX e le verifica su YY è esponenziale.

Il problema

Definizione (LCS). Date X=⟨x1,…,xm⟩X = \langle x_1, \dots, x_m \rangle e Y=⟨y1,…,yn⟩Y = \langle y_1, \dots, y_n \rangle, determinare una stringa Z=⟨z1,…,zk⟩Z = \langle z_1, \dots, z_k \rangle di lunghezza massima che sia sottosequenza sia di XX sia di YY.

Esempio. X=⟨A,B,C,B,B,D⟩X = \langle A, B, C, B, B, D \rangle, Y=⟨A,D,C,C,B,D⟩Y = \langle A, D, C, C, B, D \rangle: la LCS è Z=⟨A,C,B,D⟩Z = \langle A, C, B, D \rangle, lunga 44, realizzata in XX da ⟨1,3,4,6⟩\langle 1, 3, 4, 6 \rangle e in YY da ⟨1,4,5,6⟩\langle 1, 4, 5, 6 \rangle.

La lunghezza della LCS misura la somiglianza tra due stringhe; si usa in genomica (confronto di sequenze di DNA) e nella correzione automatica dei testi.

La LCS può non essere unica: X=⟨A,B,C⟩X = \langle A, B, C \rangle e Y=⟨B,A,C⟩Y = \langle B, A, C \rangle hanno due LCS di lunghezza 22, ⟨A,C⟩\langle A, C \rangle e ⟨B,C⟩\langle B, C \rangle. Si cerca una LCS.

Proprietà di sottostruttura ottima

Idea: guardare l'ultimo simbolo. Se XX e YY finiscono con lo stesso simbolo, quel simbolo si può sempre accoppiare; altrimenti uno dei due simboli finali non è usato. Questo riconduce un'istanza a istanze formate da coppie di prefissi, che sono (m+1)(n+1)=Θ(mn)(m + 1)(n + 1) = \Theta(mn): uno spazio quadratico, non esponenziale come le sottosequenze. La generica sottoistanza è (Xi,Yj)(X_i, Y_j) con 0≤i≤m0 \le i \le m e 0≤j≤n0 \le j \le n, e si vuole LCS(Xm,Yn)=LCS(X,Y)\text{LCS}(X_m, Y_n) = \text{LCS}(X, Y).

Teorema (sottostruttura della LCS). Siano XiX_i, YjY_j prefissi di XX e YY e sia Z=⟨z1,…,zk⟩Z = \langle z_1, \dots, z_k \rangle una LCS di XiX_i e YjY_j.

  1. Se i=0i = 0 o j=0j = 0, allora Z=εZ = \varepsilon.
  2. Se i>0i > 0 e j>0j > 0:
    • (a) se xi=yjx_i = y_j, allora zk=xi=yjz_k = x_i = y_j e Zk−1=⟨z1,…,zk−1⟩Z_{k-1} = \langle z_1, \dots, z_{k-1} \rangle è una LCS di Xi−1X_{i-1} e Yj−1Y_{j-1};
    • (b) se xi≠yjx_i \ne y_j, allora ZZ è una LCS di Xi−1,YjX_{i-1}, Y_j oppure di Xi,Yj−1X_i, Y_{j-1}, e quindi è la più lunga tra le due.

Dimostrazione. Il punto 1 è banale: un prefisso vuoto non ha sottosequenze non vuote.

Caso (a), xi=yjx_i = y_j. Siano i1<⋯<ik≤ii_1 < \dots < i_k \le i e j1<⋯<jk≤jj_1 < \dots < j_k \le j gli indici che realizzano ZZ in XiX_i e in YjY_j.

  • zk=xiz_k = x_i. Per assurdo sia zk≠xi=yjz_k \ne x_i = y_j (oppure k=0k = 0). Allora ik≠ii_k \ne i e jk≠jj_k \ne j (altrimenti zkz_k sarebbe xix_i o yjy_j), cioè ik<ii_k < i e jk<jj_k < j. Ma allora ⟨Z,xi⟩\langle Z, x_i \rangle è una sottosequenza comune, realizzata dagli indici i1<⋯<ik<ii_1 < \dots < i_k < i in XiX_i e j1<⋯<jk<jj_1 < \dots < j_k < j in YjY_j, ed è più lunga di ZZ: contraddizione con la massimalità di ZZ.
  • Zk−1Z_{k-1} è una LCS di Xi−1X_{i-1} e Yj−1Y_{j-1}. Gli indici i1<⋯<ik−1<ik≤ii_1 < \dots < i_{k-1} < i_k \le i sono tutti ≤i−1\le i - 1, e lo stesso vale in YY: quindi Zk−1Z_{k-1} è una sottosequenza comune di Xi−1X_{i-1} e Yj−1Y_{j-1}. Se esistesse una sottosequenza comune WW di Xi−1X_{i-1} e Yj−1Y_{j-1} più lunga di Zk−1Z_{k-1}, allora ⟨W,xi⟩\langle W, x_i \rangle sarebbe una sottosequenza comune di XiX_i e YjY_j più lunga di ZZ (è la tecnica del cut-and-paste: si taglia Zk−1Z_{k-1} e si incolla WW), contro la massimalità di ZZ.

Caso (b), xi≠yjx_i \ne y_j. Il simbolo zkz_k non può essere uguale sia a xix_i sia a yjy_j. Se zk≠xiz_k \ne x_i (o k=0k = 0), l'indice ii non è usato per realizzare ZZ in XiX_i, quindi ZZ è una sottosequenza comune di Xi−1X_{i-1} e YjY_j. Se zk≠yjz_k \ne y_j, per lo stesso motivo ZZ è sottosequenza comune di XiX_i e Yj−1Y_{j-1}. Almeno una delle due vale. Nel primo caso ZZ deve essere una LCS di (Xi−1,Yj)(X_{i-1}, Y_j) (una sottosequenza comune più lunga, che è anche sottosequenza comune di XiX_i e YjY_j, batterebbe ZZ). Nel secondo, di (Xi,Yj−1)(X_i, Y_{j-1}). Infine le LCS di (Xi−1,Yj)(X_{i-1}, Y_j) e di (Xi,Yj−1)(X_i, Y_{j-1}) sono entrambe sottosequenze comuni di XiX_i e YjY_j, quindi nessuna è più lunga di ZZ: ZZ è la più lunga delle due. □\square

Ricorrenza sui costi e informazione addizionale

Sia ℓ(i,j)=∣LCS(Xi,Yj)∣\ell(i, j) = |\text{LCS}(X_i, Y_j)|. Dal teorema, per 0≤i≤m0 \le i \le m e 0≤j≤n0 \le j \le n:

ℓ(i,j)={0i=0 ∨ j=01+ℓ(i−1,j−1)i,j>0 ∧ xi=yjmax⁡{ℓ(i−1,j), ℓ(i,j−1)}i,j>0 ∧ xi≠yj\ell(i, j) = \begin{cases} 0 & i = 0 \ \lor \ j = 0 \\ 1 + \ell(i-1, j-1) & i, j > 0 \ \land \ x_i = y_j \\ \max\{\ell(i-1, j),\ \ell(i, j-1)\} & i, j > 0 \ \land \ x_i \ne y_j \end{cases}

Informazione addizionale. Per i casi base la soluzione è sempre ε\varepsilon: non si salva nulla. Per gli altri ci sono solo tre modi di costruire Z=LCS(Xi,Yj)Z = \text{LCS}(X_i, Y_j) da sottoistanze: ⟨LCS(Xi−1,Yj−1),xi⟩\langle \text{LCS}(X_{i-1}, Y_{j-1}), x_i \rangle, oppure LCS(Xi,Yj−1)\text{LCS}(X_i, Y_{j-1}), oppure LCS(Xi−1,Yj)\text{LCS}(X_{i-1}, Y_j). Basta ricordare quale: una freccia

b(i,j)={↖xi=yj←xi≠yj ∧ Z=LCS(Xi,Yj−1)↑xi≠yj ∧ Z=LCS(Xi−1,Yj)b(i, j) = \begin{cases} \nwarrow & x_i = y_j \\ \leftarrow & x_i \ne y_j \ \land \ Z = \text{LCS}(X_i, Y_{j-1}) \\ \uparrow & x_i \ne y_j \ \land \ Z = \text{LCS}(X_{i-1}, Y_j) \end{cases}

Il codice bottom-up

Si usano due tabelle ℓ[0..m,0..n]\ell[0..m, 0..n] e b[0..m,0..n]b[0..m, 0..n]. La cella (i,j)(i, j) dipende solo da (i−1,j−1)(i-1, j-1), (i−1,j)(i-1, j) e (i,j−1)(i, j-1). Per un ordine coerente basta che al momento di calcolare (i,j)(i, j) queste tre siano già pronte (si inizializzano riga 00 e colonna 00, che contengono i casi base). Funzionano sia la scansione per righe (riga 11 da sinistra a destra, poi riga 22, ...) sia quella per colonne: in entrambe, le tre celle da cui si dipende precedono (i,j)(i, j). Per righe è preferibile in linguaggi che memorizzano le matrici per riga, per l'uso della cache.

LCS(X, Y)
  m <- |X|; n <- |Y|
  for i <- 0 to m do l[i, 0] <- 0
  for j <- 1 to n do l[0, j] <- 0
  for i <- 1 to m do
    for j <- 1 to n do
      if x_i = y_j then
        l[i, j] <- 1 + l[i-1, j-1]
        b[i, j] <- '↖'
      else if l[i-1, j] >= l[i, j-1] then
        l[i, j] <- l[i-1, j]
        b[i, j] <- '↑'
      else
        l[i, j] <- l[i, j-1]
        b[i, j] <- '←'
  return (l[m, n], b)

Costo. Contando solo i confronti tra simboli (modello di costo usuale per le stringhe), ce n'è uno per iterazione:

T(m,n)=∑i=1m∑j=1n1=m⋅n=Θ(mn),T(m, n) = \sum_{i=1}^{m} \sum_{j=1}^{n} 1 = m \cdot n = \Theta(mn),

quadratico (con m=nm = n). L'algoritmo restituisce la lunghezza; per ricostruire la LCS bisogna restituire anche tutta la tabella bb.

Ricostruzione. Si parte da (m,n)(m, n) e si segue la freccia: a ogni passo almeno una delle due coordinate scende, e i+ji + j diminuisce di almeno 11. Quindi le chiamate sono O(m+n)O(m + n).

PRINT_LCS(X, b, i, j)               { prima chiamata: PRINT_LCS(X, b, m, n) }
  if i = 0 or j = 0 then return
  if b[i, j] = '↖' then
    PRINT_LCS(X, b, i-1, j-1)       { prima la LCS dei prefissi... }
    print(x_i)                      { ...poi il simbolo comune }
  else if b[i, j] = '←' then PRINT_LCS(X, b, i, j-1)
  else PRINT_LCS(X, b, i-1, j)

Esempio eseguito

X=⟨A,B,C,B,B,D⟩X = \langle A, B, C, B, B, D \rangle e Y=⟨A,D,C,C,B,D⟩Y = \langle A, D, C, C, B, D \rangle. In grassetto il percorso della ricostruzione (partendo da (6,6)(6, 6)). Ogni cella ha il valore ℓ(i,j)\ell(i, j) e la freccia b(i,j)b(i, j).

ε\varepsilon A D C C B D
ε\varepsilon 0 0 0 0 0 0 0
A 0 1↖ 1← 1← 1← 1← 1←
B 0 1↑ 1↑ 1↑ 1↑ 2↖ 2←
C 0 1↑ 1↑ 2↖ 2↖ 2↑ 2↑
B 0 1↑ 1↑ 2↑ 2↑ 3↖ 3←
B 0 1↑ 1↑ 2↑ 2↑ 3↖ 3↑
D 0 1↑ 2↖ 2↑ 2↑ 3↑ 4↖

Alcune celle a mano:

  • (1,1)(1, 1): x1=A=y1x_1 = A = y_1, quindi ℓ=1+ℓ(0,0)=1\ell = 1 + \ell(0, 0) = 1, freccia ↖\nwarrow.
  • (2,5)(2, 5): x2=B=y5x_2 = B = y_5, quindi ℓ=1+ℓ(1,4)=1+1=2\ell = 1 + \ell(1, 4) = 1 + 1 = 2, freccia ↖\nwarrow.
  • (3,4)(3, 4): x3=C=y4x_3 = C = y_4, quindi ℓ=1+ℓ(2,3)=1+1=2\ell = 1 + \ell(2, 3) = 1 + 1 = 2 (la cella (3,3)(3, 3) ha lo stesso valore, perché anche y3=Cy_3 = C).
  • (4,4)(4, 4): x4=B≠C=y4x_4 = B \ne C = y_4. Si confrontano ℓ(3,4)=2\ell(3, 4) = 2 e ℓ(4,3)=2\ell(4, 3) = 2: sono uguali, e per il ≥\ge del codice vince ↑\uparrow.

Ricostruzione: (6,6)(6,6) ↖\nwarrow stampa DD (dopo), poi (5,5)(5,5) ↖\nwarrow BB, poi (4,4)(4,4) ↑\uparrow sale a (3,4)(3,4) ↖\nwarrow CC, poi (2,3)(2,3) ↑\uparrow e (1,3)(1,3) ←\leftarrow, (1,2)(1,2) ←\leftarrow, (1,1)(1,1) ↖\nwarrow AA. Letti dall'alto in basso i simboli stampati sono A,C,B,DA, C, B, D: ℓ(6,6)=4\ell(6, 6) = 4 e la LCS è ⟨A,C,B,D⟩\langle A, C, B, D \rangle. Tutta la tabella è stata confrontata con una forza bruta (enumerazione di tutte le sottosequenze) su 400400 coppie casuali di stringhe corte.

Una LCS non è unica e il risultato dipende dal criterio nei casi di parità. Per X=⟨A,B,C⟩X = \langle A, B, C \rangle e Y=⟨B,A,C⟩Y = \langle B, A, C \rangle nella cella (2,2)(2, 2) si ha ℓ(1,2)=ℓ(2,1)=1\ell(1, 2) = \ell(2, 1) = 1: con il ≥\ge del codice si sceglie ↑\uparrow e la ricostruzione dà ⟨A,C⟩\langle A, C \rangle; scegliendo ←\leftarrow nella parità si otterrebbe ⟨B,C⟩\langle B, C \rangle.

python
def lcs(X, Y):
    m, n = len(X), len(Y)
    l = [[0] * (n + 1) for _ in range(m + 1)]
    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if X[i - 1] == Y[j - 1]:
                l[i][j] = 1 + l[i - 1][j - 1]
            else:
                l[i][j] = max(l[i - 1][j], l[i][j - 1])
    i, j, z = m, n, []
    while i > 0 and j > 0:                  # ricostruzione senza la tabella b
        if X[i - 1] == Y[j - 1]:
            z.append(X[i - 1]); i -= 1; j -= 1
        elif l[i - 1][j] >= l[i][j - 1]:
            i -= 1
        else:
            j -= 1
    return l[m][n], "".join(reversed(z))

assert lcs("ABCBBD", "ADCCBD") == (4, "ACBD")

(La tabella bb si può anche non memorizzare: la freccia si ricava a posteriori confrontando le celle vicine, come fa il ciclo qui sopra.)

Il divide and conquer sulla stessa ricorrenza

Un algoritmo ricorsivo sulla stessa sottostruttura, al caso peggiore (per esempio XX e YY senza simboli in comune), ha un nodo interno con lavoro unitario per ogni coppia con m,n>0m, n > 0 e due chiamate:

T(m,n)={0m=0 ∨ n=0T(m−1,n)+T(m,n−1)+1altrimentiT(m, n) = \begin{cases} 0 & m = 0 \ \lor \ n = 0 \\ T(m-1, n) + T(m, n-1) + 1 & \text{altrimenti} \end{cases}

Minorazione (srotolamento). Con m=nm = n: T(n,n)=T(n−1,n)+T(n,n−1)+1≥2T(n−1,n−1)+1T(n, n) = T(n-1, n) + T(n, n-1) + 1 \ge 2T(n-1, n-1) + 1 (perché TT è non decrescente in ciascun argomento). Srotolando ii volte, T(n,n)≥2iT(n−i,n−i)+2i−1T(n, n) \ge 2^i T(n-i, n-i) + 2^i - 1, e con i=ni = n si ottiene T(n,n)≥2n−1T(n, n) \ge 2^n - 1.

Valore esatto: T(m,n)=(m+nm)−1T(m, n) = \binom{m+n}{m} - 1. Per induzione sulla somma m+nm + n: se m=0m = 0 o n=0n = 0, (m+nm)−1=0\binom{m+n}{m} - 1 = 0. Altrimenti, con la regola di Pascal,

T(m,n)=[(m+n−1m−1)−1]+[(m+n−1m)−1]+1=(m+nm)−1.T(m, n) = \Big[\tbinom{m+n-1}{m-1} - 1\Big] + \Big[\tbinom{m+n-1}{m} - 1\Big] + 1 = \tbinom{m+n}{m} - 1.

Per m=n=20m = n = 20 sono (4020)−1=137 846 528 819\binom{40}{20} - 1 = 137\,846\,528\,819 nodi, contro le 400400 celle della tabella. La causa sono le sottoistanze ripetute: la stessa coppia di prefissi (i,j)(i, j) viene raggiunta da molti cammini.

Risparmiare spazio

Se interessa solo la lunghezza della LCS non serve la tabella bb, e per ℓ\ell bastano due righe: la riga ii dipende solo dalla riga i−1i - 1 e dalla riga ii stessa. Scambiando i ruoli in modo che ∣Y∣≤∣X∣|Y| \le |X| si usa spazio Θ(min⁡{m,n})\Theta(\min\{m, n\}) invece di Θ(mn)\Theta(mn).

LCS_LENGTH(X, Y)                    { |Y| <= |X| }
  m <- |X|; n <- |Y|
  for j <- 0 to n do prev[j] <- 0
  for i <- 1 to m do
    curr[0] <- 0
    for j <- 1 to n do
      if x_i = y_j then curr[j] <- 1 + prev[j-1]
      else curr[j] <- max(prev[j], curr[j-1])
    prev <- curr
  return prev[n]

(Con la ricostruzione bisogna conservare tutta la tabella, o ricalcolare a pezzi.) Il confronto con la versione a tabella intera è stato verificato su stringhe casuali.

L'algoritmo memoizzato

Si mantiene la ricorsione e si aggiunge la tabella globale. La sentinella è −1-1: è corretta perché ℓ≥0\ell \ge 0. Le celle dei casi base valgono 00.

INIT_LCS(X, Y)
  m <- |X|; n <- |Y|
  if m = 0 or n = 0 then return 0
  for i <- 0 to m do l[i, 0] <- 0
  for j <- 0 to n do l[0, j] <- 0
  for i <- 1 to m do
    for j <- 1 to n do l[i, j] <- -1
  return MEMO_LCS(X, Y, m, n)

MEMO_LCS(X, Y, i, j)
  if l[i, j] = -1 then
    if x_i = y_j then
      l[i, j] <- 1 + MEMO_LCS(X, Y, i-1, j-1)
    else
      a <- MEMO_LCS(X, Y, i-1, j)
      c <- MEMO_LCS(X, Y, i, j-1)
      l[i, j] <- max(a, c)
  return l[i, j]
  • Correttezza: dalla sottostruttura e dalla scelta della sentinella.
  • Costo al caso peggiore: si analizza l'albero delle chiamate. Per 1≤i≤m1 \le i \le m e 1≤j≤n1 \le j \le n, la chiamata (i,j)(i, j) è un nodo interno al più una volta, a costo unitario (un confronto); le altre occorrenze restituiscono il valore in tabella. Quindi T(m,n)≤m⋅nT(m, n) \le m \cdot n, uguale al bottom-up.
  • Non sempre tutte le celle. Se le due stringhe sono simili (hanno lunghi tratti in comune), la discesa top-down tocca poche celle. Esempio: X=⟨A,A,B,B,C⟩X = \langle A, A, B, B, C \rangle e Y=⟨B,B,C⟩Y = \langle B, B, C \rangle. Le chiamate sono (5,3)→(4,2)→(3,1)→(2,0)(5,3) \to (4,2) \to (3,1) \to (2,0): ultimi simboli uguali (C=CC = C, poi B=BB = B, poi B=BB = B), si scende in diagonale fino al caso base (2,0)(2, 0). Solo 33 nodi interni (e un caso base) contro le 5×3=155 \times 3 = 15 celle del bottom-up. Nelle applicazioni di genomica, dove le sequenze sono quasi uguali, il vantaggio è notevole. Controllato anche con Python: la discesa memoizzata tocca esattamente (5,3),(4,2),(3,1)(5,3), (4,2), (3,1).
  • Per la coppia ABCBBDABCBBD e ADCCBDADCCBD dell'esempio eseguito il memoizzato calcola 1616 celle su 3636 (stringhe poco simili: il vantaggio è piccolo).

Domande d'esame

  1. (Risposta numerica.) Siano X=⟨a,f,r,a⟩X = \langle a, f, r, a \rangle e Y=⟨a,f,a⟩Y = \langle a, f, a \rangle e sia l(i,j)l(i, j) la lunghezza della LCS di XiX_i e YjY_j. Calcolare l(2,2)+l(4,3)l(2, 2) + l(4, 3). È proprio uno dei quesiti della prima parte delle simulazioni: svolto in Esercizio - domande di prima parte su programmazione dinamica (simulazioni d'esame). Risposta: 2+3=52 + 3 = 5.
  2. (Risposta numerica.) Per m=n=10m = n = 10, quanti confronti tra simboli esegue LCS? E quanti nodi interni al massimo ha l'albero del D&C puro? Traccia: m⋅n=100m \cdot n = 100 confronti; (2010)−1=184 755\binom{20}{10} - 1 = 184\,755 nodi interni.
  3. (Dimostrazione.) Se xi=yjx_i = y_j perché LCS(Xi,Yj)\text{LCS}(X_i, Y_j) termina con questo simbolo? Traccia: altrimenti si potrebbe aggiungere il simbolo comune alla fine, ottenendo una sottosequenza comune più lunga (indici ik<ii_k < i e jk<jj_k < j, perché zk≠xi,yjz_k \ne x_i, y_j).

Versione ripasso

  • Definizioni. Prefisso XiX_i (0≤i≤m0 \le i \le m), suffisso XiX^i, sottostringa Xi..jX_{i..j} (contigua), sottosequenza (indici crescenti, può essere realizzata da più successioni). Sottostringhe 1+m(m+1)/2=Θ(m2)1 + m(m+1)/2 = \Theta(m^2); sottosequenze di una stringa a simboli distinti ∑k(mk)=2m\sum_k \binom{m}{k} = 2^m (Binomio di NewtonLa formula per sviluppare (a+b)^n con i coefficienti binomiali.Binomio di Newton →).
  • LCS: sottosequenza comune di XX e YY di lunghezza massima, misura di somiglianza (DNA, correzione dei testi). Esempio: ⟨A,B,C,B,B,D⟩\langle A,B,C,B,B,D \rangle e ⟨A,D,C,C,B,D⟩\langle A,D,C,C,B,D \rangle hanno LCS ⟨A,C,B,D⟩\langle A,C,B,D \rangle. Può non essere unica (⟨A,C⟩\langle A,C \rangle e ⟨B,C⟩\langle B,C \rangle per ABCABC e BACBAC).
  • Teorema (prefissi Xi,YjX_i, Y_j, ZZ una LCS). (1) i=0i = 0 o j=0j = 0: Z=εZ = \varepsilon. (2a) xi=yjx_i = y_j: zk=xiz_k = x_i e Zk−1Z_{k-1} è LCS di (Xi−1,Yj−1)(X_{i-1}, Y_{j-1}). (2b) xi≠yjx_i \ne y_j: ZZ è la più lunga tra LCS(Xi−1,Yj)(X_{i-1}, Y_j) e LCS(Xi,Yj−1)(X_i, Y_{j-1}). Prova per assurdo e cut-and-paste: se zk≠xi=yjz_k \ne x_i = y_j si aggiungerebbe xix_i; se Zk−1Z_{k-1} non fosse ottima la si sostituirebbe con una più lunga e si aggiungerebbe xix_i.
  • Ricorrenza: ℓ(i,j)=0\ell(i,j) = 0 se i=0∨j=0i = 0 \lor j = 0; 1+ℓ(i−1,j−1)1 + \ell(i-1,j-1) se xi=yjx_i = y_j; max⁡{ℓ(i−1,j),ℓ(i,j−1)}\max\{\ell(i-1,j), \ell(i,j-1)\} altrimenti. Freccia b(i,j)∈{↖,←,↑}b(i,j) \in \{\nwarrow, \leftarrow, \uparrow\} (informazione addizionale).
  • Bottom-up: tabelle ℓ\ell, bb di (m+1)(n+1)(m+1)(n+1) celle, riga 00 e colonna 00 a 00, poi per righe (o per colonne), perché (i,j)(i,j) dipende da (i−1,j−1),(i−1,j),(i,j−1)(i-1,j-1), (i-1,j), (i,j-1). Costo T(m,n)=mnT(m,n) = mn confronti. PRINT_LCS segue le frecce, O(m+n)O(m+n) chiamate; stampa il simbolo dopo la chiamata ricorsiva su ↖\nwarrow.
  • Codice.
LCS(X, Y)
  for i <- 0 to m do l[i, 0] <- 0;  for j <- 1 to n do l[0, j] <- 0
  for i <- 1 to m do for j <- 1 to n do
    if x_i = y_j then l[i,j] <- 1 + l[i-1,j-1]; b[i,j] <- '↖'
    else if l[i-1,j] >= l[i,j-1] then l[i,j] <- l[i-1,j]; b[i,j] <- '↑'
    else l[i,j] <- l[i,j-1]; b[i,j] <- '←'
  return (l[m,n], b)

PRINT_LCS(X, b, i, j)
  if i = 0 or j = 0 then return
  if b[i,j] = '↖' then PRINT_LCS(X,b,i-1,j-1); print(x_i)
  else if b[i,j] = '←' then PRINT_LCS(X,b,i,j-1)
  else PRINT_LCS(X,b,i-1,j)
  • Esempio: X=ABCBBDX = ABCBBD, Y=ADCCBDY = ADCCBD, ℓ(6,6)=4\ell(6,6) = 4. Il percorso è in grassetto.
ε\varepsilon A D C C B D
ε\varepsilon 0 0 0 0 0 0 0
A 0 1↖ 1← 1← 1← 1← 1←
B 0 1↑ 1↑ 1↑ 1↑ 2↖ 2←
C 0 1↑ 1↑ 2↖ 2↖ 2↑ 2↑
B 0 1↑ 1↑ 2↑ 2↑ 3↖ 3←
B 0 1↑ 1↑ 2↑ 2↑ 3↖ 3↑
D 0 1↑ 2↖ 2↑ 2↑ 3↑ 4↖

Simboli stampati dalla ricostruzione: A,C,B,DA, C, B, D.

  • D&C puro: T(m,n)=T(m−1,n)+T(m,n−1)+1T(m,n) = T(m-1,n) + T(m,n-1) + 1, T(n,n)≥2n−1T(n,n) \ge 2^n - 1 per srotolamento; esatto (m+nm)−1\binom{m+n}{m} - 1 (Pascal): per m=n=20m = n = 20 sono 137 846 528 819137\,846\,528\,819 nodi contro 400400 celle.
  • Spazio: solo la lunghezza, due righe: Θ(min⁡{m,n})\Theta(\min\{m,n\}) (con ∣Y∣≤∣X∣|Y| \le |X|); la ricostruzione vuole tutta la tabella.
  • Memoizzazione: sentinella −1-1 (le lunghezze sono ≥0\ge 0); nodi interni al più mnmn, uno per cella; su stringhe simili molti di meno. X=AABBCX = AABBC, Y=BBCY = BBC: solo (5,3),(4,2),(3,1)(5,3), (4,2), (3,1) contro 1515 celle.
MEMO_LCS(X, Y, i, j)                { l[0,*] = l[*,0] = 0, le altre celle = -1 }
  if l[i,j] = -1 then
    if x_i = y_j then l[i,j] <- 1 + MEMO_LCS(X,Y,i-1,j-1)
    else a <- MEMO_LCS(X,Y,i-1,j); c <- MEMO_LCS(X,Y,i,j-1); l[i,j] <- max(a,c)
  return l[i,j]
  • Due righe (solo lunghezza): curr[j] <- 1 + prev[j-1] se xi=yjx_i = y_j, altrimenti max(prev[j], curr[j-1]), poi prev <- curr; spazio Θ(min⁡{m,n})\Theta(\min\{m,n\}).
  • Domande tipiche: l(2,2)+l(4,3)l(2,2) + l(4,3) per X=afraX = afra, Y=afaY = afa vale 2+3=52 + 3 = 5 (Esercizio - domande di prima parte su programmazione dinamica (simulazioni d'esame)); confronti di LCS per m=n=10m = n = 10: 100100; nodi interni del D&C: (2010)−1=184 755\binom{20}{10} - 1 = 184\,755.
  • Errori tipici: confondere sottostringa e sottosequenza; inizializzare riga e colonna 00 con altro che 00; dimenticare che nel caso xi≠yjx_i \ne y_j non si somma 11; usare la tabella con una sola riga e poi voler ricostruire; analizzare il memoizzato con la ricorrenza del D&C.

Esercizi su questo argomento

Teoria collegata