Divide et impera. Divide et impera. Divide et impera. Divide et impera

Похожие документы
Tecniche Algoritmiche: divide et impera

Esercizi per il corso di Algoritmi, anno accademico 2011/12

Tempo e spazio di calcolo (continua)

Gli algoritmi ricorsivi di ordinamento. Paolo Camurati Dip. Automatica e Informatica Politecnico di Torino

Algoritmi e Strutture Dati

Trasformare array paralleli in array di record

Un esempio di calcolo di complessità: insertion sort

Analisi algoritmi ricorsivi e relazioni di ricorrenza

Algoritmi di ordinamento: Array e ricorsione

Lo sviluppo di un semplice programma e la dimostrazione della sua correttezza

Albero di Riscorsione

Lista di esercizi 11 maggio 2016

5. DIVIDE AND CONQUER I

Cammini minimi fra tutte le coppie

Algoritmi e Strutture Dati

Algoritmi e Strutture Dati

2. Analisi degli Algoritmi

Dati e Algoritmi I (Pietracaprina) Esercizi sulle Nozioni di Base

Programmazione dinamica

Parte 1: tipi primitivi e istruzioni C

Algoritmi greedy. Gli algoritmi che risolvono problemi di ottimizzazione devono in genere operare una sequenza di scelte per arrivare alla soluzione

Esempio : i numeri di Fibonacci

ALGORITMI Docente: Prof. Domenico Cantone

Транскрипт:

Divide et impera Divide et impera La tecnica detta divide et impera è una strategia generale per impostare algoritmi (par. 9.4). Consideriamo un problema P e sia n la dimensione dei dati, la strategia consiste nel: suddividere il problema in k sottoproblemi P i di dimensione inferiore (ciascuno di dimensione n i ) e successivamente riunire i risultati ottenuti dalle k soluzioni. La frase è attribuita a Filippo il Macedone e fu un principio politico: mantenere divise le popolazioni dominate per poter governare con più facilità. Divide et impera Se i k sottoproblemi sono formalmente simili al problema di partenza, si ottiene una scomposizione ricorsiva. Ci deve pertanto essere una dimensione h del problema che porti ad una risoluzione diretta, vale a dire che non necessiti della ricorsione. Indichiamo con: S l insieme dei dati k il numero dei sottoproblemi h la dimensione limite Si può scrivere uno schema generale per la scomposizione. Divide et impera algoritmo DIVETIMP (S, n) se n < h allora risolvere direttamente il problema P altrimenti dividere S in k sottoinsiemi risolvere separatamente i k sottoproblemi P 1,, P k : DIVETIMP(S 1,n 1 ),, DIVETIMP(S k,n k ) riunire i risultati ottenuti //finese //fine algoritmo 1

Divide et impera: complessità Indichiamo con T(n) la complessità del problema P sull insieme dei dati di dimensione n; poiché l algoritmo è ricorsivo si ottengono delle formule di ricorrenza : T(n) = costante n<h T(n) = D(n) + C(n) + T(n 1 ) + T(n 2 ) + T(n k ) Ordinamenti quicksort e mergesort D(n): complessità dell algoritmo per dividere l insieme C(n): complessità dell algoritmo per riunire i risultati T(n i ): complessità dell algoritmo sull insieme di dimensione n i. L idea è la seguente: è più conveniente ordinare due array di s e t componenti piuttosto che un array di n componenti (s + t = n) si aumenta l efficienza dell ordinamento scambiando elementi lontani tra loro. (par. 9.4) Verifichiamo la prima idea. Supponiamo s=t=n/2 e prendiamo la formula n(n-1)/2 che rappresenta la complessità dell ordinamento nel caso peggiore e riscriviamola per n/2 invece che n: 2 [(n/2) (n/2 1) /2] = n 2 /4 n/2 Confrontiamo le formule: n 2 /4 n/2 < n 2 /2 n/2 = (n 2 n)/2 = = n (n-1) /2 2

