Esercizio - ricorrenza c(i,j) con bordi dati da due array, calcolo iterativo e spazio lineare (compito del 18 giugno 2026, es. 2)
Fonte: compito di Algoritmi per l'Ingegneria, Ing. Informatica UniPD, 18 giugno 2026, seconda parte, esercizio 2 (16 punti).
Testo
Sia . Dati due array di numeri reali e , si consideri la seguente ricorrenza:
- (a) [11 punti] Si fornisca lo pseudocodice di un algoritmo iterativo
COMPUTE_c(A, B)che, dati in ingresso i due array e a componenti, restituisca in uscita il valore . Si provi la correttezza dell'algoritmo proposto. - (b) [5 punti] Si determini il numero esatto di operazioni aritmetiche tra numeri reali effettuate da
COMPUTE_c(A, B).
Soluzione
Lettura della ricorrenza
I tre casi coprono tutte le coppie con senza sovrapposizioni: la prima colonna () vale ; la ultima riga (), per , vale (quindi non viene mai usato, perché ); le celle interne, con e , si calcolano con una somma e una sottrazione. È la struttura tipica della 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 →): una tabella con i casi base sul bordo e le altre celle determinate da celle già note.
(a) Ordine di riempimento
La cella dipende da tre celle:
- : stessa riga, colonna precedente;
- : riga sotto, colonna precedente;
- : riga sotto, stessa colonna.
Va scelto un ordine in cui queste tre siano calcolate prima di . Le dipendenze vanno verso colonne di indice minore o uguale e verso righe di indice maggiore: l'ordine naturale è per colonne crescenti () e, dentro ogni colonna, per righe decrescenti (). Con questo ordine, quando si arriva a :
- e stanno nella colonna , già completa (o sono nella colonna , che contiene i dati);
- è nella stessa colonna , ma una riga più in basso, già calcolata o appartenente alla riga base .
(Funziona anche la scansione per righe, dalla riga alla riga , ciascuna per colonne crescenti: la riga è completa quando si lavora sulla riga , e precede nella stessa riga. Non funzionano né le righe crescenti né le colonne decrescenti.)
COMPUTE_c(A, B)
n <- A.len
C[1, 1] <- A[1]
for i <- 2 to n do
C[i, 1] <- A[i] { prima colonna }
C[n, i] <- B[i] { ultima riga, colonne 2..n }
for j <- 2 to n do
for i <- n-1 downto 1 do
C[i, j] <- C[i, j-1] + C[i+1, j-1] - C[i+1, j]
return C[1, n](Per i cicli sono vuoti e si restituisce .)
Correttezza
Affermazione. Al termine di ogni scrittura di una cella , vale .
Per induzione sull'ordine di scrittura delle celle.
- Base. Le celle scritte nei cicli di inizializzazione, per e per , sono corrette per la definizione dei primi due casi.
- Passo. Sia con , , calcolata nel ciclo doppio. Per l'ordine di scansione, nel momento in cui si esegue l'assegnamento le tre celle , e sono già state scritte (vedi sopra) e quindi, per ipotesi induttiva, contengono , e . Allora per il terzo caso.
Poiché la cella è calcolata dal ciclo (per ), l'algoritmo restituisce .
Spazio: basta un vettore
Per calcolare una cella della colonna servono solo due celle della colonna e quella immediatamente sotto, nella stessa colonna. Quindi non serve la tabella quadratica. Con un solo vettore che contiene la colonna corrente (si sovrascrive man mano la colonna con la colonna ):
COMPUTE_c_LINEARE(A, B)
n <- A.len
for i <- 1 to n do V[i] <- A[i] { V = colonna 1 }
for j <- 2 to n do
old <- V[n] { c(n, j-1) }
V[n] <- B[j] { c(n, j) }
for i <- n-1 downto 1 do
tmp <- V[i] { c(i, j-1) }
V[i] <- tmp + old - V[i+1] { V[i+1] contiene già c(i+1, j) }
old <- tmp { diventa c(i+1, j-1) per la riga i-1 }
return V[1]Invariante: all'inizio del passo della colonna , old e . Spazio invece di . (Se si usano due vettori prev e curr bisogna che prev <- curr sia una copia o uno scambio di ruoli, non un'assegnazione che fa puntare i due nomi allo stesso array: altrimenti la colonna verrebbe sovrascritta mentre serve ancora.)
Esempio ()
, (il valore non viene usato). Tabella , con le righe dall'alto e le colonne da sinistra:
| 1 | 2 | 3 | 4 | |
|---|---|---|---|---|
| 1 | 2 | 1 | −1 | 3 |
| 2 | 5 | 6 | 8 | 4 |
| 3 | 1 | 0 | −2 | 2 |
| 4 | 3 | 4 | 6 | 2 |
La prima colonna è e l'ultima riga è : e poi . Colonna dal basso: ; ; . Colonna : . Il risultato è con operazioni.
Verifica: lo pseudocodice (tabella e vettore) è stato confrontato in Python con la definizione ricorsiva su coppie casuali di array, con fino a .
(b) Numero esatto di operazioni
Per ogni cella interna (, ) si fanno due operazioni aritmetiche (una somma e una sottrazione); le inizializzazioni non fanno operazioni. Le celle interne sono righe per colonne:
(Per : ; per : .) Il conto si ottiene con la regola di sostituzione dei cicli con le sommatorie (SommatorieIl simbolo di sommatoria, le sue proprietà (linearità, additività, cambio di indice) e le somme notevoli di Gauss e geometrica.Sommatorie →). La versione a vettore esegue le stesse operazioni: l'unica differenza è l'uso della memoria.
Versione ripasso
Testo. se ; se e ; se e . (a) [11 punti] Algoritmo iterativo COMPUTE_c(A,B) che restituisce , con prova di correttezza. (b) [5 punti] Numero esatto di operazioni aritmetiche.
- Dipendenze: usa , , . Ordine: colonne crescenti e, in ogni colonna, righe decrescenti (vale anche: righe da a , colonne crescenti). Righe crescenti o colonne decrescenti sono sbagliate.
COMPUTE_c(A, B)
n <- A.len; C[1,1] <- A[1]
for i <- 2 to n do C[i,1] <- A[i]; C[n,i] <- B[i]
for j <- 2 to n do
for i <- n-1 downto 1 do
C[i,j] <- C[i,j-1] + C[i+1,j-1] - C[i+1,j]
return C[1,n]- Correttezza (induzione sull'ordine di scrittura): le celle di bordo sono corrette per definizione; per una cella interna le tre da cui dipende sono già scritte e, per ipotesi, corrette, quindi vale il terzo caso. non è mai usato.
- Un solo vettore (colonna corrente): per ogni ,
old <- V[n]();V[n] <- B[j]; per :tmp <- V[i]; V[i] <- tmp + old - V[i+1]; old <- tmp. Spazio . Con due vettori,prev <- currdeve essere una copia. - Esempio , , :
| 1 | 2 | 3 | 4 | |
|---|---|---|---|---|
| 1 | 2 | 1 | −1 | 3 |
| 2 | 5 | 6 | 8 | 4 |
| 3 | 1 | 0 | −2 | 2 |
| 4 | 3 | 4 | 6 | 2 |
Per esempio , .
- Costo: due operazioni per ogni cella interna, celle: ( per , per ).
- Errori tipici: ordine di scansione che legge prima di averlo calcolato; usare ; dimenticare che restituisce ; contare una sola operazione per cella; con due vettori, assegnare per riferimento.