Salta al contenuto
Note per Studenti Allineamento di stringhe e sottosequenza palindroma più lunga

Allineamento di stringhe e sottosequenza palindroma più lunga

In questa pagina 4

Due esercitazioni sulla programmazione dinamica (Programmazione dinamica - Fibonacci, memoizzazione e schema del paradigmala programmazione dinamica (DP) evolve il divide and conquer: invece di risolvere di nuovo le stesse sottoistanze, le salva in una tabella. Fibonacci ricorsivo costa F_n - 1 addizioni (esponenziale), quello con tabella n - 1. Tre condizioni: sottostruttura, sottoistanze ripetute, spazio dei sottoproblemi ristretto. Bottom-up o memoizzazione (ricorsione + tabella globale). Per i problemi di ottimizzazione lo schema è: sottostruttura ottima, ricorrenza sui costi, informazione addizionale, calcolo della tabella, ricostruzione.Programmazione dinamica - Fibonacci, memoizzazione e schema del paradigma →) che riusano la struttura della Longest Common Subsequence - ricorrenza, tabella e ricostruzionela LCS di X (m caratteri) e Y (n caratteri) è la sottosequenza comune più lunga. Sottoistanze = coppie di prefissi (Theta(mn)). Ricorrenza: l(i,j) = 0 se i = 0 o j = 0; 1 + l(i-1,j-1) se x_i = y_j; max{l(i-1,j), l(i,j-1)} altrimenti. Bottom-up con tabelle l e b (freccia): mn confronti; ricostruzione in O(m+n). Il D&C sulla stessa ricorrenza costa C(m+n,m) - 1 nodi interni nel caso peggiore (esponenziale). Memoizzazione: stessa complessità nel caso peggiore, ma molto meno lavoro su stringhe simili. Spazio ridotto a Theta(min{m,n}) se serve solo la lunghezza.Longest Common Subsequence - ricorrenza, tabella e ricostruzione →: la prima con lo stesso spazio dei sottoproblemi (coppie di prefissi), la seconda con uno spazio diverso (sottostringhe).

Allineamento di stringhe di DNA

L'alfabeto del DNA è ΣDNA={A,C,G,T}\Sigma_{DNA} = \{A, C, G, T\} (i quattro nucleotidi); per l'allineamento si estende con un carattere di spazio, ΣDNAE={A,C,G,T,−}\Sigma^E_{DNA} = \{A, C, G, T, -\}. Si risolve un problema di ottimizzazione molto simile alla LCS e il costo ottimo è una metrica di similarità tra le due stringhe.

Definizione del problema

Definizione (allineamento). Date X=⟨x1,…,xm⟩X = \langle x_1, \dots, x_m \rangle e Y=⟨y1,…,yn⟩Y = \langle y_1, \dots, y_n \rangle su ΣDNA\Sigma_{DNA}, un allineamento di lunghezza kk è una coppia di stringhe ZX=⟨z1X,…,zkX⟩Z^X = \langle z^X_1, \dots, z^X_k \rangle, ZY=⟨z1Y,…,zkY⟩Z^Y = \langle z^Y_1, \dots, z^Y_k \rangle su ΣDNAE\Sigma^E_{DNA}, con k≥max⁡{m,n}k \ge \max\{m, n\}, tali che:

  1. XX è sottosequenza di ZXZ^X e YY è sottosequenza di ZYZ^Y (esistono 1≤i1<⋯<im≤k1 \le i_1 < \dots < i_m \le k con X=⟨zi1X,…,zimX⟩X = \langle z^X_{i_1}, \dots, z^X_{i_m} \rangle, e analogamente per YY);
  2. le posizioni che non realizzano caratteri di XX (rispettivamente YY) contengono solo −-;
  3. in ogni posizione rr non ci sono due −-: (zrX∈ΣDNA)∨(zrY∈ΣDNA)(z^X_r \in \Sigma_{DNA}) \lor (z^Y_r \in \Sigma_{DNA}).

In pratica ogni posizione (colonna) dell'allineamento è di uno di tre tipi: carattere di XX sopra carattere di YY; carattere di XX sopra uno spazio; spazio sopra carattere di YY.

Esempio. X=⟨A,G,T,T⟩X = \langle A, G, T, T \rangle, Y=⟨A,T,T⟩Y = \langle A, T, T \rangle. Due allineamenti:

Z^X = A G T T          Z^X = A G T T
Z^Y = A - T T          Z^Y = A T T -

Nel secondo, la posizione 22 realizza insieme x2=Gx_2 = G e y2=Ty_2 = T.

Costo. Una matrice di score S∈Z4×4S \in \mathbb{Z}^{4 \times 4} dà un intero sUVs_{UV} per ogni coppia ordinata di nucleotidi (U,V)(U, V) (di solito simmetrica, positiva sulla diagonale per i match, negativa altrove); la penalità di gap g∈Zg \in \mathbb{Z} (di solito negativa) è il peso di una posizione con uno spazio. Il peso di una posizione e il costo dell'allineamento sono

