Salta al contenuto
Note per Studenti Esercizio - costo alternato minimo e massimo di una sottosequenza (simulazione d'esame 2025-26, es. 2)

Esercizio - costo alternato minimo e massimo di una sottosequenza (simulazione d'esame 2025-26, es. 2)

Fonte: simulazione d'esame 2025-26 di Algoritmi per l'Ingegneria, Ing. Informatica UniPD, seconda parte, esercizio 2 (17 punti).

Testo

Data una stringa Z=⟨z1,z2,…,zk⟩Z = \langle z_1, z_2, \dots, z_k \rangle di interi distinti (positivi, negativi o nulli), il suo costo alternato è definito come

alt(Z)=∑i=1k(−1)i−1zi=z1−z2+z3−z4…\text{alt}(Z) = \sum_{i=1}^{k} (-1)^{i-1} z_i = z_1 - z_2 + z_3 - z_4 \dots

(si pone alt(ε)=0\text{alt}(\varepsilon) = 0). Per esempio, se Z=⟨1,−3,−1⟩Z = \langle 1, -3, -1 \rangle allora alt(Z)=+(1)−(−3)+(−1)=3\text{alt}(Z) = +(1) - (-3) + (-1) = 3. Data in ingresso una stringa XX di nn interi, si vogliono determinare il costo alternato minimo e il costo alternato massimo delle sue sottosequenze.

  • (a) [7 punti] Si formulino e si dimostrino due proprietà di sottostruttura ottima (per il problema di minimo e per quello di massimo) sullo spazio di sottoproblemi S={Xi:1≤i≤n+1}\mathcal{S} = \{X^i : 1 \le i \le n + 1\} formato da tutti i suffissi di XX (si ricorda che Xi=⟨xi,…,xn⟩X^i = \langle x_i, \dots, x_n \rangle e che Xn+1X^{n+1} denota convenzionalmente il suffisso vuoto). Si derivino le associate ricorrenze sui costi, denotando con m(i)m(i) il costo alternato minimo e con M(i)M(i) quello massimo per il sottoproblema XiX^i. (Suggerimento: potrebbe essere necessario formulare proprietà "intrecciate".)
  • (b) [6 punti] Si scriva lo pseudocodice di un algoritmo iterativo per il calcolo dei costi alternati minimo e massimo di sottosequenze di XX.
  • (c) [4 punti] Si determini il numero esatto T(n)T(n) di confronti e operazioni aritmetiche tra interi eseguite dall'algoritmo.

Soluzione

Un'identità sul costo alternato

Aggiungere un elemento davanti a una stringa inverte il segno di tutti gli altri termini:

alt(⟨x,w1,…,wk⟩)=x−w1+w2−⋯=x−(w1−w2+… )=x−alt(⟨w1,…,wk⟩).\text{alt}(\langle x, w_1, \dots, w_k \rangle) = x - w_1 + w_2 - \dots = x - (w_1 - w_2 + \dots) = x - \text{alt}(\langle w_1, \dots, w_k \rangle).

Questa identità vale anche per W=εW = \varepsilon (alt(⟨x⟩)=x−0\text{alt}(\langle x \rangle) = x - 0). È il motivo per cui le due proprietà sono "intrecciate": il minimo per XiX^i che inizia con xix_i si ottiene sottraendo xix_i a un massimo, e il massimo a un minimo. Per lavorare con i suffissi si aggiunge sempre davanti, quindi la scelta di includere o no xix_i dipende da una sottoistanza più corta, Xi+1X^{i+1}.

(a) Sottostruttura ottima

Sia Zm∗(i)Z^*_m(i) una sottosequenza di XiX^i di costo alternato minimo e ZM∗(i)Z^*_M(i) una di costo massimo (esistono: le sottosequenze di XiX^i sono finite).

Proprietà.

  1. Per i=n+1i = n + 1: Zm∗(n+1)=ZM∗(n+1)=εZ^*_m(n+1) = Z^*_M(n+1) = \varepsilon.
  2. Per 1≤i≤n1 \le i \le n: Zm∗(i)Z^*_m(i) è la più "piccola" (per costo alternato) tra Zm∗(i+1)Z^*_m(i+1) e ⟨xi,ZM∗(i+1)⟩\langle x_i, Z^*_M(i+1) \rangle.
  3. Per 1≤i≤n1 \le i \le n: ZM∗(i)Z^*_M(i) è la più "grande" tra ZM∗(i+1)Z^*_M(i+1) e ⟨xi,Zm∗(i+1)⟩\langle x_i, Z^*_m(i+1) \rangle.

Dimostrazione. Il punto 1 è banale: l'unica sottosequenza di ε\varepsilon è ε\varepsilon.

Punto 2. Sia 1≤i≤n1 \le i \le n e sia Z=Zm∗(i)=⟨xj1,…,xjk⟩Z = Z^*_m(i) = \langle x_{j_1}, \dots, x_{j_k} \rangle, con i≤j1<⋯<jk≤ni \le j_1 < \dots < j_k \le n. Due casi.

  • (2.a) Z=εZ = \varepsilon oppure j1>ij_1 > i. Allora ZZ è anche una sottosequenza di Xi+1X^{i+1}. Ogni sottosequenza di Xi+1X^{i+1} è anche sottosequenza di XiX^i, quindi ZZ ha costo minimo anche tra le sottosequenze di Xi+1X^{i+1}: è una Zm∗(i+1)Z^*_m(i+1).
  • (2.b) j1=ij_1 = i. Allora Z=⟨xi,W⟩Z = \langle x_i, W \rangle con W=⟨xj2,…,xjk⟩W = \langle x_{j_2}, \dots, x_{j_k} \rangle sottosequenza di Xi+1X^{i+1}, e per l'identità alt(Z)=xi−alt(W)\text{alt}(Z) = x_i - \text{alt}(W). Per minimizzare alt(Z)\text{alt}(Z) bisogna massimizzare alt(W)\text{alt}(W): se WW non fosse una ZM∗(i+1)Z^*_M(i+1), sostituendo WW con ZM∗(i+1)Z^*_M(i+1) (cut-and-paste) si otterrebbe una sottosequenza di XiX^i di costo strettamente minore, contraddicendo la minimalità di ZZ. Quindi WW è una ZM∗(i+1)Z^*_M(i+1).

In entrambi i casi Zm∗(i)Z^*_m(i) è uno dei due candidati Zm∗(i+1)Z^*_m(i+1) e ⟨xi,ZM∗(i+1)⟩\langle x_i, Z^*_M(i+1) \rangle, che sono entrambi sottosequenze ammissibili di XiX^i; poiché Zm∗(i)Z^*_m(i) è la migliore in assoluto, è la migliore delle due.

Punto 3. Simmetrico (si scambiano minimo e massimo). □\square

Ricorrenze sui costi. Con m(i)=alt(Zm∗(i))m(i) = \text{alt}(Z^*_m(i)) e M(i)=alt(ZM∗(i))M(i) = \text{alt}(Z^*_M(i)), l'identità sul costo dà

m(i)={0i=n+1min⁡{ m(i+1), xi−M(i+1) }1≤i≤nM(i)={0i=n+1max⁡{ M(i+1), xi−m(i+1) }1≤i≤nm(i) = \begin{cases} 0 & i = n + 1 \\ \min\{\, m(i+1),\ x_i - M(i+1) \,\} & 1 \le i \le n \end{cases} \qquad M(i) = \begin{cases} 0 & i = n + 1 \\ \max\{\, M(i+1),\ x_i - m(i+1) \,\} & 1 \le i \le n \end{cases}

Si vuole m(1)m(1) e M(1)M(1) (essendo X=X1X = X^1). Poiché ε\varepsilon è una sottosequenza, m(i)≤0≤M(i)m(i) \le 0 \le M(i) per ogni ii. L'ipotesi che gli interi siano distinti non serve alla correttezza.

(b) Algoritmo

m(i)m(i) e M(i)M(i) dipendono solo da xix_i e dai valori m(i+1)m(i+1) e M(i+1)M(i+1): si calcolano per ii decrescente da n+1n + 1 a 11 e basta conservare gli ultimi due valori, invece di due vettori.

MIN_MAX_ALT(X)
  n <- |X|
  curr_m <- 0; curr_M <- 0                  { m(n+1) = M(n+1) = 0 }
  for i <- n downto 1 do
    temp <- curr_m                          { salva m(i+1) prima di sovrascriverlo }
    curr_m <- MIN(temp, x_i - curr_M)       { m(i) = min{m(i+1), x_i - M(i+1)} }
    curr_M <- MAX(curr_M, x_i - temp)       { M(i) = max{M(i+1), x_i - m(i+1)} }
  return (curr_m, curr_M)

L'uso di temp è essenziale: il nuovo M(i)M(i) vuole il vecchio m(i+1)m(i+1), ma curr_m è già stato aggiornato a m(i)m(i). Senza temp si userebbe il valore sbagliato (errore classico). La correttezza segue dalle ricorrenze del punto (a) e dall'ordine di calcolo, che parte dal caso base i=n+1i = n + 1 e usa a ogni passo solo valori già calcolati.

Esempio

X=⟨3,−1,4,−2,5⟩X = \langle 3, -1, 4, -2, 5 \rangle (n=5n = 5). Si parte da m(6)=M(6)=0m(6) = M(6) = 0 e si scende:

ii xix_i m(i)=min⁡{m(i+1), xi−M(i+1)}m(i) = \min\{m(i+1),\ x_i - M(i+1)\} M(i)=max⁡{M(i+1), xi−m(i+1)}M(i) = \max\{M(i+1),\ x_i - m(i+1)\}
6 0 0
5 5 min⁡{0, 5−0}=0\min\{0,\ 5 - 0\} = 0 max⁡{0, 5−0}=5\max\{0,\ 5 - 0\} = 5
4 −2 min⁡{0, −2−5}=−7\min\{0,\ -2 - 5\} = -7 max⁡{5, −2−0}=5\max\{5,\ -2 - 0\} = 5
3 4 min⁡{−7, 4−5}=−7\min\{-7,\ 4 - 5\} = -7 max⁡{5, 4−(−7)}=11\max\{5,\ 4 - (-7)\} = 11
2 −1 min⁡{−7, −1−11}=−12\min\{-7,\ -1 - 11\} = -12 max⁡{11, −1−(−7)}=11\max\{11,\ -1 - (-7)\} = 11
1 3 min⁡{−12, 3−11}=−12\min\{-12,\ 3 - 11\} = -12 max⁡{11, 3−(−12)}=15\max\{11,\ 3 - (-12)\} = 15

Risultato: m(1)=−12m(1) = -12, M(1)=15M(1) = 15. Sottosequenze: il minimo è alt(⟨−1,4,−2,5⟩)=−1−4+(−2)−5=−12\text{alt}(\langle -1, 4, -2, 5 \rangle) = -1 - 4 + (-2) - 5 = -12; il massimo è alt(⟨3,−1,4,−2,5⟩)=3+1+4+2+5=15\text{alt}(\langle 3, -1, 4, -2, 5 \rangle) = 3 + 1 + 4 + 2 + 5 = 15 (ogni elemento in posizione pari è negativo e la sottrazione lo rende positivo). Entrambi i valori coincidono con quelli ottenuti enumerando tutte le 252^5 sottosequenze; l'algoritmo è stato verificato in Python con 400400 stringhe casuali di interi distinti, lunghezza fino a 99.

Se serve anche la sottosequenza, si salva per ogni ii un bit: quale dei due candidati realizza il minimo e quale il massimo (informazione addizionale, passo 2′2' dello schema in 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 →).

(c) Numero esatto di operazioni

Il ciclo ha nn iterazioni. Ognuna esegue due sottrazioni (xi−curr_Mx_i - \text{curr\_M} e xi−tempx_i - \text{temp}) e due confronti (uno in MIN, uno in MAX); gli assegnamenti non si contano. Quindi

T(n)=∑i=1n4=4n.T(n) = \sum_{i=1}^{n} 4 = 4n.

Per n=5n = 5 sono 2020 operazioni. L'algoritmo è lineare, e lo spazio ausiliario è O(1)O(1).

Versione ripasso

Testo. alt(Z)=∑i=1k(−1)i−1zi=z1−z2+z3−…\text{alt}(Z) = \sum_{i=1}^{k}(-1)^{i-1}z_i = z_1 - z_2 + z_3 - \dots, alt(ε)=0\text{alt}(\varepsilon) = 0. Data XX di nn interi: (a) [7 punti] due proprietà di sottostruttura ottima (minimo e massimo) sui suffissi XiX^i, 1≤i≤n+11 \le i \le n+1, e ricorrenze per m(i)m(i) e M(i)M(i) (suggerimento: proprietà intrecciate); (b) [6 punti] pseudocodice iterativo; (c) [4 punti] numero esatto T(n)T(n) di confronti e operazioni aritmetiche.

  • Identità chiave: alt(⟨x,W⟩)=x−alt(W)\text{alt}(\langle x, W \rangle) = x - \text{alt}(W) (aggiungere davanti inverte tutti i segni). Perciò il minimo che usa xix_i si ottiene da un massimo di Xi+1X^{i+1}, e viceversa.
  • Proprietà: Zm∗(n+1)=ZM∗(n+1)=εZ^*_m(n+1) = Z^*_M(n+1) = \varepsilon; per i≤ni \le n, Zm∗(i)Z^*_m(i) è la migliore tra Zm∗(i+1)Z^*_m(i+1) e ⟨xi,ZM∗(i+1)⟩\langle x_i, Z^*_M(i+1) \rangle; ZM∗(i)Z^*_M(i) la migliore tra ZM∗(i+1)Z^*_M(i+1) e ⟨xi,Zm∗(i+1)⟩\langle x_i, Z^*_m(i+1) \rangle. Prova: se ZZ non usa xix_i è sottosequenza di Xi+1X^{i+1} e ottima lì; se Z=⟨xi,W⟩Z = \langle x_i, W \rangle, alt(Z)=xi−alt(W)\text{alt}(Z) = x_i - \text{alt}(W) e WW deve massimizzare alt\text{alt} (cut-and-paste).
  • Ricorrenze: m(n+1)=M(n+1)=0m(n+1) = M(n+1) = 0; m(i)=min⁡{m(i+1), xi−M(i+1)}m(i) = \min\{m(i+1),\ x_i - M(i+1)\}; M(i)=max⁡{M(i+1), xi−m(i+1)}M(i) = \max\{M(i+1),\ x_i - m(i+1)\}. Sempre m(i)≤0≤M(i)m(i) \le 0 \le M(i).
MIN_MAX_ALT(X)
  n <- |X|; curr_m <- 0; curr_M <- 0
  for i <- n downto 1 do
    temp <- curr_m
    curr_m <- MIN(temp, x_i - curr_M)
    curr_M <- MAX(curr_M, x_i - temp)
  return (curr_m, curr_M)

temp serve perché M(i)M(i) usa il vecchio m(i+1)m(i+1).

  • Esempio X=⟨3,−1,4,−2,5⟩X = \langle 3, -1, 4, -2, 5 \rangle: (m,M)(m, M) per i=6,5,4,3,2,1i = 6, 5, 4, 3, 2, 1 vale (0,0),(0,5),(−7,5),(−7,11),(−12,11),(−12,15)(0,0), (0,5), (-7,5), (-7,11), (-12,11), (-12,15). Quindi minimo −12-12 con ⟨−1,4,−2,5⟩\langle -1, 4, -2, 5 \rangle e massimo 1515 con tutta XX.
  • Costo: nn iterazioni, ciascuna con 22 sottrazioni e 22 confronti: T(n)=4nT(n) = 4n (lineare, spazio O(1)O(1)).
  • Errori tipici: usare curr_m già aggiornato nel calcolo di M(i)M(i); scrivere xi+M(i+1)x_i + M(i+1) invece di xi−M(i+1)x_i - M(i+1); dimenticare che ε\varepsilon è ammessa (quindi m≤0≤Mm \le 0 \le M); contare solo i confronti.

Teoria collegata