Verifichiamo la seconda idea. Supponiamo di avere un array ordinato in senso inverso; scambiando gli elementi opposti (il primo con l ultimo, il secondo con il penultimo, ecc.) in n/2 operazioni ordiniamo l array. Questi elementi sono lontani tra loro: quelli più grandi sono al posto di quelli più piccoli. Esempio. a = (10, 8, 7, 5, 4, 2, 0, -1) (-1, 0,....., 8, 10) Dividiamo l insieme in due parti e scambiamo elementi lontani. Scegliamo un elemento dell array per eseguire i confronti e lo chiamiamo conf; dividiamo l insieme in modo tale che gli elementi più piccoli di conf possano essere messi a sinistra di conf e quelli più grandi a destra, rispettando così l ordine. A questo punto l insieme è diviso in due parti indipendenti e l elemento conf è al suo posto. conf Si potrà proseguire in maniera ricorsiva fino a considerare un array di dimensione 1, che non dovrà essere ulteriormente suddiviso. Se guardiamo l array dalla prima componente all ultima (riunire i risultati), vediamo che l array è ordinato. Scriviamo il progetto dell algoritmo secondo lo schema divide et impera per ordinare un array v dalla componente n1 alla componente n2; alla prima chiamata, n1 ed n2 saranno la prima e ultima componente dell array. algoritmo quicksort(n1,n2,v) se n1 < n2 allora //finese //fine quicksort chiamare l algoritmo partizione(n1, n2, v, k) dove k è il valore della posizione di conf chiamare quicksort(n1, k-1, v) chiamare quicksort(k+1, n2, v) Quando n1=n2 l array ha un solo elemento e pertanto la ricorsione ha termine. 3

Come scegliere l elemento conf? Dobbiamo stabilire un criterio che si possa facilmente ripetere in tutte le suddivisioni successive. Stabiliamo di scegliere la prima componente di quella porzione di array (da n1 a n2) che vogliamo ordinare: v[n1]. Ci sono varie scritture dell algoritmo quicksort, alcune ottimizzano il numero di confronti, ma lasciano inalterata la complessità. Per realizzare la partizione avremo bisogno di due indici: un indice i che scorre l array con valori crescenti e che parte dalla posizione successiva a quella di conf (i=n1+1), un altro indice k che descrive l array con valori decrescenti e parte dall ultima posizione (k=n2). Quando questi due indici saranno uguali avremo terminato la partizione e si potrà sistemare conf al suo posto. Vediamo un progetto per l algoritmo di partizione. algoritmo partizione(n1, n2, v) conf v[n1] i n1+1 k n2 mentre i k eseguire mentre v[i] conf e i k eseguire i i+1 //fine mentre mentre v[k] conf e i k eseguire k k-1 //finementre scambiare v[i] con v[k] //finementre: ciclo esterno //sistemare conf nella posizione se v[k] > conf allora k k-1 //finese scambiare v[n1] con v[k] //fine partizione k=i: è al suo posto E necessario il confronto tra conf e v[k]? 4

Esempio. 10 5 11 1 13 2 20 conf i=2 i=3 k=6 k=7 10 5 2 1 13 11 20 i=4 i=5 k=5 i = k = 5 termina anche il ciclo esterno v[k]>conf (10>13) quindi k-1 1 5 2 10 13 11 20 Esempio. 20 3 1 10 9 7 11 conf i=2 i=3 i=4 i=5 i=6 i=7= k i = k = 7 v[k]>conf falso : termina anche il ciclo esterno k non varia 11 3 1 10 9 7 20 Complessità. Contiamo i confronti tra conf e gli elementi v[i]: 0 se n = 0,1 (n1<n2) T(n) = D(n) + C(n) + T(k-1) + T(n-k) D(n) è la complessità dell algoritmo partizione C(n) = 0 guardare le due parti dell array D(n) = O(n): n-1 confronti nei predicati dei cicli interni + 1 confronto per sistemare conf. Caso peggiore: vettore ordinato, la partizione è sbilanciata: T(n) = n + T(0) + T(n-1) = n + T(n-1) = = n + (n-1 + T(0) + T(n-2)) = n + n-1 + T(n-2) = = n + (n-1) + (n-2) + T(n-3) = =.. = n + (n-1) + + 1 + T(0) + T(n (n-1)) = = n (n-1)/2 O(n 2 /2) 5

