Paradigma greedy - scelta golosa, sottostruttura e selezione delle attività
In questa pagina 4
Terzo paradigma del corso dopo il divide and conquer (Divide and conquer - proprietà di sottostruttura e albero delle chiamateUn algoritmo divide and conquer nasce da una proprietà di sottostruttura (funzioni Divide e Conquer, base n0) con sottoistanze di taglia strettamente minore; l'albero delle chiamate ne descrive l'esecuzione, e il D&C non ha memoria (ripete le sottoistanze uguali). Esempio completo: MAX di un array, con correttezza per induzione, ricorrenza e conteggio esatto n-1 tramite il teorema sugli alberi binari pieni.Divide and conquer - proprietà di sottostruttura e albero delle chiamate →) e la 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 →). Vale per i problemi di ottimizzazione (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à →): tra le soluzioni ammissibili di un'istanza se ne cerca una di costo minimo o massimo.
Dalla programmazione dinamica al greedy
In un algoritmo di DP la soluzione ottima si ottiene componendo una sequenza di scelte, e le informazioni salvate per ricostruirla (l'indice in per la catena di matrici, la freccia scelta nella tabella della LCS) sono proprio le scelte. Ma ogni scelta è meditata: si può fare solo dopo aver risolto molte sottoistanze (tutti i possibili, tutte e tre le sottoistanze della LCS). È la cautela che rende la DP applicabile a tanti problemi, ma ha un costo (una tabella di sottoistanze e un lavoro per cella).
Per alcuni problemi si può rovesciare l'approccio:
- Scelta greedy (golosa, "incauta"): si fa subito la prima scelta, quella che localmente sembra la migliore, senza risolvere nessuna sottoistanza.
- Clean-up: si ripulisce l'istanza, togliendo gli elementi incompatibili con la scelta fatta. Quello che resta è un'unica sottoistanza, la sottoistanza residua, che si risolve con lo stesso procedimento.
È il modo "umano" di risolvere un problema: si costruisce la soluzione a passi, scegliendo ogni volta ciò che conviene sul momento.
Conseguenze.
- Ogni istanza ha una sola sottoistanza, quindi lo spazio delle sottoistanze ha taglia al più lineare e non esistono sottoistanze ripetute: la memoizzazione è inutile.
- L'unica chiamata ricorsiva è l'ultima istruzione dopo il clean-up: è una ricorsione di coda, che si elimina con un ciclo in cui si alternano scelta greedy e clean-up (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 →). Gli algoritmi risultanti sono iterativi, semplici e veloci, spesso lineari o quasi lineari, basati su una scansione dell'istanza.
- Lo svantaggio è che non sempre funziona: costruire la soluzione per scelte locali non converge, in generale, all'ottimo. La classe dei problemi risolubili così è piccola, ma contiene casi importanti: l'albero di copertura minimo (Minimum spanning treeMinimum spanning tree di un grafo pesato connesso; proprietà del taglio (cut property) con dimostrazione; algoritmo di Kruskal con partizioni (union-find) e algoritmo di Prim con coda con priorità; esempio svolto sullo stesso grafo di Dijkstra; complessità O(m log n).Minimum spanning tree →), i cammini minimi (Cammini minimi e algoritmo di DijkstraGrafi pesati, lunghezza di un cammino e distanza; sottocammini di un cammino minimo; problema SSSP; algoritmo di Dijkstra con cloud e priority queue, rilassamento degli archi, esempio svolto; correttezza (due lemmi) e complessità O(min(n^2, (n+m) log n)) con lista non ordinata o heap; pesi non negativi.Cammini minimi e algoritmo di Dijkstra →) e la compressione ottima (Codifica di Huffman - codici prefissi, algoritmo e ottimalitàCompressione con codifica per carattere: la lunghezza variabile risparmia bit ma deve essere iniettiva, e i codici prefissi lo garantiscono; un codice prefisso è un trie binario con una foglia per carattere, di costo B(T) = somma di f(c) d_T(c), e quello ottimo è un albero pieno con n-1 nodi interni. L'algoritmo di Huffman fonde ripetutamente i due alberi di frequenza minima usando una coda con priorità (2n-1 estrazioni, costo O(n log n)); l'ottimalità si prova con la scelta greedy (i due caratteri più rari sono foglie sorelle di profondità massima, per scambio) e la sottostruttura (B(T) = B(T') + f(x) + f(y)). Esempio completo con 6 caratteri, B = 2,24.Codifica di Huffman - codici prefissi, algoritmo e ottimalità →).
Il codice viene prima dell'analisi. In D&C e DP si studia la sottostruttura e poi si scrive l'algoritmo; qui il codice discende direttamente dalla definizione di scelta greedy e di clean-up, ed è facile. Il lavoro difficile è dimostrare che è corretto, e ci sono scelte greedy altrettanto naturali che producono soluzioni sbagliate.
Le due proprietà da dimostrare
Proprietà di scelta greedy. Esiste sempre una soluzione ottima che contiene la scelta greedy. Garantisce che la scelta incauta non compromette l'ottimalità.
Proprietà di sottostruttura ottima. Esiste una soluzione ottima che, oltre alla scelta greedy, contiene una soluzione ottima della sottoistanza residua.
Le tecniche tipiche:
- Scelta greedy: si parte da una soluzione ottima qualunque ; se non contiene la scelta greedy la si modifica (scambio, o cut-and-paste: si toglie un pezzo e se ne incolla un altro) in che la contiene, senza peggiorare il costo. Allora è ancora ottima.
- Sottostruttura: per assurdo. Se la parte di che resta dopo la scelta greedy non fosse ottima per la residua, si sostituirebbe con una ottima (cut-and-paste) ottenendo una soluzione migliore di per l'istanza di partenza: contraddizione.
Teorema (schema di correttezza). Se un algoritmo greedy compie ogni volta la scelta greedy, fa il clean-up e risolve ricorsivamente la residua, e valgono le due proprietà, allora ritorna una soluzione ottima.
Dimostrazione. Per induzione sulla taglia dell'istanza. Base: istanza vuota, la soluzione vuota è ottima. Passo: si assume l'algoritmo corretto sulle istanze più piccole, in particolare sulla residua, che è strettamente più piccola. Per le due proprietà esiste un ottimo composto dalla scelta greedy e da una soluzione ottima della residua. L'algoritmo ritorna la scelta greedy più la soluzione che l'ipotesi induttiva garantisce ottima per la residua: ha lo stesso costo di , quindi è ottima.
Il ragionamento induttivo è sempre lo stesso: di problema in problema cambiano solo le due proprietà. Per questo negli esercizi (Esercitazione greedy - tracce su CD, scheduling shortest-first e (2,1)-boxingCome si imposta e si dimostra un esercizio greedy (scelta greedy, clean-up, algoritmo, proprietà di scelta greedy per scambio, sottostruttura ottima per assurdo, induzione), applicato a tre problemi con clean-up vuoto: tracce su CD (riempire il primo CD, Theta(n)), shortest-first scheduling (ordinare per durata, SJF vale per ogni ottimo, O(n log n)), (2,1)-boxing (accoppiare il più pesante con il più leggero, Theta(n)); tabella di confronto, controesempi e errori tipici.Esercitazione greedy - tracce su CD, scheduling shortest-first e (2,1)-boxing →) ci si limita a provare quelle.
Il controesempio. Per mostrare che una strategia greedy è sbagliata basta un'istanza in cui la scelta greedy esclude ogni ottimo: è molto più facile che dimostrare la correttezza.
Il problema della selezione delle attività
Una risorsa condivisa (un'aula) e un insieme di attività che la vogliono usare. L'attività occupa l'intervallo con (inizio incluso, fine esclusa).
Definizione. Due attività e sono compatibili se gli intervalli sono disgiunti, , cioè se Un sottoinsieme è mutuamente compatibile se lo sono tutte le coppie. Il problema (activity selection) chiede un sottoinsieme mutuamente compatibile di cardinalità massima.
Poiché gli intervalli sono aperti a destra, un'attività che finisce e una che inizia nello stesso istante sono compatibili.
Esempio. , , cioè , , , , , . In rosso le attività scelte dall'algoritmo che segue (si trovano più sotto).
Grafico interattivo: Le sei attività: in rosso quelle scelte dal greedy del primo che finisce, a1, a4 e a6
L'algoritmo greedy
Si suppone ordinate per fine non decrescente, ; se non lo sono si riordinano in (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 →). L'ordine è cruciale per la correttezza.
- Scelta greedy: la prima attività dell'ordine, , cioè quella che finisce per prima. Intuizione: lascia libera la risorsa per l'intervallo contiguo più lungo possibile.
- Clean-up: si eliminano le attività che seguono nell'ordine e sono incompatibili con , cioè quelle con , fino alla prima compatibile , con . La residua è il suffisso (vuoto se nessuna è compatibile). Le istanze residue sono sempre suffissi, individuati da un indice : l'istanza principale è e quella vuota è .
Il codice segue subito:
REC_GREEDY_SEL(s, f, i) {attivita' ordinate per fine}
n <- s.len
if i > n then return {}
G <- {i}
j <- i + 1
while j <= n and s[j] < f[i] do // clean-up: scarta le incompatibili
j <- j + 1
return G U REC_GREEDY_SEL(s, f, j)Nella condizione del while si controlla prima j <= n: con l'ordine opposto si leggerebbe s[n+1] fuori dal vettore. Si chiama con . La ricorsione è di coda, quindi si elimina. Ora basta ricordare l'ultima attività scelta, , e confrontare ogni attività con lei:
IT_GREEDY_SEL(s, f)
n <- s.len
A <- {1}
g <- 1 // ultima scelta greedy
for i <- 2 to n do
if s[i] >= f[g] then // compatibile con l'ultima scelta
A <- A U {i}
g <- i
return Adef it_greedy_sel(s, f): # indici da 1 come nel testo; s, f gia' ordinati per fine
A, g = [1], 1
for i in range(2, len(s) + 1):
if s[i - 1] >= f[g - 1]:
A.append(i); g = i
return AComplessità. La scansione fa esattamente confronti (, uno per ciascun ) e al più inserimenti: . L'ordinamento per fine costa e domina se i dati non sono già ordinati.
Esempio eseguito a mano
Stessa istanza di prima, con e .
| confronto | esito | ||||
|---|---|---|---|---|---|
| 1 | scelta iniziale | scelta | 1 () | ||
| 2 | no | scartata | 1 | ||
| 3 | no | scartata | 1 | ||
| 4 | sì | scelta | 4 () | ||
| 5 | no | scartata | 4 | ||
| 6 | sì | scelta | 6 |
Il clean-up dopo la scelta di elimina e (che iniziano prima di ): la prima attività della sottoistanza residua è , perché (la fine di e l'inizio di coincidono e questo è permesso). Il risultato ha 3 attività, e 3 è il massimo (anche è ottima: si verifica con la forza bruta sui sottoinsiemi).
Perché il clean-up può essere limitato. Il clean-up elimina solo le incompatibili che seguono immediatamente , non tutte quelle incompatibili con . Questo non compromette la correttezza: un'attività incompatibile non eliminata lo sarà con qualche scelta greedy successiva (per transitività sull'ordine per fine) e verrà scartata dopo. Esempio con : dopo la scelta di il clean-up toglie () e si ferma ad ; la residua contiene ancora , incompatibile con . Ma viene scelta e scarta (), e l'algoritmo termina con , ottima. L'effetto è un algoritmo con una sola scansione lineare.
Correttezza
Notazione: (suffisso), (con se non esiste), la cardinalità massima di un sottoinsieme mutuamente compatibile di , con .
Proprietà di scelta greedy. In esiste una soluzione ottima che contiene .
Dimostrazione (scambio). Sia una soluzione ottima di . Se basta porre . Altrimenti sia l'attività di di indice minimo; poiché è . Si pone Resta da mostrare che è ammissibile, cioè che è compatibile con ogni . Poiché e stanno insieme in sono compatibili: oppure . La seconda è impossibile: darebbe (l'ultima disuguaglianza perché e l'ordine è per fine). Dunque , e per l'ordine cioè inizia dopo la fine di : compatibile.
Proprietà di sottostruttura ottima. Vale .
Dimostrazione. () Per la scelta greedy esiste un ottimo di con . Ogni è compatibile con e ha , quindi (come sopra: l'altra alternativa darebbe ). Un'attività con ha e perciò per definizione di minimo: , ed è compatibile, quindi .
() Sia un ottimo di , con (se è vuota non c'è nulla da dimostrare). Non ogni soluzione di va bene: può contenere attività incompatibili con (come nell'esempio sopra). Si sceglie allora, per la scelta greedy applicata a , un ottimo che contiene . Ogni altro è compatibile con e ha , quindi ; anche ha per definizione di . Allora è ammissibile e .
Teorema. ritorna una soluzione ammissibile di di cardinalità .
Dimostrazione. Induzione su , la taglia di . Base: , ritorna , con . Passo: l'algoritmo ritorna con (il ciclo del clean-up calcola esattamente ). Per ipotesi induttiva è ammissibile in e ha elementi; poiché è il suo primo elemento e le altre iniziano dopo (come costruito), tutte iniziano dopo e è ammissibile. La sua cardinalità è per la sottostruttura.
Con si ottiene l'ottimo dell'istanza principale. L'algoritmo iterativo calcola gli stessi indici (confronta ogni attività con l'ultima scelta, e per transitività con tutte le precedenti, perché le fini sono crescenti).
Scelte greedy che non funzionano
Altre scelte sembrano ragionevoli ma sono sbagliate; per ciascuna basta un controesempio (verificati con la forza bruta).
| Strategia | Istanza | Cosa succede |
|---|---|---|
| minima durata | sceglie (durata 2) che è incompatibile con entrambe le altre: 1 attività, mentre ne ha 2 | |
| inizio più presto | sceglie e blocca tutto: 1 contro 2 | |
| minimo numero di incompatibilità | 11 attività: , , , , , , , , , , | ha 2 incompatibilità (con e ), tutte le altre almeno 3: la scelta cade su e si ottengono 3 attività al massimo; l'ottimo è , con 4 |
La simmetrica funziona: se le attività sono ordinate per inizio non crescente e la scelta greedy è quella che inizia per ultima, con lo stesso tipo di clean-up (si scartano quelle con dell'ultima scelta), la dimostrazione è lo specchio di quella sopra (tempo che scorre al contrario). Anche questa è verificata con la forza bruta.
Attenzione al peso. Se ogni attività ha un peso e si vuole massimizzare la somma dei pesi, il greedy per fine non basta (le istanze con peso 100 e con peso 1 lo mostrano): serve la programmazione dinamica.
Domande d'esame
- Nel problema della selezione delle attività con e , qual è l'indice della prima attività della sottoistanza residua dopo la prima scelta greedy e il relativo clean-up? Risposta: scelta greedy ; () e () vengono eliminate, ha : l'indice è 4.
- Quali due proprietà garantiscono la correttezza di un algoritmo greedy e con quali tecniche si dimostrano? Traccia: scelta greedy (esiste un ottimo che la contiene: partendo da un ottimo si fa lo scambio senza peggiorare il costo) e sottostruttura ottima (l'ottimo contiene un ottimo della residua: per assurdo con il cut-and-paste); la correttezza segue per induzione sulla taglia.
- Perché nell'algoritmo delle attività l'ordinamento per fine è cruciale? Dare un controesempio per la scelta "minima durata". Traccia: lo scambio usa , che richiede l'ordine per fine; controesempio .
Versione ripasso
- Dalla DP al greedy (confronto con 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 →): in DP ogni scelta è meditata (si risolvono prima molte sottoistanze); nel greedy si fa subito la scelta localmente migliore, poi si ripulisce l'istanza (clean-up) e resta una sola sottoistanza residua, risolta allo stesso modo.
- Conseguenze: spazio delle sottoistanze lineare, nessuna sottoistanza ripetuta (la memoizzazione non serve); la ricorsione è di coda e diventa un ciclo (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 →); algoritmi semplici e veloci, spesso lineari. Ma non sempre funziona: casi buoni sono Minimum spanning treeMinimum spanning tree di un grafo pesato connesso; proprietà del taglio (cut property) con dimostrazione; algoritmo di Kruskal con partizioni (union-find) e algoritmo di Prim con coda con priorità; esempio svolto sullo stesso grafo di Dijkstra; complessità O(m log n).Minimum spanning tree →, Cammini minimi e algoritmo di DijkstraGrafi pesati, lunghezza di un cammino e distanza; sottocammini di un cammino minimo; problema SSSP; algoritmo di Dijkstra con cloud e priority queue, rilassamento degli archi, esempio svolto; correttezza (due lemmi) e complessità O(min(n^2, (n+m) log n)) con lista non ordinata o heap; pesi non negativi.Cammini minimi e algoritmo di Dijkstra →, Codifica di Huffman - codici prefissi, algoritmo e ottimalitàCompressione con codifica per carattere: la lunghezza variabile risparmia bit ma deve essere iniettiva, e i codici prefissi lo garantiscono; un codice prefisso è un trie binario con una foglia per carattere, di costo B(T) = somma di f(c) d_T(c), e quello ottimo è un albero pieno con n-1 nodi interni. L'algoritmo di Huffman fonde ripetutamente i due alberi di frequenza minima usando una coda con priorità (2n-1 estrazioni, costo O(n log n)); l'ottimalità si prova con la scelta greedy (i due caratteri più rari sono foglie sorelle di profondità massima, per scambio) e la sottostruttura (B(T) = B(T') + f(x) + f(y)). Esempio completo con 6 caratteri, B = 2,24.Codifica di Huffman - codici prefissi, algoritmo e ottimalità →.
- Il codice viene prima della prova. Per la correttezza servono due proprietà: (1) scelta greedy, esiste un ottimo che contiene la scelta greedy (si parte da un ottimo , lo si modifica per scambio in senza peggiorare il costo); (2) sottostruttura ottima, esiste un ottimo che oltre alla scelta greedy contiene un ottimo della residua (assurdo + cut-and-paste).
- Schema di correttezza: induzione sulla taglia; base istanza vuota; passo: per (1) e (2) esiste un ottimo fatto da scelta greedy più ottimo della residua, e l'algoritmo ritorna scelta greedy più la soluzione della residua, ottima per ipotesi induttiva, quindi di pari costo. Il ragionamento è sempre lo stesso: si provano solo (1) e (2). Per escludere una strategia basta un controesempio.
- Selezione delle attività: , ; compatibili se o (fine e inizio coincidenti vanno bene). Si cerca un sottoinsieme mutuamente compatibile di cardinalità massima. Si ordina per fine non decrescente (, 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 →).
- Scelta e clean-up: scelta greedy (finisce per prima); clean-up elimina i successivi con fino a ; la residua è il suffisso . Il clean-up è limitato: un'incompatibile non eliminata (es. in ) viene scartata da una scelta successiva.
- Codice iterativo:
A <- {1}; g <- 1; for i <- 2 to n: if s[i] >= f[g] then A <- A U {i}; g <- i. Esattamente confronti, . InREC_GREEDY_SELilwhilecontrolla primaj <= n. - Esempio: , : scartate (), scelta (), scartata (), scelta (): , ottimo. La residua dopo parte da .
- Scelta greedy (prova): ottima, di indice minimo, . Per ogni altra : l'alternativa darebbe , impossibile, quindi e : è ammissibile e ha la stessa cardinalità.
- Sottostruttura (prova): . : un ottimo ha , perché implica e quindi . : si prende un ottimo di che contiene (non uno qualunque, potrebbe avere attività incompatibili con ); le altre iniziano dopo , quindi è ammissibile.
- Strategie sbagliate (controesempi): minima durata (: 1 contro 2), inizio più presto (), minimo numero di incompatibilità (11 attività: ottimo 4, il greedy ne ottiene 3). La versione simmetrica (ordine per inizio non crescente) è corretta. Con pesi il greedy non basta: serve la DP.
- Errori tipici: non ordinare per fine; usare (escludendo la compatibilità in un punto); prendere come ottimo della residua una soluzione qualunque senza imporre la scelta greedy della residua; dimenticare la verifica di ammissibilità dello scambio; contare male i confronti (, non ).
Esercizi su questo argomento
- Esercizio - (2,1)-boxing di oggetti, scelta greedy e ottimalità (esercitazione greedy, es. 10.3)
- Esercizio - domande di prima parte su greedy e Huffman (simulazioni d'esame)
- Esercizio - masterizzazione compatta di tracce su CD, scelta greedy e ottimalità (esercitazione greedy, es. 10.1)
- Esercizio - shortest-first job scheduling, proprietà SJF e sottostruttura (esercitazione greedy, es. 10.2)