Salta al contenuto
Note per Studenti Paradigma greedy - scelta golosa, sottostruttura e selezione delle attività

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 k⋆k^\star in s[1,n]s[1,n] 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 kk 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:

  1. Scelta greedy (golosa, "incauta"): si fa subito la prima scelta, quella che localmente sembra la migliore, senza risolvere nessuna sottoistanza.
  2. 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.

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 A^\hat A; 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 A⋆A^\star che la contiene, senza peggiorare il costo. Allora A⋆A^\star è ancora ottima.
  • Sottostruttura: per assurdo. Se la parte di A⋆A^\star 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 A⋆A^\star 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 A⋆A^\star 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 A⋆A^\star, quindi è ottima. □\square

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à S={a1,…,an}S = \{a_1, \dots, a_n\} che la vogliono usare. L'attività aja_j occupa l'intervallo [sj,fj)[s_j, f_j) con sj<fjs_j < f_j (inizio incluso, fine esclusa).

Definizione. Due attività aia_i e aja_j sono compatibili se gli intervalli sono disgiunti, [si,fi)∩[sj,fj)=∅[s_i, f_i) \cap [s_j, f_j) = \emptyset, cioè se sj≥fioppuresi≥fj.s_j \ge f_i \quad\text{oppure}\quad s_i \ge f_j . 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. s=(2,1,2,3,5,7)s = (2,1,2,3,5,7), f=(3,5,6,6,7,8)f = (3,5,6,6,7,8), cioè a1=[2,3)a_1 = [2,3), a2=[1,5)a_2 = [1,5), a3=[2,6)a_3 = [2,6), a4=[3,6)a_4 = [3,6), a5=[5,7)a_5 = [5,7), a6=[7,8)a_6 = [7,8). 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 a1,…,ana_1, \dots, a_n ordinate per fine non decrescente, f1≤f2≤⋯≤fnf_1 \le f_2 \le \dots \le f_n; se non lo sono si riordinano in O(nlog⁡n)O(n \log n) (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, a1a_1, 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 a1a_1, cioè quelle con sj<f1s_j < f_1, fino alla prima compatibile aga_g, con g=min⁡{j:sj≥f1}g = \min\{j : s_j \ge f_1\}. La residua è il suffisso ag,…,ana_g, \dots, a_n (vuoto se nessuna è compatibile). Le istanze residue sono sempre suffissi, individuati da un indice ii: l'istanza principale è i=1i = 1 e quella vuota è i=n+1i = n+1.

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 i=1i = 1. La ricorsione è di coda, quindi si elimina. Ora basta ricordare l'ultima attività scelta, gg, 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 A
python
def 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 A

Complessità. La scansione fa esattamente n−1n-1 confronti (si≥fgs_i \ge f_g, uno per ciascun i=2,…,ni = 2, \dots, n) e al più n−1n-1 inserimenti: Θ(n)\Theta(n). L'ordinamento per fine costa O(nlog⁡n)O(n \log n) e domina se i dati non sono già ordinati.

Esempio eseguito a mano

Stessa istanza di prima, con s=(2,1,2,3,5,7)s = (2,1,2,3,5,7) e f=(3,5,6,6,7,8)f = (3,5,6,6,7,8).

ii aia_i confronto si≥fgs_i \ge f_g esito AA gg
1 [2,3)[2,3) scelta iniziale scelta {1}\{1\} 1 (fg=3f_g = 3)
2 [1,5)[1,5) 1≥31 \ge 3 no scartata {1}\{1\} 1
3 [2,6)[2,6) 2≥32 \ge 3 no scartata {1}\{1\} 1
4 [3,6)[3,6) 3≥33 \ge 3 sì scelta {1,4}\{1,4\} 4 (fg=6f_g = 6)
5 [5,7)[5,7) 5≥65 \ge 6 no scartata {1,4}\{1,4\} 4
6 [7,8)[7,8) 7≥67 \ge 6 sì scelta {1,4,6}\{1,4,6\} 6

Il clean-up dopo la scelta di a1a_1 elimina a2a_2 e a3a_3 (che iniziano prima di f1=3f_1 = 3): la prima attività della sottoistanza residua è a4a_4, perché s4=3≥f1=3s_4 = 3 \ge f_1 = 3 (la fine di a1a_1 e l'inizio di a4a_4 coincidono e questo è permesso). Il risultato ha 3 attività, e 3 è il massimo (anche {a1,a5,a6}\{a_1, a_5, a_6\} è 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 a1a_1, non tutte quelle incompatibili con a1a_1. 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 (3,5),(4,6),(5,7),(4,8),(7,9)(3,5), (4,6), (5,7), (4,8), (7,9): dopo la scelta di a1=[3,5)a_1 = [3,5) il clean-up toglie a2a_2 (4<54 < 5) e si ferma ad a3=[5,7)a_3 = [5,7); la residua {a3,a4,a5}\{a_3, a_4, a_5\} contiene ancora a4=[4,8)a_4 = [4,8), incompatibile con a1a_1. Ma a3a_3 viene scelta e scarta a4a_4 (4<74 < 7), e l'algoritmo termina con {a1,a3,a5}\{a_1, a_3, a_5\}, ottima. L'effetto è un algoritmo con una sola scansione lineare.

Correttezza

Notazione: Si={ai,…,an}S_i = \{a_i, \dots, a_n\} (suffisso), g(i)=min⁡{j>i:sj≥fi}g(i) = \min\{j > i : s_j \ge f_i\} (con Sg(i)=∅S_{g(i)} = \emptyset se non esiste), opt(i)\mathrm{opt}(i) la cardinalità massima di un sottoinsieme mutuamente compatibile di SiS_i, con opt(n+1)=0\mathrm{opt}(n+1) = 0.

Proprietà di scelta greedy. In SiS_i esiste una soluzione ottima che contiene aia_i.

Dimostrazione (scambio). Sia A^\hat A una soluzione ottima di SiS_i. Se ai∈A^a_i \in \hat A basta porre A⋆=A^A^\star = \hat A. Altrimenti sia aha_h l'attività di A^\hat A di indice minimo; poiché ai∉A^a_i \notin \hat A è h>ih > i. Si pone A⋆=(A^∖{ah})∪{ai},∣A⋆∣=∣A^∣.A^\star = (\hat A \setminus \{a_h\}) \cup \{a_i\}, \qquad |A^\star| = |\hat A| . Resta da mostrare che A⋆A^\star è ammissibile, cioè che aia_i è compatibile con ogni aj∈A^∖{ah}a_j \in \hat A \setminus \{a_h\}. Poiché aja_j e aha_h stanno insieme in A^\hat A sono compatibili: sj≥fhs_j \ge f_h oppure sh≥fjs_h \ge f_j. La seconda è impossibile: darebbe fj≤sh<fh≤fjf_j \le s_h < f_h \le f_j (l'ultima disuguaglianza perché j>hj > h e l'ordine è per fine). Dunque sj≥fhs_j \ge f_h, e per l'ordine fi≤fh≤sj,f_i \le f_h \le s_j , cioè aja_j inizia dopo la fine di aia_i: compatibile. □\square

Proprietà di sottostruttura ottima. Vale opt(i)=1+opt(g(i))\mathrm{opt}(i) = 1 + \mathrm{opt}(g(i)).

Dimostrazione. (≤\le) Per la scelta greedy esiste un ottimo AA di SiS_i con ai∈Aa_i \in A. Ogni aj∈A∖{ai}a_j \in A \setminus \{a_i\} è compatibile con aia_i e ha fj≥fif_j \ge f_i, quindi sj≥fis_j \ge f_i (come sopra: l'altra alternativa si≥fjs_i \ge f_j darebbe fj≤si<fif_j \le s_i < f_i). Un'attività con sj≥fis_j \ge f_i ha j>ij > i e perciò j≥g(i)j \ge g(i) per definizione di minimo: A∖{ai}⊆Sg(i)A \setminus \{a_i\} \subseteq S_{g(i)}, ed è compatibile, quindi ∣A∣−1≤opt(g(i))|A| - 1 \le \mathrm{opt}(g(i)).

(≥\ge) Sia BB un ottimo di SgS_{g}, con g=g(i)g = g(i) (se è vuota non c'è nulla da dimostrare). Non ogni soluzione di SgS_g va bene: può contenere attività incompatibili con aia_i (come a4a_4 nell'esempio sopra). Si sceglie allora, per la scelta greedy applicata a SgS_g, un ottimo BB che contiene aga_g. Ogni altro aj∈Ba_j \in B è compatibile con aga_g e ha j>gj > g, quindi sj≥fg>sg≥fis_j \ge f_g > s_g \ge f_i; anche aga_g ha sg≥fis_g \ge f_i per definizione di gg. Allora {ai}∪B\{a_i\} \cup B è ammissibile e opt(i)≥1+∣B∣=1+opt(g)\mathrm{opt}(i) \ge 1 + |B| = 1 + \mathrm{opt}(g). □\square

Teorema. REC_GREEDY_SEL(s,f,i)\mathrm{REC\_GREEDY\_SEL}(s, f, i) ritorna una soluzione ammissibile di SiS_i di cardinalità opt(i)\mathrm{opt}(i).

Dimostrazione. Induzione su n−i+1n - i + 1, la taglia di SiS_i. Base: i>ni > n, ritorna ∅\emptyset, con opt=0\mathrm{opt} = 0. Passo: l'algoritmo ritorna {ai}∪R\{a_i\} \cup R con R=REC_GREEDY_SEL(s,f,g(i))R = \mathrm{REC\_GREEDY\_SEL}(s, f, g(i)) (il ciclo del clean-up calcola esattamente g(i)g(i)). Per ipotesi induttiva RR è ammissibile in Sg(i)S_{g(i)} e ha opt(g(i))\mathrm{opt}(g(i)) elementi; poiché aga_g è il suo primo elemento e le altre iniziano dopo fgf_g (come costruito), tutte iniziano dopo fif_i e {ai}∪R\{a_i\} \cup R è ammissibile. La sua cardinalità è 1+opt(g(i))=opt(i)1 + \mathrm{opt}(g(i)) = \mathrm{opt}(i) per la sottostruttura. □\square

Con i=1i = 1 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 [0,5),[4,6),[5,10)[0,5), [4,6), [5,10) sceglie [4,6)[4,6) (durata 2) che è incompatibile con entrambe le altre: 1 attività, mentre {[0,5),[5,10)}\{[0,5),[5,10)\} ne ha 2
inizio più presto [0,10),[1,2),[3,4)[0,10), [1,2), [3,4) sceglie [0,10)[0,10) e blocca tutto: 1 contro 2
minimo numero di incompatibilità 11 attività: A [0,4)A\,[0,4), B [5,9)B\,[5,9), C [10,14)C\,[10,14), D [15,19)D\,[15,19), S [8,11)S\,[8,11), L1 [2,6)L_1\,[2,6), L2 [3,7)L_2\,[3,7), L3 [3,6)L_3\,[3,6), R1 [12,16)R_1\,[12,16), R2 [13,17)R_2\,[13,17), R3 [12,17)R_3\,[12,17) SS ha 2 incompatibilità (con BB e CC), tutte le altre almeno 3: la scelta cade su SS e si ottengono 3 attività al massimo; l'ottimo è {A,B,C,D}\{A, B, C, D\}, 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 f>sf > s 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 [0,10)[0,10) con peso 100 e [0,5),[5,10)[0,5), [5,10) con peso 1 lo mostrano): serve la programmazione dinamica.

Domande d'esame

  1. Nel problema della selezione delle attività con s=(2,1,2,3,5,7)s = (2,1,2,3,5,7) e f=(3,5,6,6,7,8)f = (3,5,6,6,7,8), qual è l'indice della prima attività della sottoistanza residua dopo la prima scelta greedy e il relativo clean-up? Risposta: scelta greedy a1=[2,3)a_1 = [2,3); a2a_2 (1<31 < 3) e a3a_3 (2<32 < 3) vengono eliminate, a4a_4 ha s4=3≥f1=3s_4 = 3 \ge f_1 = 3: l'indice è 4.
  2. 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.
  3. Perché nell'algoritmo delle attività l'ordinamento per fine è cruciale? Dare un controesempio per la scelta "minima durata". Traccia: lo scambio usa fi≤fh≤sjf_i \le f_h \le s_j, che richiede l'ordine per fine; controesempio [0,5),[4,6),[5,10)[0,5), [4,6), [5,10).

Versione ripasso

Esercizi su questo argomento

Teoria collegata