Sottostruttura ottima
Una scelta ottima contiene, al suo interno, scelte ottime per i sottoproblemi che genera.
Algoritmi · Ottimizzazione
La programmazione dinamica risolve una volta sola ogni sottoproblema e conserva il risultato. Matrix Chain Order rende il metodo particolarmente visibile: il costo ottimo nasce riempiendo diagonale dopo diagonale due tabelle triangolari.
Dividere un problema non basta. La programmazione dinamica è utile quando molti percorsi ricorsivi chiedono gli stessi sottoproblemi e una soluzione globale ottima può essere composta da soluzioni ottime più piccole.
Una scelta ottima contiene, al suo interno, scelte ottime per i sottoproblemi che genera.
La ricorsione ingenua ricalcola più volte gli stessi stati. Memorizzarli elimina la ripetizione.
Definisci precisamente cosa rappresenta una cella e quale decisione permette di passare a stati più piccoli.
Parti dal problema completo, usa la ricorsione e salva ogni risultato alla prima visita. Vengono calcolati soltanto gli stati effettivamente richiesti.
Parti dai casi base e riempi una tabella in un ordine che renda già disponibili tutte le dipendenze. MatrixChainOrder usa questo approccio.
Il prodotto di matrici è associativo, quindi ogni parentesizzazione produce la stessa matrice finale. Cambia però enormemente il numero di moltiplicazioni scalari. Non stiamo moltiplicando le matrici: stiamo scegliendo l’ordine meno costoso.
10·30·5 + 10·5·60 = 4 500
30·5·60 + 10·30·60 = 27 000
Una catena di n matrici è descritta dal vettore p[0…n]: Aᵢ ha dimensione p[i−1] × p[i].
m[i,j] è il numero minimo di moltiplicazioni scalari necessarie per calcolare Aᵢ…Aⱼ. s[i,j] memorizza il valore di k che realizza quel minimo.
Le catene di lunghezza 1 costano zero. Per una catena più lunga proviamo ogni ultimo taglio k: costo della parte sinistra, costo della parte destra e costo per moltiplicare i due risultati.
Come nello schema alla lavagna, la diagonale dei casi base è in basso. Ogni nuova diagonale sale di un livello: quando calcoliamo una cella, tutte le celle da cui dipende sono già state riempite.
Avvia l’animazione per confrontare i tagli.
Costo ottimo: —
M conserva il valore ottimo, ma da solo non dice quali scelte lo hanno prodotto. S conserva il taglio: partendo da s[1,n], ricostruiamo ricorsivamente prima A₁…Aₖ e poi Aₖ₊₁…Aₙ.
Print(i,j) → se i = j stampa Aᵢ; altrimenti stampa “(”, Print(i,s[i,j]), Print(s[i,j]+1,j), “)”.
Quando calcoliamo m[i,j], ogni possibile parentesizzazione ha un ultimo prodotto che separa la catena in Aᵢ…Aₖ e Aₖ₊₁…Aⱼ per un unico k. Per sottostruttura ottima, se una delle due parti non fosse ottima potremmo sostituirla e migliorare la soluzione globale. Provare tutti i k e scegliere il minimo è quindi sufficiente.
| Risorsa | Costo | Perché |
|---|---|---|
| Sottoproblemi | Θ(n²) | Una cella per ogni intervallo [i,j]. |
| Tempo | Θ(n³) | Per ogni intervallo si provano fino a n−1 tagli. |
| Memoria | Θ(n²) | Le due tabelle triangolari M e S. |
| Ricostruzione | Θ(n) | La parentesizzazione contiene n matrici e n−1 tagli. |