Divide and conquer - proprietà di sottostruttura e albero delle chiamate
In questa pagina 5
Il primo paradigma di progetto: scomporre un'istanza grande in sottoistanze più piccole dello stesso problema, risolverle, e ricombinare le soluzioni. Gli algoritmi ricorsivi di Algoritmi ricorsiviAlgoritmi ricorsivi come induzione eseguita; albero della ricorsione e record di attivazione nello stack; esempi ReverseArray, LinearSum, Power in tempo logaritmico, Fibonacci ricorsivo (esponenziale) e con memoizzazione; analisi della complessità con l'albero della ricorsione e correttezza per induzione.Algoritmi ricorsivi → ne sono realizzazioni. Il problema è (Problemi computazionali - istanze, soluzioni, correttezza e complessitàUn problema computazionale è una relazione totale (non necessariamente univoca) tra istanze e soluzioni; un algoritmo è una funzione dalle istanze alle soluzioni e risolve il problema se la sua uscita è sempre in relazione con l'ingresso. Correttezza = coerenza con la specifica; complessità = costo in funzione della taglia, per default al caso peggiore, con le istruzioni a costo unitario scelte in modo significativo.Problemi computazionali - istanze, soluzioni, correttezza e complessità →); indica le sequenze finite (anche vuote) di elementi di .
Proprietà di sottostruttura
Definizione (proprietà di sottostruttura). Per è una terna :
- una soglia e la descrizione esplicita delle soluzioni delle istanze di base, cioè con ;
- una funzione di Divide , che a un'istanza con associa una sequenza di sottoistanze ;
- una funzione di Conquer , che da una sequenza di soluzioni ne costruisce una.
Devono valere, per ogni istanza non di base con :
- per (le sottoistanze sono strettamente più piccole);
- se sono tali che , allora .
La condizione 1 garantisce la terminazione (le taglie sono interi non negativi e decrescono), la condizione 2 garantisce che la ricombinazione dia una soluzione. Dalla proprietà si ottiene l'algoritmo se si hanno: un metodo diretto per le istanze di base, un algoritmo che realizza e un algoritmo che realizza . Il metacodice è:
D&C(i) {i in I}
[B]: if |i| <= n0 then *risolvi direttamente*; return
[D]: <i_1, ..., i_k> <- A_D(i) // fase di divide
[R]: for j <- 1 to k do s_j <- D&C(i_j) // fase di recurse
[C]: return A_C(s_1, ..., s_k) // fase di conquerNella fase [R] le soluzioni delle sottoistanze si ottengono "gratis" con le chiamate ricorsive. Le frasi tra asterischi *...* indicano attività che non si codificano in pseudocodice: vanno usate il meno possibile e mai dove la specifica in pseudocodice è possibile; // introduce un commento.
La correttezza della proprietà implica quella dell'algoritmo: la dimostrazione ricalca la proprietà.
Albero delle chiamate
L'esecuzione su un'istanza si schematizza con l'albero delle chiamate: la radice è l'istanza principale, i figli di un nodo sono le sottoistanze generate dalla sua fase [D], le foglie sono istanze di base (taglia ). In principio ogni nodo può avere un numero diverso di figli; per tutti gli algoritmi del corso l'arietà è la stessa per tutti i nodi interni.
- Divide espande l'albero dalla radice alle foglie (top-down); Conquer lo contrae dalle foglie alla radice (bottom-up), fino alla soluzione dell'istanza principale.
- Quando il sottoalbero di una sottoistanza viene contratto e la sua soluzione calcolata, tutta l'informazione dei suoi nodi viene distrutta: il D&C non ha memoria del processo di risoluzione. Se la stessa sottoistanza compare in punti diversi dell'albero, il suo sottoalbero viene espanso e contratto più volte, con conseguenze sul tempo che possono essere disastrose: è la debolezza che la programmazione dinamica corregge (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 →).
- Per la stessa ragione l'ordine in cui l'albero viene espanso è irrilevante. In pratica la ricorsione usa lo stack di sistema e l'albero viene generato e contratto gradualmente, con una visita anticipata (in preordine) che contiene l'uso di memoria.
Un esempio paradigmatico: il massimo di un array
Problema. Dato con interi, trovare il massimo.
Sottostruttura. Per si partiziona l'array in due sottoarray non vuoti (il più bilanciati possibile) e il massimo è il massimo tra i due massimi. Per il massimo è l'elemento stesso, quindi . La generica sottoistanza è il massimo del sottovettore , con : i parametri dell'algoritmo devono poter indicare la sottoistanza, e si chiama la procedura come . La taglia di è .
MAX(A, i, j)
[B]: if i = j then return A[i]
[D]: k <- floor((i + j)/2)
[R]: m1 <- MAX(A, i, k)
m2 <- MAX(A, k + 1, j)
[C]: if m1 >= m2 then return m1 else return m2def MAX(A, i, j):
if i == j:
return A[i]
k = (i + j) // 2
m1 = MAX(A, i, k)
m2 = MAX(A, k + 1, j)
return m1 if m1 >= m2 else m2Correttezza
Il metodo è l'induzione sulla taglia , con tre parti: base [B] (correttezza delle istanze di taglia ), ipotesi [HP] (le soluzioni delle sottoistanze di taglia minore di sono corrette), tesi [TH] (Divide produce sottoistanze strettamente più piccole e Conquer dà una soluzione dell'istanza).
[B] Se , allora , cioè : MAX restituisce , l'unico elemento di , che ne è anche il massimo.
[HP] MAX è corretta per tutte le taglie , con .
[TH], correttezza del Divide. Le due sottoistanze sono e , con : Poiché si ha , e di conseguenza . Il lato destro dà ; il lato sinistro, , dà , e anche e (le sottoistanze sono non vuote). Quindi le due sottoistanze hanno taglia strettamente minore di , e per [HP] e sono corretti.
[TH], correttezza del Conquer. Siano , , . Per [HP] e . Poiché e il massimo di un'unione finita è il massimo dei massimi, MAX restituisce .
La dimostrazione dell'algoritmo ricalca quella della proprietà; nel corso si prova di norma solo la proprietà e la correttezza dell'algoritmo ne è conseguenza immediata.
Complessità: la ricorrenza
Modello di costo: costa solo il confronto tra elementi di ; confronti tra indici, assegnamenti e il resto costano . Il caso base costa (c'è solo un confronto tra indici). Per si sommano i costi di Divide (: nessun confronto tra elementi), Recurse (due chiamate, che introducono i termini impliciti) e Conquer (: il confronto ). Le due sottoistanze hanno taglie e , quindi, per ogni ,
Una ricorrenza definisce in funzione di valori di su argomenti minori; di solito è data a tratti, con i casi di base espliciti. Poiché il dominio di è , l'argomento deve essere sempre intero: non si può scrivere senza ipotesi. Per potenza di i valori restano interi a ogni livello; non basta che sia pari (per si arriva a , dispari, a cui la definizione semplificata non si applica). Con questa restrizione si risolve facilmente e si ricava l'ordine di grandezza; poi altre tecniche generalizzano.
Srotolamento (unfolding), . Si applica ripetutamente la definizione implicita fino ai casi di base: Il processo si ferma quando è il caso base, (vedi Esponenziale e logaritmoLa funzione esponenziale a^x (base positiva diversa da 1) e la sua inversa, il logaritmo in base a, con grafici e proprietà.Esponenziale e logaritmo →). Sostituendo, con e : dove si è usata la somma geometrica troncata per (qui ; vedi SommatorieIl simbolo di sommatoria, le sue proprietà (linearità, additività, cambio di indice) e le somme notevoli di Gauss e geometrica.Sommatorie →).
Verifica della formula chiusa per induzione. Ottenuta una formula chiusa, una verifica per induzione protegge dagli errori di calcolo. Si prova per :
- [B] : .
- [HP] per le potenze di minori di , con .
- [TH] e è una potenza di minore di , quindi .
Il vero parametro dell'induzione è , che varia sui naturali consecutivi e genera tutte le potenze di . Che il risultato valga per ogni , non solo per le potenze di due, si dimostra con l'induzione parametrica (Induzione parametrica - limitazioni esatte delle ricorrenzeL'induzione parametrica prova una limitazione T(n) <= cn+d (o >=) con costanti c e d incognite: si simula la verifica per induzione, si raccolgono i vincoli su c e d dalla base e dal passo HP => TH, e se l'intersezione non è vuota si sceglie la coppia migliore. Per MAX dà esattamente T(n) = n-1; può fallire anche con guess veri (si aggiunge un parametro o un termine di ordine inferiore).Induzione parametrica - limitazioni esatte delle ricorrenze →) oppure con l'albero (sotto).
Albero della ricorrenza di MAX
Per l'istanza (taglia ) l'albero delle chiamate è:
[1 7 1 7]
/ \
[1 7] [1 7]
/ \ / \
[1] [7] [1] [7]Le frecce di generazione vanno verso il basso (Divide), quelle di risoluzione verso l'alto (Conquer). Si noti che questa chiamata risolve ripetutamente sottoistanze identiche ( compare due volte) e replica tutto il lavoro: è la mancanza di memoria. Associando a ogni nodo il costo del suo Divide più Conquer (il lavoro, o work), si ottiene l'albero della ricorrenza: stessa forma, ma con la taglia nel nodo e il lavoro accanto.
(4) 1
/ \
(2) 1 (2) 1
/ \ / \
(1) 0 (1) 0 (1) 0 (1) 0La somma di tutti i lavori è , quindi .
Per qualsiasi l'albero non è completo: per la radice ha come figlio sinistro la foglia e come destro il nodo interno , che genera le due foglie e . Valgono però due fatti, per ogni :
- ogni nodo interno ha sempre due figli (due chiamate ricorsive su istanze non di base) e lavoro ;
- ci sono esattamente foglie, perché ogni chiamata partiziona l'insieme del suo sottovettore in due sottoinsiemi disgiunti e si arriva alle istanze , tutte con lavoro .
Un albero binario si dice pieno se ogni nodo interno ha esattamente figli (non confondere con completo, in cui tutte le foglie sono allo stesso livello).
Teorema. Un albero binario pieno con foglie ha sempre nodi interni.
Dimostrazione per induzione su .
- [B] Un albero pieno con una sola foglia non ha nodi interni ().
- [HP] La proprietà vale per gli alberi pieni con foglie, , con .
- [TH] Sia un albero pieno con foglie, quindi con almeno un nodo interno. Si prenda un nodo interno di livello massimo. I suoi figli stanno a un livello maggiore e non possono essere interni (altrimenti non avrebbe livello massimo tra gli interni): sono quindi due foglie. Sia l'albero ottenuto togliendo i due figli di . In scompaiono due foglie e compare una nuova foglia (), per cui ha foglie ed è ancora pieno; ha un nodo interno in meno di . Per [HP] ha nodi interni, quindi ne ha .
Poiché in MAX i soli contributi non nulli vengono dai nodi interni, ciascuno di lavoro , si conclude per ogni , in accordo con l'induzione parametrica. Un conteggio con Python conferma il risultato:
from functools import lru_cache
@lru_cache(None)
def T(n): # ricorrenza con parti intere
return 0 if n == 1 else T(n // 2) + T(-(-n // 2)) + 1
assert all(T(n) == n - 1 for n in range(1, 2000))Lavoro che dipende dall'istanza. Non sempre istanze della stessa taglia hanno lo stesso costo. Con MERGESORT, se il modello conta solo i confronti tra chiavi, durante la fusione appena uno dei due sottovettori è esaurito il resto dell'altro si copia senza altri confronti: il lavoro del Conquer in un nodo varia con l'istanza, pur restando limitato da (somma delle lunghezze) . Per questo la ricorrenza di MERGESORT, nel caso peggiore, è (Albero della ricorrenza e formula generale per le ricorrenzeL'albero della ricorrenza ha la forma dell'albero delle chiamate dell'istanza peggiore e T(n) è la somma dei lavori dei nodi. Per la famiglia T(n) = s(n)T(f(n)) + w(n) con base T0 per n <= n0, contando i nodi per livello con le iterate di f e il livello f*(n,n0) si ottiene la formula chiusa T(n) = somma_l [prodotto_j s(f^(j)(n))] w(f^(l)(n)) + [prodotto s] T0, verificata per induzione e applicata a esempi (merge sort, Hanoi, ricorrenze con radice).Albero della ricorrenza e formula generale per le ricorrenze →).
Domande d'esame
- Si determini per , per . Risposta: la ricorrenza ha lo stesso albero di MAX ( foglie, nodi interni a lavoro nullo), con lavoro a ogni foglia: , . Vedi Esercizio - domande di prima parte su problemi computazionali, ricorrenze e conteggi (simulazioni d'esame).
- Quali condizioni deve soddisfare una proprietà di sottostruttura e perché? Risposta: sottoistanze di taglia strettamente minore (terminazione) e per (correttezza del Conquer); più la soluzione esplicita per .
- Dimostrare che un albero binario pieno con foglie ha nodi interni. Traccia: induzione su rimuovendo i due figli-foglia di un nodo interno di livello massimo.
Versione ripasso
- Sottostruttura : soluzioni esplicite per ; con ; con (Problemi computazionali - istanze, soluzioni, correttezza e complessitàUn problema computazionale è una relazione totale (non necessariamente univoca) tra istanze e soluzioni; un algoritmo è una funzione dalle istanze alle soluzioni e risolve il problema se la sua uscita è sempre in relazione con l'ingresso. Correttezza = coerenza con la specifica; complessità = costo in funzione della taglia, per default al caso peggiore, con le istruzioni a costo unitario scelte in modo significativo.Problemi computazionali - istanze, soluzioni, correttezza e complessità →). Metacodice: [B] caso base, [D] divide, [R] recurse, [C] conquer.
- Albero delle chiamate: radice = istanza principale, foglie = istanze di base; Divide scende (top-down), Conquer risale (bottom-up). Senza memoria: le sottoistanze uguali vengono ricalcolate (rimedio: 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 →). L'ordine di espansione è irrilevante.
- MAX: , due chiamate su e , confronto finale. Correttezza per induzione sulla taglia : [B] ; Divide: dà taglie ; Conquer: massimo di un'unione = massimo dei massimi.
- Ricorrenza (solo i confronti tra elementi costano): , . Senza arrotondamenti serve (non basta pari). Srotolando: , , quindi ; verifica per induzione sul parametro .
- Albero della ricorrenza: nodo = taglia, accanto il lavoro (Divide+Conquer); = somma dei lavori. In MAX: nodi interni con figli e lavoro , foglie a lavoro .
- Teorema: un albero binario pieno (ogni interno ha figli) con foglie ha nodi interni; induzione togliendo i due figli-foglia di un interno di livello massimo. Quindi per ogni .
- Errori tipici: scrivere per qualsiasi; confondere albero pieno e completo; tenere come parametro dell'induzione senza dire che è ; credere che il lavoro dipenda solo dalla taglia (MERGESORT dipende dall'istanza).
- Metacodice:
if |i| <= n0 then risolvi direttamente; return/<i_1..i_k> <- A_D(i)/for j <- 1 to k do s_j <- D&C(i_j)/return A_C(s_1..s_k). La correttezza della proprietà implica quella dell'algoritmo; le frasi tra asterischi*...*si usano il meno possibile. - Proprietà dell'albero: arietà uguale per tutti i nodi interni negli algoritmi del corso; foglie con taglia ; la ricorsione usa lo stack e visita l'albero in preordine, contenendo la memoria; la mancanza di memoria è la debolezza del D&CSe la stessa sottoistanza compare in più punti dell'albero, il suo sottoalbero viene espanso e contratto più volte: per Fibonacci il costo diventa esponenziale..
- Passi della correttezza di MAX: [B] , restituisce ; [HP] corretto per taglie ; [TH] Divide: e perché ; Conquer: , .
- Srotolamento (): , si ferma a () con , quindi . Albero di : 3 nodi interni di lavoro , 4 foglie di lavoro , ; per albero sbilanciato ( e ).
- Ricorrenza di MAX: costa solo il confronto tra elementi; per ogni , non (che vale solo per : non basta pari, ).
- Lavoro dipendente dall'istanza: merge sort fusione ordinata, caso peggiore confronti (Algoritmi di ordinamentoSelection sort e insertion sort (quadratici, con invarianti), merge sort (divide et impera, Theta(n log n)), quick sort (caso pessimo quadratico, medio n log n), heap sort (in loco, Theta(n log n)), ordinamento senza confronti per chiavi intere in un intervallo piccolo, limite inferiore Omega(n log n) per gli algoritmi basati su confronti.Algoritmi di ordinamento →).
Esercizi su questo argomento
- Esercizio - domande di prima parte su problemi computazionali, ricorrenze e conteggi (simulazioni d'esame)
- Esercizio - due elementi più piccoli di una stringa con divide and conquer (simulazione d'esame 2023-24, problema 1)
- Esercizio - funzione g(n) definita per ricorrenza, calcolo memoizzato e formula chiusa (compito del 19 giugno 2024, es. 2)
- Esercizio - ricorrenza con sottoistanze di taglia metà e un quarto, forma chiusa e profondità delle foglie (compito del 5 settembre 2025, es. 1)
Teoria collegata
- Albero della ricorrenza e formula generale per le ricorrenze
- Codifica di Huffman - codici prefissi, algoritmo e ottimalità
- Fast Fourier Transform - algoritmo ricorsivo e trasformata inversa
- Induzione parametrica - limitazioni esatte delle ricorrenze
- Master Theorem - enunciato, casi e applicazioni
- Paradigma greedy - scelta golosa, sottostruttura e selezione delle attività
- Problemi computazionali - istanze, soluzioni, correttezza e complessità
- Prodotto di matrici - algoritmo iterativo e ricorsivo a blocchi
- Programmazione dinamica - Fibonacci, memoizzazione e schema del paradigma