Progetto di algoritmi sequenziali (un solo “esecutore”) • Divide et Impera “Per regnare occorre tenere divisi i nemici e trarne vantaggio” • Greedy “fai ad ogni passo la scelta più conveniente” Buoni risultati immediati possono dare buoni risultati anche a lungo termine • Backtrack “Prova a fare qualche cosa; se non funziona, disfala e prova qualcos’altro” • Programmazione dinamica Filosofia Divide et Impera portata all’estremo: se non si sa quali sottoproblemi risolvere, si può provare a risolverli tutti e conservare i risultati per poterli usare successivamente per risolvere il problema di dimensioni più grandi “Pianifica il metodo di soluzione del problema in modo che sia applicabile dinamicamente a dati di dimensione via via crescente” • Ricerca locale “Non guardare più in là del proprio orto” Algoritmi di approssimazione Per molti problemi di ottimizzazione c’è poca speranza di progettare un algoritmo che trovi la soluzione ottima con complessità polinomiale. In tali casi, comunque, ci si può spesso accontentare di una soluzione che sia “abbastanza vicina” a quella ottima. Algoritmi probabilistici La computazione dipende dai valori prodotti da un generatore di numeri casuali Algoritmi euristici Forniscono una soluzione ammissibile, non necessariamente ottima nè approssimata. Spesso basati su tecniche greedy o di ricerca locale Algoritmi paralleli (più di un “esecutore”) Divide et impera (Divide and Conquer) Dividi il problema in sottoproblemi piu` semplici e risolvili ricorsivamente Divide et impera - Schema generale Divide-et-impera (P, n) if n ≤ k then “risolvi direttamente” else “dividi P in sottoproblemi P1, P2,…,Ph di dimensione n1, n2,…, nh risp.” for i ← 1 to h do Divide-et-impera (Pi, ni) “combina i risultati di P 1,…, Ph per ottenere quello di P” Divide et impera - Complessità Il costo dell’algoritmo è di solito costante per i casi in cui la soluzione viene costruita direttamente; per i casi in cui non è immediata, il costo può essere espresso come somma di h + 2 costi: • il costo di dividere il problema nei sottoproblemi • il costo di combinare i risultati dei sottoproblemi per ottenere il risultato del problema • gli h costi delle soluzioni dei sottoproblemi T(n) = c = D(n) + C(n) + Σi=1..h T(ni) n≤k n>k Problema: è relativamente semplice esprimere la complessità con una relazione di ricorrenza, è però piuttosto complicato risolvere la relazione di ricorrenza per trovare la complessità asintotica. Ci sono tre modi per trovare la soluzione: • metodo di sostituzione • metodo iterativo • metodo principale Il metodo di sostituzione • Ipotizza un limite asintotico • verifica l’ipotesi con una dimostrazione per induzione esempio: T(n) = 4T(n/2) + n possiamo provare T(n) = O(n3) mostriamo che esistono c ed n0 : T(n) ≤ c n3 per tutti gli n ≥ n0 Supponendo T(k) ≤ c k3 per tutti i k < n, si ottiene T(n) = 4T(n/2) + n ≤ 4c(n/2)3 + n = cn3 - (c/2 n3 - n) ≤ cn3 per c ≥ 2 e n ≥ 1 Anche T(1) deve essere ≤ c 13 un tentativo migliore: T(n) = O(n2) Cerchiamo di dimostrare che T(n) ≤ c n2 per tutti gli n ≥ n0 Supponendo T(k) ≤ c k2 per tutti i k < n, si ottiene T(n) = 4T(n/2) + n ≤ 4c(n/2)2 + n = cn2 + n ≤ cn2 per nessun c ≥ 0 Dobbiamo modificare l’ipotesi: T(n) ≤ c n2 - d n, d ≥ 1 T(n) = 4T(n/2) + n ≤ 4 (c(n/2)2 - d(n/2)) + n = cn2 - 2dn + n = cn2 - dn - (dn - n) Si può scegliere d = 1. ≤ cn2 - dn Bisogna fare attenzione alle condizioni al contorno. Non è sempre scontato che esse siano limitate da c f(n). Ad esempio supponiamo di aver dimostrato per la ricorrenza T(n) = 2 T(n/2) + n che T(n) ≤ c n lgn e sia T(1) = 1 Allora non si ottiene T(1) ≤ c 1 lg1 per nessun c, essendo lg1 = 0 La difficoltà può essere superata ricordando che la notazione asintotica richiede di dimostrare T(n) ≤ c n lgn per gli n ≥ n0, dove n0 è una costante. Consideriamo allora T(2) e T(3), in quanto per n > 3, la relazione non dipende più da T(1). T(2) = 4 ≤ c 2 lg2 T(3) = 5 ≤ c 3 lg3 Basta scegliere c ≥ 2 Un inconveniente del metodo di sostituzione è che non esiste una regola per trovare la soluzione corretta per ogni ricorrenza. Di solito bisogna provare più tentativi, ma se la ricorrenza è simile ad una di cui si conosce la soluzione, si può provare la stessa soluzione. Ad esempio: T(n) = 2T(n/2 + 15) + n Intuitivamente per n grande la differenza tra n/2 e n/2 + 15 non è molto significativa. Si può perciò tentare con la soluzione T(n) = O(n lgn), che sappiamo risolvere la ricorrenza T(n) = 2T(n/2) + n. Dimostriamo il risultato anticipato prima che la ricorrenza T(n) = 2T (n/2) + n appartiene all’insieme O(n lgn) Proviamo T(n) ≤ c n lgn T(n) = 2T (n/2) + n ( n/2 ≤ n/2 ) ≤ 2 (c (n/2) lg(n/2)) + n = c n lgn - c n lg2 + n = c n lgn - c n + n ≤ c n lgn per c ≥ 1 Il metodo iterativo • Sviluppare la ricorrenza ed esprimerla come somma di termini che dipendono solo da n e dalle condizioni al contorno. Per risolvere la ricorrenza si applicano le tecniche per limitare le sommatorie. Consideriamo di nuovo l’equazione T(n) = 4T(n/2) + n T(n) = n + 4T(n/2) = n + 4 ( n/2 + 4 T(n/4)) = n + 2n + 42(n/4 + 4 T(n/8) ) = n + 2n + 4n + 43(n/8 + 4 T(n/16) ) = n + 2n + … + 2lgn-1 n + 2lgnT(1) = n (1 + 2 + … + 2lgn-1 ) + n Θ(1) = n (2lgn - 1) + Θ(n) = n2 - n + Θ(n) = Θ(n2) Alberi di ricorsione Disegnamo l’unfolding della ricorrenza T(n) = n + 4T(n/2) T(n) n T(n/2) T(n/2) T(n/2) T(n/2) n n/2 n/2 n n/2 n/2 2n lgn n/4 n/4 n/4 n/4 n/4 n/4 n/4 n/4 n/4 n/4 n/4 n/4 n/4 n/4 n/4 n/4 4n 2lgn Θ(1) lgn-1 Totale: n Σ 2i + 2lgn Θ(1) = n (n - 1) + n Θ(1) i=0 = Θ(n2) Insertion-sort {length[A] ≥ (A) 1} 1Insertion-sort for j ← 2 to length[A] (A) do 2 key ← A[j] {A[1..j-1] è ordinato} 3 {inserisci A[j] nella sequenza A[1. .j-1] for jspostando ← 2 toa length[A] do destra gli elementi > di A[j]} ← A[j] 4 i key ← j-1 {inserisci sequenza 5 while i > 0A[j] andnella A[i] > key do A[1. .j-1] 6 7 8 spostando a destra A[i+1] ← A[i] gli elementi > di A[j]} ← i-1 i ←i j-1 A[i+1] while← i >key 0 and A[i] > key do A[i+1] ← A[i] i ← i-1 A[i+1] ← key {A[1..length[A]] è ordinato} Al j-esimo passo: {A[1..j-1] e` ordinato} A 1 2 3 3 5 10 ... j j+1 25 26 6 29 3 length[A] ... 8 4 21 ... 8 4 21 6 key A 1 2 3 3 5 10 ... i j j+1 25 26 26 29 3 . . . A 1 2 3 4 3 5 10 10 ... j j+1 25 26 29 3 ... 8 4 21 A 1 2 3 4 3 5 10 10 ... j j+1 25 26 29 3 ... 8 4 21 j 25 26 29 3 ... 8 4 21 length[A] i=2 6 key A 1 2 3 4 3 5 6 10 ... {A[1..j] e` ordinato e l’insieme degli elementi è ricostruito} Insertion-sort (A) { A[1..j-1] è ordinato } for j ← 2 to n do key ← A[j] {inserisci A[j] nella sequenza A[1. .j-1] spostando a destra gli elementi > di A[j]} i ← j-1 {A[1..j-1] è ordinato & A[i+1..j-1] > key} while i > 0 and A[i] > key do A[i+1] ← A[i] i ← i-1 {A[1..j-1] è ordinato & A[i+1..j-1] > key & A[i] ≤ key} A[i+1] ← key {A[1..n] è ordinato} O(n2) Insertion-sort = O(n2) n = length[A] Un algoritmo di ordinamento Divide et Impera Merge-Sort (A, p, q) if p < q then r ← (p + q)/2 Merge-Sort (A, p, r) Merge-Sort (A, r+1, q) Merge (A, p, r, q) T(n) = T(n/2) + T(n/2) + Θ(n) n=q-p+1 per n ≥ 2 T(n) soddisfa T(n) = Θ(1) T(n) = 2T(n/2) + Θ(n) per n = 1 per n ≥ 2 n n n n/2 n/2 lgn n/4 n/4 n/4 n/4 n 2lgn Θ(1) Totale: (lg n - 1) n + 2lgn Θ(1) = Θ(n lgn) T(n) = Θ(n lgn) L’algoritmo Merge-Sort è perciò asintoticamente migliore dell’algoritmo Insertion-sort Si può inoltre dimostrare che Θ(n lgn) confronti sono necessari nel caso peggiore per ordinare n numeri per qualunque algoritmo basato sui confronti. Quindi l’algoritmo Merge-Sort è (asintoticamente) ottimo. Il metodo principale Teorema principale - versione semplificata Siano a ≥ 1, b > 1 e c ≥ 0 delle costanti. T(n) sia definita dalla seguente ricorrenza: T(n) = a T(n/b) + Θ(nc) dove n/b rappresenta n/b oppure n/b. Allora T(n) è asintoticamente limitato nel modo seguente: • se c < logba allora T(n) = Θ(nlogba) • se c = logba allora T(n) = Θ(nc lgn) • se c > logba allora T(n) = Θ(nc) Esempi di uso del teorema principale Il tempo di calcolo per l’algoritmo DI-Massimo-ricorsivo soddisfa la ricorrenza: T(n) = 2 T(n/2) + Θ(1) a=b=2 c = 0 < 1 = logba Caso 1 del teorema principale: T(n) = Θ(nlog22) = Θ(n) Il tempo di calcolo per l’algoritmo Merge-Sort soddisfa la ricorrenza: T(n) = 2 T(n/2) + Θ(n) a=b=2 c = 1 = logba Caso 2 del teorema principale: T(n) = Θ(nc lgn) = Θ(n lgn) Ancora qualche esempio: 1. T(n) = 8 T(n/2) + n2 a=8 b=2 c = 2 < 3 = logba 2. Caso 1 del teorema principale: T(n) = Θ(nlog28) = Θ(n3) T(n) = 7 T(n/2) + Θ(n2) a=7 b=2 c = 2 < logba ≈ 2.801 Caso 1 del teorema principale: T(n) = Θ(nlog27) = O(n3) Per le equazioni di ricorrenza: 1. T1(n) = 4 T(n/2) + n 2. T2(n) = 4 T(n/2) + n2 3. T3(n) = 4 T(n/2) + n3 Si applicano rispettivamente i casi 1, 2 e 3 del teorema principale: a=4 b=2 c = i logba = 2 Si ha allora 1. 2. 3. T1(n) = Θ(n2) T2(n) = Θ(n2lgn) T3(n) = Θ(n3) Come possiamo “leggere” le complessità ottenute? Caso 1. T1(n) = 4 T(n/2) + n = Θ(n2) si applica se il costo della divisione in sottoproblemi e della costruzione della soluzione a partire da quella dei sottoproblemi (nc) è trascurabile rispetto al costo della soluzione dei sottoproblemi stessi (a T(n/b)). Caso 2. T2(n) = 4 T(n/2) + n2 = Θ(n2lgn) si applica se il costo della divisione in sottoproblemi e della costruzione della soluzione a partire da quella dei sottoproblemi (n c) è analogo al costo della soluzione dei sottoproblemi stessi (a T(n/b)). Caso 3. T3(n) = 4 T(n/2) + n3 = Θ(n3) si applica se il costo della divisione in sottoproblemi e della costruzione della soluzione a partire da quella dei sottoproblemi (n c) è il fattore dominante