Caso favorevole: conf sempre al centro: la partizione è bilanciata: L idea è la seguente: dividere l insieme in due parti uguali di n/2 componenti T(n) = n + T(n/2) + T(n/2) = n + 2T(n/2) = = n + 2(n/2 + 2T(n/4)) = 2n + 2 2 T(n/2 2 ) = =.. = k n + 2 k T(n/2 k ) n/2 n/2 se fossero già ordinate le potremmo riunire con un algoritmo di fusione (merge) se n = 2 k allora k = log 2 n O(n log 2 n) Esempio. Consideriamo i due array ordinati A e B e costruiamo l array MG che contiene gli elementi di A e B (con eventuali ripetizioni) in ordine: si considerano le prime componenti, quella più piccola viene inserita nell array MG, e si considera la sua successiva; quando uno dei due è terminato, basta ricopiare l altro (par. 3.5). A: 1 5 6 8 10 B: 0 1 3 4 MG: 0 1 1 3 4 5 6 8 10 Per ordinare le due parti di n/2 componenti, usiamo in maniera ricorsiva la stessa strategia: dividere a metà, per poi fondere le parti ordinate, proseguendo fino ad un array di un solo elemento (par. 9.5). La dimensione limite è h=2: se n<2 c è un solo elemento. Vediamo quindi il progetto dell algoritmo mergesort per ordinare un array v dalla componente p alla componente q. 6

algoritmo mergesort(v, p, q) se p<q allora medio (p+q)/2 //troncata chiamare mergesort(v, p, medio) chiamare mergesort(v, medio+1, q) chiamare merge(v, p, medio, q) finese finemergesort Per gestire la fusione pensiamo all array diviso in due parti, da p a medio, e da medio+1 a q; inoltre usiamo un array di supporto s per appoggiare le componenti di v in ordine. Progetto dell algoritmo di fusione (merge) algoritmo merge(v,p,medio,q) h p i p k medio+1 mentre h medio e k q eseguire se v[h] v[k] allora s[i] v[h] h h+1 altrimenti s[i] v[k] k k+1 finese i i+1 finementre //ricopiare la parte di array non esaminata se h = medio+1 allora copiare in s la seconda parte altrimenti copiare in s la prima parte finese ricopiare s sul v //fine merge Esercizio. Implementare gli algoritmi quicksort e mergesort ed eseguire le prove dei tempi o contare le chiamate ricorsive. 7

Complessità. Contiamo i confronti tra gli elementi dell array: 0 se n = 0,1 T(n) = D(n) + C(n) + T(n/2) + T(n/2) D(n) = 0 calcolo di medio C(n) è la complessità dell algoritmo di fusione C(n) = O(n): vengono considerati tutti gli elementi delle due parti lunghe n/2 Il numero di confronti è sempre lo stesso perché anche se l array è ordinato si esegue sempre la divisione a metà e la fusione delle due parti; le partizioni sono bilanciate: T(n) = n + T(n/2) + T(n/2) = n + 2T(n/2) = = n + 2(n/2 + 2T(n/4)) = 2n + 2 2 T(n/2 2 ) = =.. = k n + 2 k T(n/2 k ) se n = 2 k allora k = log 2 n O(n log 2 n) e Confrontiamo i due algoritmi. L algoritmo mergesort ha la complessità più bassa nel caso peggiore O(nlog 2 n); esegue però molte ricopiature per eseguire la fusione. L algoritmo quicksort ha caso peggiore O(n 2 /2), ma nel caso favorevole e medio è O(nlog 2 n). Alcuni linguaggi (Java) implementano un algoritmo sort per il problema dell ordinamento utilizzando: l ordinamento per inserimento per n<7, e quicksort negli altri casi. Albero delle chiamate ricorsive. v0 v1 v2 v3 v4 v5 v6 v7 1 0 8 5 4 3-1 9 0 1 5 8 1 0 8 5 4 3-1 9 0 1 5 8 1 0 8 5 4 3-1 9 1 0 8 5 4 3-1 9 8

