Lez. 10a1 Universita' di Ferrara Facolta' di Scienze Matematiche, Fisiche e Naturali Laurea Specialistica in Informatica Algoritmi Avanzati Strategie per la progettazione di algoritmi: divide & conquer problema del min-max quicksort Copyright © by Claudio Salati.
2 DIVIDE-&-CONQUER Nella progettazione di un algoritmo la strategia e' quasi sempre quella di suddividere il problema in sottoproblemi piu' semplici. Dato un problema P su un insieme di dati di dimensione n una maniera di suddividerlo in problemi piu' semplici e' quella di considerare problemi analoghi a P ma su insiemi di dati di dimensione minore (cioe' problemi piu' piccoli: piccolo e' facile) SI RICONDUCE LA SOLUZIONE DI P, dove # P = n, ALLA SOLUZIONE DI m PROBLEMI P1, P2, …, Pm, dove i | 1 i m : # P i = n/k OVVIAMENTE BISOGNA CHE SIA FACILE: OPERARE LA SCOMPOSIZIONE RICOMPORRE LE m SOLUZIONI PARZIALI NELLA SOLUZIONE COMPLESSIVA
3 DIVIDE-&-CONQUER PERCHE' CONSIDERARE LO STESSO PROBLEMA SOLO PER DIMENSIONI PIU' PICCOLE DOVREBBE ESSERE CONVENIENTE? PERCHE' SE IL PROBLEMA E' DI COMPLESSITA' n 2 DIVIDERLO PER 2 SIGNIFICA OTTENERE UN PROBLEMA 4 VOLTE PIU' FACILE! Non solo, OPERANDO RICORSIVAMENTE SI ARRIVA AD OTTENERE PROBLEMI ELEMENTARI: e.g., ORDINARE UN VETTORE DI LUNGHEZZA 1 E' MOLTO FACILE, E' GIA' DI PER SE STESSO ORDINATO
4 DIVIDE-&-CONQUER void divideAndConquer(items data [], int fromItem, int toItem) { // risolve il problema per il sottovettore // data[fromItem..toItem]; // la risoluzione si suppone sul posto if (smallEnough(fromItem, toItem)) solve(data, fromItem, toItem); else { int middle = (fromItem + toItem) / 2; // splitProblem(data, fromItem, toItem, // &middle); divideAndConquer(data, fromItem, middle); divideAndConquer(data, middle + 1, toItem); combine(data, fromItem, toItem, middle); }
5 DIVIDE-&-CONQUER IL PARTIZIONAMENTO AVVIENE DI NORMA VERSO PROBLEMI BILANCIATI, DI UGUALE DIMENSIONE SBILANCIARE NON CONVIENE SUPPONIAMO DI APPLICARE MERGESORT A 2 SOTTOPROBLEMI SBILANCIATI: T(n) = T(1) + T(n-1) + T(merge, 1, n-1) T(1) E' COSTANTE T(merge, 1, n-1) E' O(n) E LA COMPLESSITA' TOTALE E' O(n 2 ) INFATTI CON LO SBILANCIAMENTO SI E' TRASFORMATO MERGESORT IN STRAIGHT-INSERTION!
6 DIVIDE-&-CONQUER SE g(n) E' LA COMPLESSITA' DELLA SOLUZIONE DI UN PROBLEMA smallEnough() E f(n) E' LA COMPLESSITA' DI combine() 2 SOLUZIONI PARZIALI, CIASCUNA DI DIMENSIONE n/2 ALLORA LA COMPLESSITA' DI divideAndConquer() E' T(n) = g(n)per n smallEnough() T(n) = 2 * T(n/2) + f(n)per n !smallEnough() La complessita della operazione di scomposizione del problema in due sotto-problemi (che nello schema di funzione coincide con il calcolo di middle ) e supposta assorbibile entro la complessita' di combine()
7 DIVIDE-&-CONQUER Vogliamo valutare il costo T(n) di risolvere un problema di dimensione n SUPPONIAMO DI DIVIDERE UN PROBLEMA DI DIMENSIONE n IN a SOTTOPROBLEMI DI DIMENSIONE n/b CIASCUNO E CHE IL LAVORO PER OPERARE LA RICOMBINAZIONE DELLE SOLUZIONI PARZIALI SIA c * n d (INGLOBANDO IN QUESTO ANCHE L'EVENTUALE COSTO DEL PARTIZIONAMENTO IN SOTTOPROBLEMI) E SUPPONIAMO CHE RISOLVERE UN PROBLEMA DI DIMENSIONE 1 ABBIA COSTO COSTANTE k 1 ALLORA a * T(n/b) E' IL COSTO DI RISOLVERE TUTTI I SOTTOPROBLEMI
8 DIVIDE-&-CONQUER Definizione ricorsiva di T(n): T(n)= c * n d + a * T(n/b) T(1)= k 1 espandiamo ricorsivamente T(n/b) T(n)= c * n d + a * (c * (n/b) d + a * T(n/b 2 )) = c * n d * (1 + a/b d ) + a 2 * T(n/b 2 ) e continuando ad espandere ricorsivamente per p=log b n volte T(n)= c * n d * (1 + a/b d + … + (a/b d ) p-1 ) + a p * T(1) ma a p = a = (b ) = (b ) = n log b nlog b alog b n log b a {
9 DIVIDE-&-CONQUER cioe' (se x 1) e
10 DIVIDE-&-CONQUER se x<1 allora per cui k 2 maggiora la sommatoria per qualsiasi n finito se x=1 allora la sommatoria vale (n+1)*a se x>1 per n sufficientemente grande la sommatoria e' maggiorata da a*x n+1
11 DIVIDE-&-CONQUER N.B.: la sommatoria e' quella dei primi n termini della serie geometrica La serie geometrica e la sua sommatoria convergono per x < 1 divergono per x > 1 Per x = 1 la sommatoria della serie geometrica diverge Per x>1 e n abbastanza grande la sommatoria della serie geometrica coincide approssimativamente, a meno di costanti moltiplicative, con il suo ultimo termine (o meglio, con il termine successivo, per tenere conto dei termini precedenti) N.B.: nel nostro caso x = a / b d
12 DIVIDE-&-CONQUER a < b d Caso: x = a / b d < 1 e quindi, poiche' log b a < d : in questo caso domina il lavoro non ricorsivo di ricomposizione dei risultati parziali
13 DIVIDE-&-CONQUER a > b d Caso: x = a / b d > 1 ma: e quindi: in questo caso domina il lavoro ricorsivo
14 DIVIDE-&-CONQUER a = b d Caso: x = a / b d = 1 e quindi: in quanto log b a=d in questo caso il lavoro ricorsivo e quello di ricomposizione si bilanciano, e compare il termine logaritmico
15 DIVIDE-&-CONQUER Riassumendo: se a b d se a = b d
16 DIVIDE-&-CONQUER Nel caso del mergesort: a = 2 b = 2 d = 1 –quindi: a = b d –quindi: T(n) = O(n * log 2 n) Nel caso dellalgoritmo del torneo: a = 2 b = 2 d = 0 –quindi: a > b d –quindi: T(n) = = O(n)
17 DIVIDE-&-CONQUER In generale, se allora salvo che le due quantita' maggiori siano uguali. In questo caso e':
18 DIVIDE-&-CONQUER: esempio Immaginiamo di dovere individuare elemento minimo e massimo di un vettore. L'algoritmo seguente fornisce una soluzione banale del problema typedef... elemento; struct result { elemento min; elemento max; }; struct result easyMinMax(elemento v[], int n) { 1 assert(n>0); struct result res; 2 res.min = res.max = v[0]; 3 for (int i=1; i<n; i+=1) { 4 if (v[i] < res.min) res.min = v[i]; 5 if (v[i] > res.max) res.max = v[i]; 6 } 7 return (res); 8 }
19 DIVIDE-&-CONQUER: esempio Per analizzare la complessita' dell'algoritmo ci si concentra sul numero di confronti tra elementi del vettore che esso esegue. perche' le altre operazioni sono in numero proporzionale a questi confronti. perche' se gli elementi sono oggetti complessi (e.g. stringhe) il costo di questi confronti e' predominante rispetto al costo delle altre operazioni. la complessita' di easyMinMax() e' evidentemente 2*(n-1). 2*(n-1) rappresenta caso migliore, peggiore, e medio. e' anche immediato vedere come easyMinMax() puo' essere migliorata: se v[i] res.max quindi il secondo if (riga 6) deve essere eseguito solo se la condizione del primo (riga 5) risulta falsa
20 DIVIDE-&-CONQUER: esempio typedef... elemento; struct result { elemento min; elemento max; }; struct result betterMinMax(elemento v[], int n) { 1 assert(n>0); struct result res; 2 res.min = res.max = v[0]; 3 for (int i=1; i<n; i+=1) { 4 if (v[i] < res.min) res.min = v[i]; 5 else if (v[i] > res.max) res.max = v[i]; 6 // end if } 7 return (res); 8 } 9
21 DIVIDE-&-CONQUER: esempio la complessita' di betterMinMax() non e' piu' sempre la stessa per i casi migliore, peggiore, e medio. il caso migliore si ha quando v[] e' ordinato in modo non crescente: in questo caso il numero di confronti e' n-1. il caso peggiore si ha quando v[] e' ordinato in modo non decrescente: in questo caso il numero di confronti e' 2*(n-1). nel caso medio sara' v[i]<res.min meta' delle volte e quindi il numero medio di confronti sara' 3*(n-1)/2. e' possibile fare "di meglio"? applichiamo la strategia divide & conquer!
22 DIVIDE-&-CONQUER: esempio typedef... elemento; typedef struct result { elemento min; elemento max; } result; result dAndCMinMax(elemento v[], 1 int from, int to) { assert(0<=from && from<= to && to<=#v); result res; 2 if (to-from <= 1) { 3 // 1 o 2 elementi nel sottovettore if (v[from] <=v [to]) { 4 res.min = v[from]; res.max = v[to]; 5 } else { 6 res.min = v[to]; res.max = v[from]; 7 } 8 } else { 9 // continua alla pagina seguente
23 DIVIDE-&-CONQUER: esempio // continuazione di dAndCMinMax() // piu' di 2 elementi nel sottovettore int mid = (from + to) / 2; 10 result r1 = dAndCMinMax(v, from, mid); 11 result r2 = dAndCMinMax(v, mid+1, to); 12 res.min = (r1.min <= r2.min) ? r1.min 13 : r2.min; 14 res.max = (r1.max >= r2.max) ? r1.max 15 : r2.max; 16 } 17 return (res); 18 } 19
24 DIVIDE-&-CONQUER: esempio dAndCMinMAX() correttezza: per esercizio (elementare per induzione matematica). dAndCMinMAX() complessita': e' data dalla seguente relazione ricorsiva: T(1) = T(2) = 1 T(n : n>2) = 2 * T(n/2) + 2 T(n)= 2 * (2 * T(n/4) + 2) + 2= 4 * T(n/4) + (4+2) = 4 * (2 * T(n/8) + 2) + (4+2)= 8 * T(n/8) + (8+4+2) =... e per k tale che 2 k =n
25 DIVIDE-&-CONQUER: esempio N.B.: la complessita' calcolata per dAndCMinMAX() si applica in tutti i casi, peggiore, migliore e medio. In effetti e' dimostrabile che nessun algoritmo basato su confronti puo' richiedere meno di 3*n/2-2 confronti. Ma dAndCMinMAX() e' davvero migliore di betterMinMax() ? in termini di complessita' spaziale e' peggiore: richiede l'allocazione di log(n) record di attivazione ricorsiva. nel confronto si potrebbero contare anche le altre operazioni, e allora il vantaggio di dAndCMinMAX() diventerebbe minore. c'e' poi da considerare l'overhead temporale delle chiamate ricorsive. Cio' dimostra l'importanza, in certe circostanze, di considerare i coefficienti moltiplicativi e i termini di grado inferiore nelle funzioni che descrivono la complessita' degli algoritmi.
26 ALGORITMI DI ORDINAMENTO PER CONFRONTO Algoritmo O(n 2 )Corrispondente algoritmo O(n*log(n)) straight insertion mergesort straight selection heapsort bubblesort ? Straight insertion si basa sulla creazione di un sottovettore localmente ordinato e sull'inserimento in esso, uno dopo l'altro, degli elementi restanti del vettore Mergesort si basa sulla creazione di due sottovettori localmente ordinati e sulla loro fusione ordinata Straight selection, heapsort e bubblesort si basano sulla creazione di un sottovettore globalmente ordinato e con la sua estensione successiva con gli elementi del restante sottovettore disordinato N.B.: tra sottovettore ordinato e sottovettore disordinato esiste una precisa relazione: tutti gli elementi del primo sono di tutti gli elementi del secondo
27 ALGORITMI DI ORDINAMENTO PER CONFRONTO Straight insertion si basa –sulla creazione di 2 sottovettori, un sottovettore localmente ordinato e un sottovettore non ordinato (senza alcuna relazione dordine tra gli elementi di un sottovettore e quelli dellaltro) –sull'inserimento nel sottovettore ordinato, uno dopo laltro, degli elementi del sottovettore non ordinato Mergesort si basa –sulla creazione di 2 sottovettori, entrambi localmente ordinati (senza alcuna relazione drdine tra gli elementi di un sottovettore e quelli dellaltro) –sul merge ordinato dei 2 sottovettori –sulluso della strategia Divide&Conquer
28 ALGORITMI DI ORDINAMENTO PER CONFRONTO Straight selection si basa –sulla creazione di 2 sottovettori, un sottovettore globalmente ordinato e un sottovettore non ordinato (gli elementi del sottovettore non ordinato sono tutti maggiori o uguali degli elementi del sottovettore globalmente ordinato) –sull'inserimento nel sottovettore ordinato dellelemento minimo del sottovettore non ordinato Heapsort si basa –sulla creazione di 2 sottovettori, un sottovettore globalmente ordinato e un sottovettore non ordinato (gli elementi del sottovettore non ordinato sono tutti minori o uguali degli elementi del sottovettore globalmente ordinato) –sul fatto che il sottovettore non ordinato e costituito da uno heap basato sulla relazione maggiore o uguale –sull'inserimento nel sottovettore ordinato dellelemento massimo del sottovettore non ordinato –sulluso della differenziazione formale
29 ALGORITMI DI ORDINAMENTO PER CONFRONTO Straight exchange e/o bubblesort si basano –sulla creazione di 2 sottovettori, un sottovettore globalmente ordinato e un sottovettore non ordinato (gli elementi del sottovettore non ordinato sono tutti maggiori o uguali degli elementi del sottovettore globalmente ordinato) –sullutilizzo di una successione di scambi con elementi adiacenti del sottovettore non ordinato per portare un elemento del sottovettore non ordinato al proprio posto nel sottovettore ordinato Quicksort si basa –sullapplicazione della strategia Divide&Conquer –sullutilizzo, per spostare un elemento verso la sua posizione corretta, di salti il piu lunghi possibile
30 QUICKSORT Come saranno i due sottovettori in cui viene suddiviso il vettore da ordinare secondo la strategia Divide&Conquer? Non possono essere entrambi globalmente ordinati, altrimenti le 2 chiamate ricorsive e la funzione combine() non avrebbero niente da fare, avrebbe fatto gia tutto la funzione splitProblem() ! Quindi almeno inizialmente non potranno essere ordinati, saranno le chiamate ricorsive a ordinarli. Ma la relazione dordine tra i due sottovettori? La si conserva! La funzione splitProblem() dovra essere tale da suddividere il vettore in 2 sottovettori tali che gli elementi delluno sono tutti minori o uguali degli elementi dellaltro. La funzione combine() sara vuota!
31 QUICKSORT Quicksort si basa come bubblesort sullo scambio di elementi disordinati. Lo scambio di elementi in quicksort non ha lo scopo di mettere in ordine elementi adiacenti. Lo scambio in quicksort non avviene tra elementi adiacenti. Gli elementi fanno degli spostamenti maggiori: e' per questo che ci si aspetta un comportamento migliore di quello di bubblesort Lo scambio di elementi in quicksort ha lo scopo di creare due sottovettori che abbiano la seguente proprieta': Proprieta' P: Tutti gli elementi del primo sottovettore sono minori o uguali di tutti gli elementi del secondo sottovettore. Per ottenere un vettore ordinato e quindi sufficiente ordinare localmente ciascuno dei due sottovettori ottenuti dal partizionamento del vettore. L'ordinamento dei due sottovettori e' ovviamente possibile applicando ricorsivamente la procedura.
32 QUICKSORT i = 0..m-1: v[i] v[m] i = m+1..N-1: v[i] v[m] e quindi: i = 0..m-1, j = m+1..N-1 : v[i] v[j] L'operazione di suddivione del vettore nei due sottovettori separati dall'elemento pivot m e' chiamata partizionamento v[0]v[1]v[N-1]... vettore v v[0]...v[m]v[N-1]... vettore v
33 QUICKSORT: lelemento pivot Lelemento pivot dovrebbe essere lelemento mediano del vettore v In questo modo i due sottovettori alla sua destra e alla sua sinistra sarebbero bilanciati Ma come e possibile conoscere a priori quale e lelemento mediano del vettore? Ci vorrebbe un oracolo. In mancanza delloracolo ci si affida al calcolo delle probabilita: si utilizza come pivot un elemento a caso, il piu comodo, il primo elemento del (sotto-)vettore.
34 QUICKSORT: l'algoritmo void quicksort(int v[], int from, int to) { if (to > from) { int m = partition(v, from, to); quicksort(v, from, m-1); quicksort(v, m+1, to); } La funzione partition() effettua il partizionamento del vettore v[] in due sottovettori che soddisfano la proprieta' P intorno all'elemento pivot di indice m Per ordinare il vettore int v[n] e' sufficiente chiamare quicksort(v, 0, n-1);
35 QUICKSORT: funzione partition().1 int partition(int v[], int from, int to) { assert(to>from); int pivot = v[from]; // seleziono il pivot int fromScan = from+1; int toScan = to; // i=from+1..fromScan-1: v[i] v[from] // i=toScan+1..to: v[i] v[from] for (;;) { while (fromScan <= toScan && v[fromScan] <= pivot) fromScan += 1; while (toScan >= fromScan && v[toScan] >= pivot) toScan -= 1; // continua alla pagina successiva
36 QUICKSORT: funzione partition().2 // int partition(): continua if (toScan > fromScan) { int temp = v[fromScan]; v[fromScan] = v [toScan]; v[toScan] = temp; fromScan += 1; toScan -= 1; } else { break; } assert (toscan == fromscan-1); int m = fromScan - 1; v[from] = v[m]; v[m] = pivot; return(m); }
37 QUICKSORT: correttezza.1 Teorema: La funzione partition() suddivide il sottovettore v[from..to] in tre parti (la prima o la terza eventualmente vuote): il sottovettore v[from..m-1] l'elemento v[m] il sottovettore v[m+1..to] tali che: k = from.. m-1: v[k] v[m] k = m+1.. to: v[m] v[k] Dimostrazione: Discende immediatamente dall'invariante del ciclo esterno e dal fatto che tale ciclo termina quando toScan=fromScan-1. L'elemento v[m] al termine della funzione e' uguale all'elemento v[from] all'inizio di essa.
38 QUICKSORT: correttezza.2 Teorema: La funzione quicksort() ordina il sottovettore v[from..to] in modo non decrescente. Dimostrazione: Per induzione matematica sulla lunghezza del sottovettore v[from..to]. Ciascuno dei due sottovettori delle chiamate ricorsive e' lungo al piu' to-from, per cui l'ipotesi induttiva e' applicabile. Dopo le chiamate ricosive i due sottovettori sono ordinati: la correttezza globale discende dalla correttezza della funzione partition().
39 QUICKSORT: complessita'.1 L'algoritmo quicksort ha complessita' O(n 2 ). Nel caso peggiore infatti ogni partizionamento avviene con uno dei due sottovettori vuoto. La chiamata ricorsiva e' quindi per un sottovettore di lunghezza inferiore di un solo elemento a quella del sottovettore della attivazione chiamante. Si hanno quindi n attivazioni della funzione e ogni attivazione, relativa ad un sottovettore di lunghezza k, effettua k-1 confronti. Nel caso migliore pero' il partizionamento e con sottovettori bilanciati: in questo caso l'altezza dell'albero delle chiamate ricorsive e' solo log(n) e il comportamento dell'algoritmo diventa di complessita' n*log(n). La domanda quindi e': Quale e' il comportamento atteso? Quale e' il comportamento medio?
40 QUICKSORT: complessita' media.2 L'ipotesi e' che tutte le permutazioni della sequenza di elementi da ordinare siano equiprobabili. Sia T(n) la complessita' dell'algoritmo nell'ordinamento di un sottovettore di lunghezza n: T(0) = T(1) = b (costante) T(n) = (n - 1) + T(i-1) + T(n-i), per n 2, con i = 1..n essendo i la posizione nel sottovettore partizionato dell'elemento pivot. Nell'ipotesi fatta i puo' assumere qualunque valore tra 1 e n con uguale probabilita', per cui in media (1)
41 Proviamo se la relazione e' soddisfatta da T(n) = c * n * log(n) (che e' la complessita' "desiderata"). Sostituiamo nella relazione ricorsiva (1): (2) Quanto vale la sommatoria che compare in (2)? QUICKSORT: complessita' media.3 ?
42 Consideriamo una generica funzione monotona crescente f(x) (3) QUICKSORT: complessita' media.4
43 Ma per cui, sottraendo (f(n) - f(1)) dall'integrale di (3): (4) consideriamo allora f(x) = x * ln(x) tenendo conto che e sostituendo nella disequazione (4) (vedi pagina seguente) QUICKSORT: complessita' media.5
44 cioe' QUICKSORT: complessita' media.6
45 E dividendo per ln(2) che sostituiamo nella relazione ricorsiva (2): quindi, a meno di termini di ordine minore: T(n) = O(n*log(n)) QUICKSORT: complessita' media.7