p(zrX,zrY)={szrXzrYzrX≠− ∧ zrY≠−galtrimentic(ZX,ZY)=∑r=1kp(zrX,zrY).p(z^X_r, z^Y_r) = \begin{cases} s_{z^X_r z^Y_r} & z^X_r \ne - \ \land \ z^Y_r \ne - \\ g & \text{altrimenti} \end{cases} \qquad c(Z^X, Z^Y) = \sum_{r=1}^{k} p(z^X_r, z^Y_r).

Problema dell'allineamento ottimo. Date X,Y∈ΣDNA∗X, Y \in \Sigma_{DNA}^*, SS e gg, determinare il costo massimo di un allineamento delle due stringhe.

Esempio (seguito). sUU=1s_{UU} = 1, sUV=−3s_{UV} = -3 per U≠VU \ne V, g=−2g = -2. Il primo allineamento costa 1+(−2)+1+1=11 + (-2) + 1 + 1 = 1; il secondo 1+(−3)+1+(−2)=−31 + (-3) + 1 + (-2) = -3. Il primo è ottimo (controllato enumerando tutti i 129129 allineamenti ammissibili delle due stringhe: nessuno supera 11).

Sottostruttura ottima

Appena si sceglie come finiscono i due allineamenti, il resto è un allineamento di prefissi. Lo spazio dei sottoproblemi è quindi quello della LCS: {(Xi,Yj):0≤i≤m, 0≤j≤n}\{(X_i, Y_j) : 0 \le i \le m,\ 0 \le j \le n\}.

Casi base. Se i=0i = 0 il prefisso X0X_0 è vuoto: per la condizione 3 non possono esserci due spazi nella stessa posizione, quindi l'unico allineamento ammissibile di (ε,Yj)(\varepsilon, Y_j) è ZX0=⟨−,…,−⟩Z^{X_0} = \langle -, \dots, - \rangle (jj spazi), ZYj=YjZ^{Y_j} = Y_j, di costo j⋅gj \cdot g. Simmetricamente per j=0j = 0 il costo è i⋅gi \cdot g; per i=j=0i = j = 0 la coppia di stringhe vuote ha costo 00.

Casi non di base (i>0i > 0, j>0j > 0). Sia (ZXi,ZYj)(Z^{X_i}, Z^{Y_j}) un allineamento ottimo di (Xi,Yj)(X_i, Y_j), di lunghezza kk. L'ultima posizione può essere solo di tre tipi:

  1. zkXi=xiz^{X_i}_k = x_i e zkYj=yjz^{Y_j}_k = y_j (l'ultima posizione realizza gli ultimi caratteri di entrambi). Allora le prime k−1k - 1 posizioni sono un allineamento ottimo di (Xi−1,Yj−1)(X_{i-1}, Y_{j-1}) — altrimenti, con il cut-and-paste, se ne potrebbe sostituire uno migliore e ottenere un allineamento di (Xi,Yj)(X_i, Y_j) di costo superiore — e c(ZXi,ZYj)=c(ZXi−1,ZYj−1)+sxiyjc(Z^{X_i}, Z^{Y_j}) = c(Z^{X_{i-1}}, Z^{Y_{j-1}}) + s_{x_i y_j}.
  2. zkXi=xiz^{X_i}_k = x_i e zkYj=−z^{Y_j}_k = - (solo l'ultimo carattere di XiX_i). Le prime k−1k - 1 posizioni sono un allineamento ottimo di (Xi−1,Yj)(X_{i-1}, Y_j) e c=c(ZXi−1,ZYj)+gc = c(Z^{X_{i-1}}, Z^{Y_j}) + g.
  3. zkXi=−z^{X_i}_k = - e zkYj=yjz^{Y_j}_k = y_j, simmetrico: allineamento ottimo di (Xi,Yj−1)(X_i, Y_{j-1}) e c=c(ZXi,ZYj−1)+gc = c(Z^{X_i}, Z^{Y_{j-1}}) + g.

I tre allineamenti ottenuti dai tre punti a partire dagli ottimi dei sottoproblemi sono tutti ammissibili, quindi vince quello di costo massimo.

Ricorrenza sui costi

Con C(i,j)C(i, j) il costo dell'allineamento ottimo di (Xi,Yj)(X_i, Y_j), per 0≤i≤m0 \le i \le m e 0≤j≤n0 \le j \le n:

C(i,j)={j⋅gi=0i⋅gj=0max⁡{ C(i−1,j−1)+sxiyj,  C(i−1,j)+g,  C(i,j−1)+g }i>0 ∧ j>0C(i, j) = \begin{cases} j \cdot g & i = 0 \\ i \cdot g & j = 0 \\ \max\{\, C(i-1, j-1) + s_{x_i y_j},\ \ C(i-1, j) + g,\ \ C(i, j-1) + g \,\} & i > 0 \ \land \ j > 0 \end{cases}

Per ricostruire l'allineamento si salva, come nella LCS, una freccia: ↖\nwarrow, ↑\uparrow o ←\leftarrow secondo quale dei tre termini realizza il massimo.

Codice bottom-up

Il grafico delle dipendenze è identico a quello della LCS (la cella (i,j)(i, j) dipende da (i−1,j−1)(i-1, j-1), (i−1,j)(i-1, j) e (i,j−1)(i, j-1)), quindi si riempie per righe.

ALIGN(X, Y, S, g)
  m <- |X|; n <- |Y|
  for i <- 0 to m do C[i, 0] <- i * g; b[i, 0] <- '↑'     { b[0, 0] non viene letta }
  for j <- 1 to n do C[0, j] <- j * g; b[0, j] <- '←'
  for i <- 1 to m do
    for j <- 1 to n do
      d <- C[i-1, j-1] + S[x_i, y_j]
      u <- C[i-1, j] + g
      l <- C[i, j-1] + g
      C[i, j] <- max(d, u, l)
      if C[i, j] = d then b[i, j] <- '↖'
      else if C[i, j] = u then b[i, j] <- '↑'
      else b[i, j] <- '←'
  return (C[m, n], b)

PRINT_ALIGN(X, Y, b, i, j)       { stampa le colonne dell'allineamento da sinistra a destra }
  if i = 0 and j = 0 then return
  if b[i, j] = '↖' then
    PRINT_ALIGN(X, Y, b, i-1, j-1);  stampa la colonna (x_i sopra y_j)
  else if b[i, j] = '↑' then
    PRINT_ALIGN(X, Y, b, i-1, j);    stampa la colonna (x_i sopra '-')
  else
    PRINT_ALIGN(X, Y, b, i, j-1);    stampa la colonna ('-' sopra y_j)

Il costo è Θ(mn)\Theta(mn) (confronti e addizioni tra interi) e la ricostruzione ha O(m+n)O(m + n) chiamate.

Esempio eseguito

X=ACCGTX = ACCGT, Y=AGCTY = AGCT, sUU=1s_{UU} = 1, sUV=−3s_{UV} = -3 per U≠VU \ne V, g=−2g = -2. In grassetto il percorso della ricostruzione.

ε\varepsilon A G C T
ε\varepsilon 0 −2← −4← −6← −8←
A −2↑ 1↖ −1← −3← −5←
C −4↑ −1↑ −2↖ 0↖ −2←
C −6↑ −3↑ −4↖ −1↖ −3↖
G −8↑ −5↑ −2↖ −3↑ −4↖
T −10↑ −7↑ −4↑ −5↖ −2↖

Celle a mano:

  • Riga 00 e colonna 00: C(0,j)=j⋅(−2)C(0, j) = j \cdot (-2) e C(i,0)=i⋅(−2)C(i, 0) = i \cdot (-2).
  • (1,1)(1, 1): x1=y1=Ax_1 = y_1 = A, quindi C(0,0)+sAA=0+1=1C(0, 0) + s_{AA} = 0 + 1 = 1; le altre due scelte danno C(0,1)+g=−4C(0,1) + g = -4 e C(1,0)+g=−4C(1,0) + g = -4. Massimo 11, ↖\nwarrow.
  • (2,2)(2, 2): x2=Cx_2 = C e y2=Gy_2 = G sono diversi: C(1,1)+sCG=1−3=−2C(1,1) + s_{CG} = 1 - 3 = -2; C(1,2)+g=−1−2=−3C(1,2) + g = -1 - 2 = -3; C(2,1)+g=−1−2=−3C(2,1) + g = -1 - 2 = -3. Massimo −2-2, ↖\nwarrow.
  • (2,3)(2, 3): x2=y3=Cx_2 = y_3 = C: C(1,2)+1=−1+1=0C(1,2) + 1 = -1 + 1 = 0, contro −5-5 e −4-4. Vale 00.
  • (4,3)(4, 3): x4=Gx_4 = G, y3=Cy_3 = C. C(3,2)+sGC=−4−3=−7C(3,2) + s_{GC} = -4 - 3 = -7; C(3,3)+g=−1−2=−3C(3,3) + g = -1 - 2 = -3; C(4,2)+g=−2−2=−4C(4,2) + g = -2 - 2 = -4. Massimo −3-3 con ↑\uparrow: GG è allineata a uno spazio.

Risultato: C(5,4)=−2C(5, 4) = -2. Ricostruzione: (5,4)↖(5,4) \nwarrow colonna (T,T)(T, T); (4,3)↑(4,3) \uparrow colonna (G,−)(G, -); (3,3)↖(3,3) \nwarrow colonna (C,C)(C, C); (2,2)↖(2,2) \nwarrow colonna (C,G)(C, G); (1,1)↖(1,1) \nwarrow colonna (A,A)(A, A). Letto in avanti:

Z^X = A C C G T
Z^Y = A G C - T       costo: 1 + (-3) + 1 + (-2) + 1 = -2

La tabella e la ricostruzione sono state confrontate con una enumerazione di tutti gli allineamenti su 300300 coppie casuali di stringhe corte e matrici di score casuali.

Spazio lineare

Le stringhe di DNA sono molto lunghe e uno spazio Θ(mn)\Theta(mn) è inaccettabile. Se serve solo il costo ottimo (nessuna ricostruzione) basta una sola riga, anzi un solo vettore C[0..n]C[0..n], supponendo n≤mn \le m (altrimenti si scambiano i ruoli). All'inizio dell'iterazione jj interna all'iterazione ii, C[0..j−1]C[0..j-1] contiene la riga ii già calcolata, e C[j..n]C[j..n] la riga i−1i - 1. Calcolando C(i,j)C(i, j) servono C(i−1,j−1)C(i-1, j-1), C(i−1,j)=C[j]C(i-1, j) = C[j] e C(i,j−1)=C[j−1]C(i, j-1) = C[j-1]. Il primo termine va conservato prima di sovrascrivere C[j]C[j]: lo si tiene nella variabile prev.

ALIGNMENT_COST(X, Y, S, g)                { vincolo: |X| >= |Y| }
  m <- |X|; n <- |Y|
  for j <- 0 to n do C[j] <- j * g       { riga 0 }
  for i <- 1 to m do
    prev <- C[0]                         { C(i-1, 0), serve alla colonna j = 1 }
    C[0] <- i * g                        { C(i, 0) }
    for j <- 1 to n do
      new <- max(prev + S[x_i, y_j], C[j] + g, C[j-1] + g)
      prev <- C[j]                       { C(i-1, j), per la colonna j+1 }
      C[j] <- new
  return C[n]

Nell'esempio, dopo l'iterazione ii il vettore vale la riga ii della tabella: dopo i=1i = 1 è (−2,1,−1,−3,−5)(-2, 1, -1, -3, -5), dopo i=3i = 3 è (−6,−3,−4,−1,−3)(-6, -3, -4, -1, -3), e alla fine C[4]=−2C[4] = -2. Il tempo è Θ(mn)\Theta(mn), lo spazio ausiliario Θ(min⁡{m,n})\Theta(\min\{m, n\}); verificato su stringhe casuali contro la tabella completa.

Memoizzazione e spazio. Il codice memoizzato non permette di risparmiare spazio, perché le sottoistanze sono generate in ordine dinamico e non ordinato. In compenso, su sequenze molto simili fa molto meno lavoro del bottom-up. Il compromesso: se lo spazio quadratico non è un problema, la memoizzazione è preferibile; con spazio lineare serve il bottom-up (più lento su DNA molto simili).

Sottosequenza palindroma più lunga (LPS)

Definizione. Una stringa Z=⟨z1,…,zm⟩Z = \langle z_1, \dots, z_m \rangle è palindroma se z1+h=zm−hz_{1+h} = z_{m-h} per 0≤h≤m−10 \le h \le m - 1, cioè ZZ è uguale alla stringa letta al contrario. La stringa vuota è palindroma (m=0m = 0). Esistono palindrome di lunghezza pari (⟨A,C,C,A⟩\langle A, C, C, A \rangle) e dispari (⟨A,I,A⟩\langle A, I, A \rangle).

Data X=⟨x1,…,xn⟩X = \langle x_1, \dots, x_n \rangle, si cerca la sua Longest Palindrome Subsequence, LPS(X)\text{LPS}(X): la sottosequenza palindroma più lunga. Per esempio X=⟨S,O,B,S⟩X = \langle S, O, B, S \rangle ha LPS(X)=⟨S,O,S⟩\text{LPS}(X) = \langle S, O, S \rangle.

Lo spazio dei sottoproblemi

Se Z=⟨z1,…,zm⟩Z = \langle z_1, \dots, z_m \rangle è una sottosequenza palindroma di XX, realizzata dagli indici j1<⋯<jmj_1 < \dots < j_m, allora anche Z2..m−1Z_{2..m-1} è palindroma, ed è sottosequenza di X2..n−1X_{2..n-1} (se non è vuota), perché j2>1j_2 > 1 e jm−1<nj_{m-1} < n. Si toglie un carattere da ciascun estremo: servono quindi le sottostringhe Xi..jX_{i..j}, non i prefissi. Lo spazio è SX={Xi..j:1≤i≤j≤n}\mathcal{S}_X = \{X_{i..j} : 1 \le i \le j \le n\}, di cardinalità Θ(n2)\Theta(n^2).

Sottostruttura ottima

Sia Z=⟨z1,…,zm⟩Z = \langle z_1, \dots, z_m \rangle una LPS di Xi..jX_{i..j}.

  1. i=ji = j: Z=⟨xi⟩Z = \langle x_i \rangle (base).
  2. j=i+1j = i + 1: se xi=xjx_i = x_j allora Z=⟨xi,xj⟩Z = \langle x_i, x_j \rangle, altrimenti Z=⟨xi⟩Z = \langle x_i \rangle oppure Z=⟨xj⟩Z = \langle x_j \rangle (base).
  3. j>i+1j > i + 1 (almeno tre caratteri):
    • se xi=xjx_i = x_j, allora z1=zm=xiz_1 = z_m = x_i e Z2..m−1Z_{2..m-1} è una LPS di Xi+1..j−1X_{i+1..j-1};
    • se xi≠xjx_i \ne x_j, allora ZZ è la più lunga tra una LPS di Xi+1..jX_{i+1..j} e una LPS di Xi..j−1X_{i..j-1}.

Dimostrazione dei punti 1 e 2: banali. Punto 3, xi=xjx_i = x_j. Siano j1<⋯<jmj_1 < \dots < j_m gli indici che realizzano ZZ. Si dimostra prima z1=zm=xiz_1 = z_m = x_i. Se Z=εZ = \varepsilon o z1=zm≠xiz_1 = z_m \ne x_i, allora ⟨xi,Z,xj⟩\langle x_i, Z, x_j \rangle sarebbe ancora palindroma (i due estremi sono uguali tra loro) e sottosequenza di Xi..jX_{i..j} (usa gli indici i<j1i < j_1 e j>jmj > j_m, perché z1,zm≠xiz_1, z_m \ne x_i implica che né ii né jj siano tra gli indici di ZZ), più lunga di ZZ di 22 caratteri: contraddizione. Quindi z1=zm=xiz_1 = z_m = x_i e m≥2m \ge 2. Z2..m−1Z_{2..m-1} è palindroma ed è sottosequenza di Xi+1..j−1X_{i+1..j-1}. Se non fosse una LPS di Xi+1..j−1X_{i+1..j-1}, si prenderebbe una LPS Z′Z' di Xi+1..j−1X_{i+1..j-1} e ⟨xi,Z′,xj⟩\langle x_i, Z', x_j \rangle sarebbe una sottosequenza palindroma di Xi..jX_{i..j} più lunga di ZZ (cut-and-paste).

Punto 3, xi≠xjx_i \ne x_j. Poiché ZZ è palindroma, z1=zmz_1 = z_m non può essere uguale sia a xix_i sia a xjx_j. Se z1=zm≠xiz_1 = z_m \ne x_i, l'indice ii non è usato e ZZ è una sottosequenza di Xi+1..jX_{i+1..j}; se z1=zm≠xjz_1 = z_m \ne x_j, ZZ è sottosequenza di Xi..j−1X_{i..j-1} (può valere anche entrambi). In ciascun caso ZZ è palindroma e, per cut-and-paste, deve essere una LPS della sottostringa corrispondente. Poiché le LPS di Xi+1..jX_{i+1..j} e Xi..j−1X_{i..j-1} sono anche sottosequenze palindrome di Xi..jX_{i..j}, ZZ è la più lunga delle due. □\square

Ricorrenza e informazione addizionale

Con p(i,j)=∣LPS(Xi..j)∣p(i, j) = |\text{LPS}(X_{i..j})|, per 1≤i≤j≤n1 \le i \le j \le n:

p(i,j)={1i=j2j=i+1 ∧ xi=xj1j=i+1 ∧ xi≠xj2+p(i+1,j−1)j>i+1 ∧ xi=xjmax⁡{p(i+1,j), p(i,j−1)}j>i+1 ∧ xi≠xjp(i, j) = \begin{cases} 1 & i = j \\ 2 & j = i + 1 \ \land \ x_i = x_j \\ 1 & j = i + 1 \ \land \ x_i \ne x_j \\ 2 + p(i+1, j-1) & j > i + 1 \ \land \ x_i = x_j \\ \max\{p(i+1, j),\ p(i, j-1)\} & j > i + 1 \ \land \ x_i \ne x_j \end{cases}

Informazione addizionale, per i casi j>i+1j > i + 1: b(i,j)=↙b(i, j) = \swarrow se LPS(Xi..j)=⟨xi,LPS(Xi+1..j−1),xj⟩\text{LPS}(X_{i..j}) = \langle x_i, \text{LPS}(X_{i+1..j-1}), x_j \rangle; ↓\downarrow se =LPS(Xi+1..j)= \text{LPS}(X_{i+1..j}); ←\leftarrow se =LPS(Xi..j−1)= \text{LPS}(X_{i..j-1}).

Codice bottom-up per diagonali

Sia ℓ=j−i+1\ell = j - i + 1 la lunghezza della sottostringa. I casi base stanno sulle diagonali ℓ=1\ell = 1 e ℓ=2\ell = 2. Per ℓ≥3\ell \ge 3, la cella (i,j)(i, j) dipende da (i+1,j−1)(i+1, j-1) (lunghezza ℓ−2\ell - 2) e da (i+1,j)(i+1, j) e (i,j−1)(i, j-1) (lunghezza ℓ−1\ell - 1): una scansione per diagonali crescenti è coerente. Sono valide anche la scansione per righe dalla nn alla 11 (colonne crescenti) e quella per colonne dalla 11 alla nn (righe decrescenti).

COMPUTE_P(X)
  n <- |X|
  for i <- 1 to n-1 do
    P[i, i] <- 1
    if x_i = x_{i+1} then P[i, i+1] <- 2 else P[i, i+1] <- 1
  P[n, n] <- 1
  for l <- 3 to n do
    for i <- 1 to n - l + 1 do
      j <- i + l - 1
      if x_i = x_j then
        P[i, j] <- P[i+1, j-1] + 2;  B[i, j] <- '↙'
      else if P[i+1, j] >= P[i, j-1] then
        P[i, j] <- P[i+1, j];        B[i, j] <- '↓'
      else
        P[i, j] <- P[i, j-1];        B[i, j] <- '←'
  return (P[1, n], B)

Costo. Spazio Θ(n2)\Theta(n^2) per PP e BB. Un confronto tra caratteri per ogni sottostringa di lunghezza ≥2\ge 2, cioè per ogni coppia i<ji < j:

T(n)=∑i=1n−1∑j=i+1n1=∑i=1n−1(n−i)=∑i′=1n−1i′=n(n−1)2=Θ(n2).T(n) = \sum_{i=1}^{n-1} \sum_{j=i+1}^{n} 1 = \sum_{i=1}^{n-1} (n - i) = \sum_{i'=1}^{n-1} i' = \frac{n(n-1)}{2} = \Theta(n^2).

Se interessa solo la lunghezza, le tabelle si possono sostituire con tre vettori (diagonali ℓ\ell, ℓ−1\ell - 1 e ℓ−2\ell - 2), spazio lineare, come per la LCS (verificato con Python).

Esempio eseguito

X=ABCBDACBX = ABCBDACB (n=8n = 8). Ogni cella (i,j)(i, j) con i<ji < j contiene p(i,j)p(i, j) e la freccia; in grassetto le celle visitate dalla ricostruzione.

1: A 2: B 3: C 4: B 5: D 6: A 7: C 8: B
1: A 1 1 1↓ 3↓ 3↓ 5↙ 5← 5↓
2: B 1 1 3↙ 3← 3← 3↓ 5↙
3: C 1 1 1↓ 1↓ 3↙ 3↓
4: B 1 1 1↓ 1↓ 3↙
5: D 1 1 1↓ 1↓
6: A 1 1 1↓
7: C 1 1
8: B 1

Alcune celle:

  • (2,4)(2, 4): x2=x4=Bx_2 = x_4 = B, quindi p=2+p(3,3)=2+1=3p = 2 + p(3, 3) = 2 + 1 = 3 (↙\swarrow).
  • (1,3)(1, 3): x1=A≠C=x3x_1 = A \ne C = x_3; p(2,3)=1p(2, 3) = 1 e p(1,2)=1p(1, 2) = 1 sono uguali, e per il ≥\ge del codice si sceglie ↓\downarrow.
  • (1,6)(1, 6): x1=x6=Ax_1 = x_6 = A, quindi p=2+p(2,5)=2+3=5p = 2 + p(2, 5) = 2 + 3 = 5 (↙\swarrow).
  • (1,8)(1, 8): x1=A≠B=x8x_1 = A \ne B = x_8; p(2,8)=5≥p(1,7)=5p(2, 8) = 5 \ge p(1, 7) = 5, quindi ↓\downarrow.

Ricostruzione (cammino evidenziato): (1,8)↓(1,8) \downarrow passa a (2,8)(2,8), che è ↙\swarrow e stampa x2=Bx_2 = B; (3,7)(3,7) è ↙\swarrow e stampa CC; (4,6)(4,6) è ↓\downarrow e va a (5,6)(5,6), dove j=i+1j = i + 1 e x5=D≠A=x6x_5 = D \ne A = x_6: si stampa x5=Dx_5 = D; risalendo si stampano x7=Cx_7 = C e x8=Bx_8 = B. La LPS è ⟨B,C,D,C,B⟩\langle B, C, D, C, B \rangle, di lunghezza p(1,8)=5p(1, 8) = 5 (la stessa dei controlli con forza bruta e con LCS(X,XR)\text{LCS}(X, X^R)).

PRINT_LPS(X, B, i, j)                      { prima chiamata: PRINT_LPS(X, B, 1, n) }
  if i = j then print(x_i); return
  if j = i + 1 then
    if x_i = x_j then print(x_i x_j) else print(x_i)
    return
  if B[i, j] = '↙' then
    print(x_i)
    PRINT_LPS(X, B, i+1, j-1)
    print(x_j)
  else if B[i, j] = '↓' then PRINT_LPS(X, B, i+1, j)
  else PRINT_LPS(X, B, i, j-1)

(Nelle dispense i return e il ramo else dei casi base sono omessi: senza di essi, dopo il caso base il codice leggerebbe B[i,j]B[i,j], non definito.) Il numero di caratteri stampati è ∣LPS(X)∣=O(n)|\text{LPS}(X)| = O(n). L'albero delle chiamate ha un solo nodo per livello e la differenza j−ij - i (all'inizio n−1n - 1) decresce di 11 o di 22 a ogni livello, fino a 11 o 00: i confronti sono Θ(n)\Theta(n).

Relazione con la LCS. LPS(X)\text{LPS}(X) ha la stessa lunghezza di LCS(X,XR)\text{LCS}(X, X^R), con XRX^R la stringa rovesciata (verificato per 400400 stringhe casuali). Il codice dedicato è però più economico: usa il solo triangolo superiore (n(n−1)/2n(n-1)/2 confronti, invece degli n2n^2 della LCS).

python
def lps_len(x):
    n = len(x)
    p = [[0] * n for _ in range(n)]
    for i in range(n):
        p[i][i] = 1
    for i in range(n - 1):
        p[i][i + 1] = 2 if x[i] == x[i + 1] else 1
    for l in range(3, n + 1):
        for i in range(n - l + 1):
            j = i + l - 1
            p[i][j] = p[i + 1][j - 1] + 2 if x[i] == x[j] else max(p[i + 1][j], p[i][j - 1])
    return p[0][n - 1]

assert lps_len("ABCBDACB") == 5 and lps_len("SOBS") == 3

Domande d'esame

  1. (Risposta numerica.) Per X=⟨S,O,B,S⟩X = \langle S, O, B, S \rangle qual è LPS(X)\text{LPS}(X) e quanti confronti tra caratteri esegue COMPUTE_P su una stringa di n=10n = 10 caratteri? Traccia: ⟨S,O,S⟩\langle S, O, S \rangle; n(n−1)/2=45n(n-1)/2 = 45 confronti.
  2. (Quesito aperto.) Perché per la LPS lo spazio dei sottoproblemi è quello delle sottostringhe e non dei prefissi? Come si scandisce la tabella? Traccia: a ogni passo si tolgono caratteri da entrambi gli estremi, quindi servono sottostringhe Xi..jX_{i..j} (Θ(n2)\Theta(n^2)); (i,j)(i, j) dipende da (i+1,j−1)(i+1, j-1), (i+1,j)(i+1, j), (i,j−1)(i, j-1), di lunghezza ℓ−2\ell - 2 e ℓ−1\ell - 1: per diagonali crescenti.
  3. (Calcolo.) Con sUU=1s_{UU} = 1, sUV=−3s_{UV} = -3 e g=−2g = -2, per X=⟨A,C⟩X = \langle A, C \rangle e Y=⟨A⟩Y = \langle A \rangle quanto vale C(2,1)C(2, 1)? Qual è l'allineamento? Traccia: C(1,1)=1C(1, 1) = 1, C(2,0)=−4C(2, 0) = -4, C(1,0)=−2C(1, 0) = -2; C(2,1)=max⁡{−2−3, 1−2, −4−2}=−1C(2, 1) = \max\{-2 - 3,\ 1 - 2,\ -4 - 2\} = -1, con ↑\uparrow: AC/A−A C / A -, costo 1−2=−11 - 2 = -1.

Esercizi svolti di esame su problemi dello stesso tipo (ricorrenza sulle sottostringhe, scansione per diagonali): Esercizio - sottosequenza quasi palindroma di lunghezza massima (compito dell'8 luglio 2024, es. 2) e Esercizio - sottosequenza antipalindroma di lunghezza massima (compito del 9 luglio 2025, es. 2).

Versione ripasso

  • Allineamento. ΣDNAE={A,C,G,T,−}\Sigma^E_{DNA} = \{A, C, G, T, -\}. Coppia (ZX,ZY)(Z^X, Z^Y) di lunghezza k≥max⁡{m,n}k \ge \max\{m, n\}: XX sottosequenza di ZXZ^X, YY di ZYZ^Y, spazi solo fuori dai caratteri di XX e YY, mai due spazi nella stessa posizione. Peso di una posizione: szrXzrYs_{z^X_r z^Y_r} se nessuno spazio, gg altrimenti; costo = somma; si cerca il massimo. Esempio X=AGTTX = AGTT, Y=ATTY = ATT, sUU=1s_{UU} = 1, sUV=−3s_{UV} = -3, g=−2g = -2: AGTT/A−TTA G T T / A - T T costa 1−2+1+1=11 - 2 + 1 + 1 = 1 (ottimo); AGTT/ATT−A G T T / A T T - costa −3-3.
  • Sottostruttura (prefissi Xi,YjX_i, Y_j, ultima posizione di tre tipi, cut-and-paste): (xi,yj)(x_i, y_j) da (Xi−1,Yj−1)(X_{i-1}, Y_{j-1}), (xi,−)(x_i, -) da (Xi−1,Yj)(X_{i-1}, Y_j), (−,yj)(-, y_j) da (Xi,Yj−1)(X_i, Y_{j-1}). Casi base: l'unico allineamento di (ε,Yj)(\varepsilon, Y_j) ha jj spazi, costo jgjg.
  • Ricorrenza: C(i,0)=igC(i,0) = ig, C(0,j)=jgC(0,j) = jg, C(i,j)=max⁡{C(i−1,j−1)+sxiyj, C(i−1,j)+g, C(i,j−1)+g}C(i,j) = \max\{C(i-1,j-1) + s_{x_i y_j},\ C(i-1,j) + g,\ C(i,j-1) + g\}. Frecce ↖,↑,←\nwarrow, \uparrow, \leftarrow; tempo Θ(mn)\Theta(mn), ricostruzione O(m+n)O(m + n).
  • Un solo vettore (solo costo, ∣X∣≥∣Y∣|X| \ge |Y|): prev <- C[0]; C[0] <- i*g; for j: new <- max(prev + S[x_i,y_j], C[j] + g, C[j-1] + g); prev <- C[j]; C[j] <- new. Spazio Θ(min⁡{m,n})\Theta(\min\{m,n\}). Memoizzato: niente risparmio di spazio, meno lavoro su sequenze simili.
ε\varepsilon A G C T
ε\varepsilon 0 −2← −4← −6← −8←
A −2↑ 1↖ −1← −3← −5←
C −4↑ −1↑ −2↖ 0↖ −2←
C −6↑ −3↑ −4↖ −1↖ −3↖
G −8↑ −5↑ −2↖ −3↑ −4↖
T −10↑ −7↑ −4↑ −5↖ −2↖

X=ACCGTX = ACCGT, Y=AGCTY = AGCT: C(5,4)=−2C(5,4) = -2, allineamento ACCGT/AGC−TA C C G T / A G C - T (costo 1−3+1−2+11 - 3 + 1 - 2 + 1). Celle: (2,2)(2,2): max⁡{1−3, −1−2, −1−2}=−2\max\{1 - 3,\ -1 - 2,\ -1 - 2\} = -2; (2,3)(2,3): max⁡{−1+1, −3−2, −2−2}=0\max\{-1 + 1,\ -3 - 2,\ -2 - 2\} = 0; (4,3)(4,3): max⁡{−4−3, −1−2, −2−2}=−3\max\{-4 - 3,\ -1 - 2,\ -2 - 2\} = -3 con ↑\uparrow.

ALIGNMENT_COST(X, Y, S, g)       { |X| >= |Y|, un solo vettore C[0..n] }
  for j <- 0 to n do C[j] <- j*g
  for i <- 1 to m do
    prev <- C[0]; C[0] <- i*g
    for j <- 1 to n do
      new <- max(prev + S[x_i,y_j], C[j] + g, C[j-1] + g)
      prev <- C[j]; C[j] <- new
  return C[n]
  • LPS. Palindroma: z1+h=zm−hz_{1+h} = z_{m-h}, anche ε\varepsilon. Spazio: sottostringhe Xi..jX_{i..j} (Θ(n2)\Theta(n^2)), perché si tolgono i due estremi. Struttura per j>i+1j > i + 1: xi=xjx_i = x_j implica z1=zm=xiz_1 = z_m = x_i e Z2..m−1Z_{2..m-1} è LPS di Xi+1..j−1X_{i+1..j-1}; xi≠xjx_i \ne x_j: la più lunga tra LPS(Xi+1..j)(X_{i+1..j}) e LPS(Xi..j−1)(X_{i..j-1}).
  • Ricorrenza: p(i,i)=1p(i,i) = 1; p(i,i+1)=2p(i,i+1) = 2 se xi=xi+1x_i = x_{i+1}, altrimenti 11; per j>i+1j > i+1: 2+p(i+1,j−1)2 + p(i+1,j-1) se xi=xjx_i = x_j, altrimenti max⁡{p(i+1,j),p(i,j−1)}\max\{p(i+1,j), p(i,j-1)\}. Frecce ↙,↓,←\swarrow, \downarrow, \leftarrow. Scansione per diagonali ℓ=3..n\ell = 3..n (valgono anche righe da nn a 11 o colonne). Confronti: ∑i=1n−1(n−i)=n(n−1)2\sum_{i=1}^{n-1}(n-i) = \frac{n(n-1)}{2}; stampa Θ(n)\Theta(n).
  • Codice.
ALIGN: C[i,0] <- i*g; C[0,j] <- j*g; per i, j >= 1:  d <- C[i-1,j-1] + S[x_i,y_j]; u <- C[i-1,j] + g; l <- C[i,j-1] + g
       C[i,j] <- max(d,u,l);  b[i,j] <- '↖' se = d, '↑' se = u, altrimenti '←'

COMPUTE_P(X): P[i,i] <- 1; P[i,i+1] <- 2 se x_i = x_{i+1} altrimenti 1
  for l <- 3 to n do for i <- 1 to n-l+1 do
    j <- i+l-1
    if x_i = x_j then P[i,j] <- P[i+1,j-1] + 2; B[i,j] <- '↙'
    else if P[i+1,j] >= P[i,j-1] then P[i,j] <- P[i+1,j]; B[i,j] <- '↓'
    else P[i,j] <- P[i,j-1]; B[i,j] <- '←'
  • Esempio LPS: X=ABCBDACBX = ABCBDACB (n=8n = 8), p(1,8)=5p(1,8) = 5. Celle con i<ji < j (in grassetto le visitate dalla ricostruzione):
1: A 2: B 3: C 4: B 5: D 6: A 7: C 8: B
1: A 1 1 1↓ 3↓ 3↓ 5↙ 5← 5↓
2: B 1 1 3↙ 3← 3← 3↓ 5↙
3: C 1 1 1↓ 1↓ 3↙ 3↓
4: B 1 1 1↓ 1↓ 3↙
5: D 1 1 1↓ 1↓
6: A 1 1 1↓
7: C 1 1
8: B 1

Ricostruzione: (1,8)↓(1,8) \downarrow, (2,8)↙(2,8) \swarrow (stampa BB), (3,7)↙(3,7) \swarrow (stampa CC), (4,6)↓(4,6) \downarrow, (5,6)(5,6) base: DD; poi CC, BB: LPS =BCDCB= BCDCB. Celle: p(1,6)=2+p(2,5)=5p(1,6) = 2 + p(2,5) = 5, p(2,4)=2+p(3,3)=3p(2,4) = 2 + p(3,3) = 3. LPS(X)\text{LPS}(X) ha la lunghezza di LCS(X,XR)\text{LCS}(X, X^R), ma il codice dedicato usa metà delle celle.

  • Prova per xi=xjx_i = x_j: se fosse z1=zm≠xiz_1 = z_m \ne x_i (o Z=εZ = \varepsilon), ⟨xi,Z,xj⟩\langle x_i, Z, x_j \rangle sarebbe palindroma e più lunga di 22; se Z2..m−1Z_{2..m-1} non fosse una LPS di Xi+1..j−1X_{i+1..j-1}, si sostituirebbe con una LPS Z′Z' (cut-and-paste). Per xi≠xjx_i \ne x_j: z1=zmz_1 = z_m non è uguale sia a xix_i sia a xjx_j, quindi ZZ vive in Xi+1..jX_{i+1..j} o in Xi..j−1X_{i..j-1}.
  • Stampa: PRINT_LPS(X,B,i,j): i=ji = j stampa xix_i e termina; j=i+1j = i+1 stampa xixjx_ix_j se uguali, altrimenti xix_i, e termina; ↙\swarrow: stampa xix_i, ricorre su (i+1,j−1)(i+1,j-1), stampa xjx_j; ↓\downarrow: ricorre su (i+1,j)(i+1,j); ←\leftarrow: su (i,j−1)(i,j-1). Un nodo per livello, j−ij - i cala di 11 o 22: Θ(n)\Theta(n) confronti.
  • Errori tipici: usare i prefissi invece delle sottostringhe per la LPS; scansione per righe crescenti; dimenticare che per xi=xjx_i = x_j si somma 22 e non 11; sovrascrivere C[j]C[j] prima di salvare prev; stampare i casi base senza return.

Esercizi su questo argomento

Teoria collegata