Altri algoritmi per Fibonacci Altri algoritmi per Fibonacci I due seguenti algoritmi si basano sulle matrici. Consideriamo la matrice A e la matrice A n-1 : si può dimostrare per induzione 1 1 A= che la matrice A n-1 = A A.. A 1 0 n-1 volte 1 1 n-1 F n F n-1 A n-1 = = 1 0 F n-1 F n-2 Altri algoritmi per Fibonacci Esercizio. Verificare la formula per n=2, 3, 4. Si può allora costruire un algoritmo che calcola la potenza n-esima di A, matrice M, e restituisce il primo elemento di M, corrispondente a F n : intestazione funzione fibonacci4 (n intero) intero i M I //matrice identità per i da 1 a n-1 eseguire M M * A //fineper restituire M[0][0] //finealgoritmo Altri algoritmi per Fibonacci La complessità di tempo è O(n): infatti c è una struttura iterativa per calcolare il prodotto tra due matrici di dimensione 2 2 (un numero finito di prodotti e somme) La complessità di spazio è O(1): le matrici A, M, I occupano una quantità di spazio costante. Il prossimo algoritmo è una ottimizzazione dell algoritmo precedente: si può eseguire la potenza n-esima con un algoritmo basato sui quadrati. 9

Altri algoritmi per Fibonacci Esempio. Vogliamo calcolare 4 8. 4 8 = 4 4. 4 8 4 2 = 16 16 2 = 4 4 = 256 256 2 = 4 8 quindi in 3= log 2 8 passi abbiamo eseguito il calcolo. In generale: M n = ( M n/2 ) 2 con n pari Altri algoritmi per Fibonacci poiché la divisione è troncata, se n è dispari (n/2) 2 è uguale a ((n-1)/2) 2 = (n-1) occorre perciò un altra moltiplicazione per M. intestazione funzione fibonacci5 (n intero) M I chiama potenzamatrice(m, n-1) restituire M[0][0] //finealgoritmo Altri algoritmi per Fibonacci intestazione algoritmo potenzamatrice(matrice M, intero n) se n>1 allora potenzamatrice(m, n/2) M M * M //finese se n è dispari allora M M * A //finese //finealgoritmo La complessità di tempo è O(log 2 n) La complessità di spazio è O(1). Puntatori o riferimenti 10

Puntatori o riferimenti Puntatori o riferimenti Un puntatore è una variabile che contiene l indirizzo di un area di memoria. Quando si definisce una variabile, questa viene allocata in memoria e viene individuata dal suo nome. È possibile anche accedere alla variabile tramite il suo indirizzo. Per definire in C++ una variabile puntatore si utilizza il simbolo * e si indica il tipo di dato dell area di memoria. Sintassi. nometipo *nomepuntatore; Esempio. int i; i=5; int *p; p 5 i Puntatori o riferimenti Con la definizione int *p; si dice che p è un puntatore ad un area di memoria di tipo int. Pertanto p può contenere l indirizzo di memoria di una variabile di tipo int. Per ottenere l indirizzo di memoria di una variabile già allocata, si utilizza l operatore & operatore indirizzo_di Puntatori o riferimenti Per collegare il puntatore p all area allocata per la variabile i eseguiamo l assegnazione: p=&i; p In tale modo i si può accedere ad i anche tramite p: *p è il contenuto dell area vista da p. 5 11

Puntatori o riferimenti Tramite il puntatore p possiamo variare il contenuto dell area relativa alla variabile i. Esempio. *p = 27; 27 cout<<i; il valore stampato per i sarà 27. p i 12