Introduzione Algoritmi e Strutture Dati ✦ Strutture dati viste finora ✦ ✦ Capitolo 10 - Code con priorità e insiemi disgiunti Sequenze, insiemi e dizionari Strutture “speciali” ✦ ✦ Le operazioni base (inserimento, cancellazione, lettura, etc.) possono non essere sufficienti: a volte è necessario inventarsene di nuove Se non tutte le operazioni sono necessarie, è possibile realizzare strutture dati più efficienti, “specializzate” per particolare compiti Alberto Montresor Università di Trento This work is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike License. To view a copy of this license, visit http://creativecommons.org/licenses/by-nc-sa/2.5/ or send a letter to Creative Commons, 543 Howard Street, 5th Floor, San Francisco, California, 94105, USA. 1 © Alberto Montresor Code con priorità ✦ ✦ Alberi binari L'idea ✦ ✦ Una struttura dati – detta heap – che mantiene dati in modo parzialmente ordinato Albero binario perfetto ✦ Utilizzazioni ✦ ✦ Code con priorità ✦ ✦ Gli oggetti vengono estratti dalla coda in base alla loro priorità ✦ Algoritmo di ordinamento ✦ Costo computazionale: O(n log n) ✦ Ordinamento sul posto © Alberto Montresor Tutte le foglie hanno la stessa profondità h Nodi interni hanno grado 2 ✦ Albero binario completo ✦ ✦ Un albero perfetto ✦ Heapsort ✦ 2 © Alberto Montresor 3 ✦ Ha altezza ⎣log N⎦ ✦ Altezza h → #nodi= 2h+1-1 © Alberto Montresor Tutte le foglie hanno profondità h o h-1 Tutti i nodi a livello h sono “accatastati” a sinistra Tutti i nodi interni hanno grado 2, eccetto al più uno 4 Alberi binari heap ✦ Max heap Un albero binario completo è un albero max-heap sse ✦ ✦ ✦ Alcune informazioni sugli alberi heap 16 Ad ogni nodo i viene associato un valore A[i] ✦ A[p(i)] ≥ A[i] 14 Un albero binario completo è un albero min-heap sse ✦ Ad ogni nodo i viene associato un valore A[i] ✦ A[p(i)] ≤ A[i] 10 8 7 9 3 ✦ 2 4 ✦ ✦ Le definizioni e gli algoritmi di max-heap sono simmetrici rispetto a min-heap 5 Vettore heap ✦ ✦ A[2] 14 A[3] 10 A[4] A[5] A[6] A[7] Come è organizzato? A[1] contiene la radice p(i) = ⎣i/2⎦ ✦ l(i) = 2i ✦ ✦ Antisimmetrico: se n≥m e m≥n, allora m=n ✦ Transitivo: se n≥m e m≥r, allora n≥r Ordinamenti parziali 10 {a,b} {a} {b} ∅ Utili per modellare gerarchie complesse o mantenere informazioni parziali ✦ Nozione più debole di un ordinamento totale... ✦ Ma più semplice da costruire 6 8 7 Ricapitolando: ✦ A[1] 16 ✦ Vettore A[1..n] ✦ Riflessivo: Ogni nodo è ≥ di se stesso 14 © Alberto Montresor ✦ Come è fatto? ✦ ✦ 16 Vettore heap E' possibile rappresentare un albero binario heap tramite un vettore heap (oltre che tramite puntatori) ✦ Un albero heap è un ordinamento parziale ✦ 1 © Alberto Montresor ✦ Un albero heap non impone alcuna relazione di ordinamento fra i figli di un nodo 9 Procedure per gestire heap ✦ maxHeapRestore - O(log n) ✦ ✦ 3 A[8] 2 4 1 A[10] A[9] Un algoritmo che mantiene la proprietà di max-heap heapBuild - O(n) ✦ ✦ r(i) = 2i+1 Vettore max-heap: A[i] ≥ A[2i], A[i] ≥ A[2i+1] Un algoritmo che costruisce un max-heap da zero heapsort - O(n log n) ✦ Ordina sul posto un vettore n = 10 16 14 10 8 7 9 3 2 4 1 © Alberto Montresor 7 © Alberto Montresor www.xkcd.com 8 in input. maxHeapRestore() ✦ maxHeapRestore(I TEM[ ] A, integer i, integer dim) Input ✦ Un vettore A e un indice i ✦ Gli alberi binari con radici l(i) e r(i) sono max-heap ✦ E' possibile che A[i] sia minore di A[l(i)] o A[r(i)] ✦ ✦ maxHeapRestore() integer max ⇥ i if l(i) dim and A[l(i)] > A[max] then max ⇥ l(i) if r(i) dim and A[r(i)] > A[max] then max ⇥ r(i) if i ⌅= max CAPITOLO 10.then CODE CON PRIORITÀ E INSIEMI DISGIUNTI A[i] ⇤ A[max] maxHeapRestore(A, max, dim) Benché la procedura sort() abbia complessità ottima se si usa uno hea In altre parole: il sottoalbero con radice i può non essere un max-heap Scopo ✦ Ripristinare la proprietà di max-heap sul sottoalbero con radice i ✦ Facendo “scendere” l'elemento A[i] nel vettore © Alberto Montresor svantaggio di richiedere un doppio trasferimento di elementi dal vettore A heap di nuovo al vettore A, con conseguente spreco di memoria. È possib l’ordinamento in loco, cioè direttamente nel vettore A senza un valutare secondo v Per valutare la complessità di heapBuild (), dobbiamo dopo aver(), “ristrutturato” stesso in modo verifichi ledel proprietà un so m pRestore che dipendeAda i e dim. Siache m l’altezza nodo idinel perheap cui è A[i necessario un max-heap sarà chiarito lo . . . dim], definita come segue:a breve. l’altezza di una foglia 9 maxHeapRestore() ✦ ✦ 5, 15, 12, 9, 10, 7, 4, 3, 6, 8, 2 ✦ E' un vettore max-heap? ✦ E' possibile applicare maxHeapRestore() alla radice? Domanda: ✦ ✦ soddisfi le proprietà di max-heap. Poiché maxHeapRestore() lavora assum Si notisiano che in heap di n elementi ci dalle sonofoglie al piùe risalire ⌥n/2m+1 giàuno max-heap, dobbiamo partire verso ✦ sottostanti Principio generale maxHeapRestore () O(m) è richiamata su elementi compresi frarichiede ⌅n/2⇧ + 1tempo e n sono fogliequando nell’albero B; sono quind ✦ Sia A[1..n] un vettore da ordinare per✦ definizione, essendo composti dadell'albero un solo eelemento. Applicando Tutti i nodi A[⎣n/2⎦+1 ... n] sono foglie quindi heap di un elemento maxH da tendocuidainiziare ⌅n/2⇧ e risalendo fino ad 1, si restaurano le proprietà di max-hea i sottoalberi siano max-heap (infatti, o sono foglie o sono nodi su cui abb ✦ La procedura heapBuild() attraversa i restanti nodi dell'albero ed esegue maxHeapRestore ()). maxHeapRestore() Domanda: Esempio di funzionamento ✦ Qual è la complessità in tempo di maxHeapRestore()? heapBuild(I TEM[ ] A, integer n) Domanda ✦ 10 d Dato ilche vettore . . . n] in cuiègli elementi sono disposti in un ordine q un nodo non A[1 è una foglia uguale al massimo delle altezze bile permutarli richiamando maxHeapRestore() più volte in modo che al te procedura heapBuild() si basa su una discesa dal nodo i fino ad una foglia, essa © Alberto Montresor for integer i ⇥ ⌅n/2⇧ downto 1 do maxHeapRestore(A, i, n) Dimostrare la correttezza di maxHeapRestore() © Alberto Montresor 11 Invece di operare su una struttura di dati a parte e su associazioni e versione di maxHeapRestore() utilizzata qui lavora direttamente sul vettor 12 © Alberto Montresor • Conclusione: Al termine, i = 0. Quindi il nodo 1 è una radice di uno heap. 1.2 heapBuild() complessità di heapBuild() è ✦ Domanda: Esempio di funzionamento ✦ ✦ n 2m+1 m=0 14, 45, 28, 34, 15, 20, 12, 30, 21, 25, 16, 22 ⌅ ·m , Domanda: che è delloCorrettezza stesso ordine di grandezza di ✦ ✦ ✦ O ⇤log n ⇧ Esprimere un'invariante di ciclo Dimostrare la sua correttezza Domanda – Complessità ✦ Qual è la complessità di Dato che heapBuild()? ⇤ O n log ⇧n m 2m m=0 34 log ⇧n 45⌅ . 15 1.2 14 Domanda 5.6 - Costo computazionale build-max-heap() Complessità Domanda 5.6 che - Costo computazionale Abbiamo detto max-heapify() costabuild-max-heap() O(log n); viene eseguito n/2 volte. Quindi una stim costo computazionale è O(n log n). Ma questa stima non è stretta. Il fatto è che i passi vengono ese ✦ max-heapify() Abbiamo detto che costa O(log n);vengono viene eseguito n/2 una stima del Le operazioni maxHeapRestore() eseguite su volte. heap diQuindi altezza variabile su heap di altezza variabile. costo computazionale è O(n log n). Ma questa stima non è stretta. Il fatto è che i passi vengono eseguiti ✦ Viene eseguito ⎡n/2⎤ volte su heap di altezza 0 eseguito n/2 volte su heap di altezza 0 su heapViene di altezza variabile. Viene eseguito n/4 voltedisualtezza heap di 1 Viene eseguito n/2 volte su heap 0 altezza ✦ Viene eseguito ⎡n/4⎤ volte su heap di altezza 1 Viene eseguito n/8 voltedisualtezza heap di Viene eseguito n/4 volte su heap 1 altezza 2 ✦ Viene ... Viene eseguito n/8 volte su heap di altezza 2 volte su heap di altezza 2 eseguito ⎡n/8⎤ 28 h ... Viene eseguito n/2 + 1 volte su heaph+1di altezza h. ⎡n/2 h. ⎤ volte su heap di altezza h Viene eseguito n/2h +✦ 1...Viene volte su eseguito heap di altezza Quindi si ha la seguente relazione: ✦ Da cui si deduce Quindi si ha la seguente relazione: 20 12 ⇥log n⇤ ⇥log n⇤ ⇥ n ⇥log n⇤ ⇥log ⇥n⇤ ⇥ ⇥ ⇥log n⇤ ⇥log n⇤ h+1 h ⇥ n ⇥ ⇥ ⇥ h = n (1/2) h = n/2 (1/2) h ⇤ n/2 1/2)h h = O(n) h+1 h h h+1 h = n (1/2) h = n/2 (1/2) h ⇤ n/2 1/2) h = O(n) 2 h=1 h=1 h=1 h=1 2h+1 1 ⇧ m m h=1 h=1 2 < = ⇥2 = 2 m m 1 2 230 21 125 16 22 Vi ricordo Vi ricordo che: ✦che: m=0 m=0 2 Ricordate: h=1 ⇥ ⇥ hx = h xh= hx h=1 x la complessità di heapBuild() è O(n). (1 x)2 (1 x)2 h=1 h=1 Terminata la ristrutturazione, l’elemento massimo di A[1 . . . se n]xsi< se trova 1. x Poiché = 1/2,La consegue che la sommatoria dà origine aldàvalore 2, che moltiplicato n/2 <in 1. xA[1]. Poiché xne =posi1/2, ne consegue che la sommatoria origine al valore 2, cheper moltiplicato per n. zione che gli compete è A[n]; facendo un’operazione deleteMaxdà (),come l’elemento in A[n] viene dàrisultato come risultato n. 13 14 sistemato nello heap A[1 . . . n 1] e A[n] diviene libera. È quindi possibile scrivere l’elemento Ordinamento heap massimo in tramite A[n]. L’operazione può quindi essere ripetuta con lo heap A[1 . . . nheapsort() 1], poi con lo heap A[1 . . . n 2], e cosı̀ via fino allo heap A[1 . . . 2]; terminata quest’ultima operazione, 1 1 ✦ Intuizione ✦ Domanda: Esempio di funzionamento il minimo si trova in A[1]. 45 ✦ Il primo elemento dello heap è sempre il massimo ✦ 14, 45, 28, 34, 15, 20, 12, 30, 21, 25, 16, 22 La procedura di ordinamento risultante è detta heapsort() ed è dovuta a Williams (1964); ✦ Andrebbe collocato nell'ultima posizione si noti che invece di chiamare max() per leggere il massimo e una deleteMax() per rimuoverlo, ✦ Domanda: Correttezza ad✦ ogni passoinilultima primo e l’ultimo elemento vengono scambiati e maxHeapRestore () viene L'elemento posizione? In testa! ✦ Esprimere un'invariante di ciclo 34 28 chiamata direttamente. ✦ Chiama maxHeapRestore() per ripristinare la situazione ✦ ✦ © Alberto Montresor Per x < 1 © Alberto Montresor Dimostrare la sua correttezza heapsort(I TEM[ ] A, integer n) heapBuild(A, n) for integer i ⇥ n downto 2 do A[1] ⇤ A[i] maxHeapRestore(A, 1, i 1) ✦ Domanda: Complessità ✦ Qual è la complessità di heapsort()? 14 La chiamata iniziale a heapBuild() costa (n). Poiché lo heap A[1 . . . n] ha n elementi, 15 l’altezza della radice (il nodo 1) è O(log n). Ciascuna chiamata a maxHeapRestore () sul- © Alberto Montresor 30 © Alberto Montresor 25 21 15 22 16 12 20 16 solo percorso radice-foglia, entrambi possono essere realizzati in modo da richiedere al più O(log n) passi. Code con priorità Vediamo ora in dettaglio le procedure in pseudocodice che realizzano la coda con priorità basata su heap. Per semplicità, le priorità saranno rappresentate da numeri interi, ma qua✦ Coda con priorità lunque tipo dotato di una relazione di ordinamento ⇥ potrebbe andare bene. La struttura di ✦ dati è mantenuta in undati vettore di tipoun P RIORITY Una struttura cheheap servediarecord mantenere insiemeI TEM S di; ognuno di questi record contiene, oltre ad una coppia elemento-priorità, la posizione attuale elementi x, ciascuno con un valore associato di priorità del record all’interno del vettore. Questa informazione è necessaria per realizzare decrease(). Esercizi ✦ Esercizio ✦ ✦ Esercizio ✦ ✦ Scrivere un'implementazione iterativa di maxHeapRestore() (in pseudo-codice) P RIORITY I TEM integer priorità I TEM valore integer pos Esercizio ✦ ✦ Dimostrare che uno heap con n nodi ha altezza θ(log n) Dimostrare che il tempo di esecuzione nel caso peggiore di maxHeapRestore() è Ω(log n) Oltre al vettore heap H, la struttura di dati contiene un intero capacità che indica la dimensione massima del vettore, inizializzata al momento di costruire la struttura di dati, e un intero dim che indica quanti elementi sono attualmente presenti nel vettore e corrisponde alla posizione (nel vettore heap) della foglia di B che sta più a destra fra quelle di livello massimo. Esercizio ✦ Implementare heapsort() nel vostro linguaggio preferito 170 % Priorità % Elemento % Posizione nel vettore heap Algoritmi e Strutture di Dati © Alberto Montresor 17 insert() rimane inalterato e le operazioni max(), deleteMax() e increase() sostituiscono le altre operazioni. Specifica Code con priorità P RIORITY Q UEUE u % Crea una coda con priorità vuota PriorityQueue() % Restituisce true se la coda con priorità è vuota boolean isEmpty() Esempio di utilizzo u Simulatore event-driven u Ad ogni evento è associato un timestamp di esecuzione u Ogni evento può generare nuovi eventi, con timestamp arbitrari u % Restituisce l’elemento minimo di una coda con priorità non vuota I TEM min() % Rimuove e restituisce il minimo da una coda con priorità non vuota I TEM deleteMin() u Una coda con min-priorità può essere utilizzata per eseguire gli eventi in ordine di timestamp Esempi di organizzazione p 3 ev % Inserisce l’elemento x con priorità p in una coda con priorità non piena % Restituisce un oggetto P RIORITY I TEM che identifica x all’interno della coda P RIORITY I TEM insert(I TEM x, integer p) p 7 ev % Diminuisce la priorità dell’elemento identificato da x portandola al valore p decrease(P RIORITY I TEM x, integer p) © Alberto Montresor 18 © Alberto Montresor 19 © Alberto Montresor evento p 5 ev p3 p4 p5 p6 p8 p7 ev ev ev ev ev ev 20 caso pessimo, la risalita continua fino alla radice (i = 1). La procedura termina restituendo Il valore 0radicati in dim indica una coda con priorità vuota. Laradice funzione min() è molto semplice: se toalberi nei figli sinistro e destro della rispettano la proprietà (3), mentre l’unica l’oggetto P RIORITY I TEM in cui l’elemento è stato inserito. laCode coda con priorità non è vuota, è sufficiente restituire il primo elemento del vettore. con priorità Inserimento possibile eccezione è proprio la radice, che potrebbe essere maggiore dei propri figli. La funzione swap(), oltre a scambiare le posizioni i e P RIORITY I TEM insert(I TEM x, integer p) posizione dei record mano a mano che vengono spostati P RIORITY Q UEUE minHeapRestore(A, i, dim) ha il compito di rettificare questa situazione; La procedura parprecondition: dim < capacità integer capacità di elementi nella coda te dall’assunto che gli alberi radicati in % l(i)Numero e r(i) massimo (se esistenti) rispettino già le proprietà dello swap(P RIORITY I TEM[ ] H, integer i, integer j) dim dim + 1 integer dim % Numero attuale di elementi nella coda heap, e verifica se uno dei figli di i è minore di i stesso. Se questo accade (i ⌅= min), gli H[i] ⇤ H[j] H[dim] new P RIORITY I TEM() P RIORITY I TEM[ ] H % Vettore heap H[i].pos ⇥ i elementi in A[i] e A[min] vengono scambiati. A questo punto, il problema può essersiH[dim].valore spostato x P RIORITY Q UEUE PriorityQueue(integer n) H[j].pos ⇥ j nella posizione che Qi UEUE sottoalberi radicati in l(min) e r(min) sono H[dim].priorità heap (lo p P RIORITY Qmin: UEUE tsiamo newcerti P RIORITY H[dim].pos dim t.capacità n erano prima di eseguire questa chiamata di minHeapRestore()), ma è possibile che l’elemento integer i dim t.dim 0 Per realizzare deleteMin() useremo una procedura a min violi la proprietà (3) dello heap, ovvero che A[min] sia maggiore di A[l(min)] o A[r(min)]. while i > 1 and H[i].priorità < H[p(i)].priorità do nella sezione successiva. deleteMin() s t.H new P RIORITY I TEM[1 . . . n] utilizzata anche La procedura minHeapRestore() viene quindi chiamata ricorsivamente su min. La chiamata swap (H, i, p(i)) scriviamo la prima posizione dello heap (quella che co return t ricorsiva termina quando i e i suoi figli rispettano le proprietà di uno heap. i p(i) si trova nella foglia più a destra del livello massimo, e d I TEM min() numero di elementi presenti è diminuito di 1. L’albero return H[i] precondition: dim > 0 Il codice della deleteMin() e della minHeapRestore() è mostrato di seguito; si noti che return H[1].valore deleteMin () chiama minHeapRestore() sulla radice, subito dopo aver eseguito lo scambio con l’ultimo elemento. toalberi radicati nei figli sinistro e destro della radice risp possibile eccezione è proprio la radice, che potrebbe ess La procedura minHeapRestore(A, i, dim) ha il comp te dall’assunto che gli alberi radicati in l(i) e r(i) (se esi Nella insert(), si incrementa il valore di dim per far spazio al nuovo elemento x. Questo Concludiamo con la procedure decrease(), che prende in input un oggetto di tipo P RIORI heap, e verifica se uno dei figli di i è minore di i stes corrisponde ad aggiungere una foglia nel livello massimo di B, nella posizione più a sinistra 176 Algoritmi e Strutture di D elementi in A[i] e A[min] vengono scambiati. A questo TY Iquelle TEM e ne aggiorna la priorità, riposizionandolo nel vettore heap se necessario. Poiché la 22 p ©fra Alberto Montresor disponibili. In questa posizione, l’elemento x potrebbe non rispettare la proprietà21 © Alberto Montresor nella posizione min: siamo certi che i sottoalberi radic priorità è diminuita, viene quella del padre scambiata asedim; necessario. (3), ovvero potrebbe essere più confrontata piccolo di suocon padre. Il cursore i vienee inizializzato erano prima di eseguire questa chiamata di minHeapRes priorità Rimozione del minimo e riduzione priorità seCode i noncon coincide con-laminHeapRestore() radice e l’elemento contenuto in H[i] è più piccolo di quello di suo min violi la proprietà (3) dello heap, ovvero che A[min] s padre, questi vengono scambiati dall’operazione swap() e il cursore i risale alla posizione del La procedura minHeapRestore() viene quindi chiamata minHeapRestore (P RIORITY I TEM [ ] A, integer i, integer dim) rispettata. Nel padre p[i]. Il ciclo while si ripete fino a quando la proprietà (3) non viene I TEM deleteMin(I TEM x) ricorsiva termina quando i e i suoi figli rispettano le prop casointeger pessimo, min la risalita ⇥ i continua fino alla radice (i = 1). La procedura termina restituendo l’oggetto P RIORITY I TEM in cui l’elemento è stato inserito. if l(i) dim and A[l(i)].priorità < A[min].priorità then min ⇥ l(i) if r(i) I TEM dim and A[r(i)].priorità P RIORITY insert (I TEM x, integer p) < A[min].priorità then min ⇥ r(i) if i ⌅= min then precondition: dim < capacità swap (A, i, min) dim dim + 1 minHeapRestore H[dim] new P RIORITY(A, I TEMmin, () dim) swap(H, 1, dim) dim ⇥ dim 1 minHeapRestore(H, 1, dim) return H[dim + 1] H[dim].valore x H[dim].priorità p H[dim].pos dim integer i dim while i > 1 and H[i].priorità < H[p(i)].priorità do swap(H, i, p(i)) i p(i) return H[i] © Alberto Montresor precondition: dim > 0 decrease(P RIORITY I TEM x, integer p) Il codice della deleteMin() e della minHeapRestore deleteMin() chiama minHeapRestore() sulla radice, subi l’ultimo elemento. Concludiamo con la procedure decrease(), che prend e ne aggiorna la priorità, riposizionandolo nel priorità è diminuita, viene confrontata con quella del pad TY I TEM minHeapRestore(P RIORITY I TEM[ ] A, integer i, integ integer min ⇥ i precondition: p < x.priorità if l(i) dim and A[l(i)].priorità < A[min].priorità x.priorità ⇥ p if r(i) dim and A[r(i)].priorità < A[min].priorità integer i ⇥ x.pos if i ⌅= min then while i > 1 and H[i].priorità < H[p(i)].priorità do i, min) swap(A, swap(H, i, p(i)) minHeapRestore(A, min, dim) i ⇥ p(i) 23 © Alberto Montresor 24 Code con priorità ✦ Esercizio ✦ ✦ Struttura dati per insiemi disgiunti ✦ Qual è la complessità delle operazioni sulle code con priorità? Motivazioni ✦ Esercizio ✦ ✦ In alcune applicazioni siamo interessati a gestire insiemi disgiunti CAPITOLO di oggetti 10. CODE CON PRIORITÀ E INSIEMI DISGIUNTI Scrivere lo pseudocodice di una versione heapBuild() basata sulla insert() Esempio: componenti di un grafo Operazioni fondamentali: e un’operazione di unione (che unisce due componenti distinte X S unire più insiemi nuova componente X ⇤Y , distruggendo X e Y e lasciando inalterate tutte identificare l'insieme a cui appartiene un oggetto La diversità rispetto all’usuale tipo di dato insieme consiste nel fatto Struttura dati operazioni di inserimento o di cancellazione; tutti gli elementi vengono i S = { S1, S2disgiunta , ..., Sn } di insiemi dinamici disgiunti divisiUna in collezione una componente per elemento, e le successive operazi che leOgni componenti rimangano disgiunte. insieme è identificato da uncomunque rappresentante univoco Nel seguito, assumeremo che l’insieme generativo sia costituito dag 1 e n; in gran parte delle applicazioni pratiche, questa scelta non ridu struttura, in quanto insiemi di dimensione costante n possono essere ma 26 stessa dimensione, e i valori 1 . . . n rappresentano gli indici di tale vettor seguente specifica: Primitive degli insiemi disgiunti (Merge-Find) ✦ ✦ Le due procedure creano lo stesso heap? Dimostrare o produrre un esempio contrario ✦ ✦ ✦ Qual è la complessità di questa versione di heapBuild()? ✦ ✦ Esercizio ✦ ✦ L'azione di delete(PRIORITYITEM x) cancella l'elemento x dallo heap. Scrivete una versione di delete() che operi in tempo O(log n). © Alberto Montresor ✦ 25 Scelta del rappresentante ✦ ✦ ✦ M FSET Il rappresentante può essere un qualsiasi membro dell’insieme Si ✦ Operazioni di ricerca del rappresentante su uno stesso insieme devono restituire sempre lo stesso oggetto % Crea n componenti {1}, . . . , {n} M FSET Mfset(integer n) Solo in caso di unione con altro insieme il rappresentante può cambiare % Restituisce il rappresentante della componente contenente x integer find(integer x) Il rappresentante può essere un elemento specifico dell’insieme ✦ ✦ © Alberto Montresor % Unisce le componenti che contengono x e y merge(integer x, integer y) Si devono definire le caratteristiche degli insiemi e una regola per caratterizzare il rappresentante Esempio: l’elemento più piccolo/grande di un insieme © Alberto Montresor 27 M FSET(n) inizializza la struttura di dati, creando n componenti, ognu ne uno degli interi fra 1 e n. La funzione find(x) restituisce l’identificat contenente x; tale funzione, applicata a due elementi della stessa comp re lo stesso identificatore; applicata a due elementi contenuti in compo 28 © Alberto Montresor Esempio ✦ Possiamo fare meglio? La struttura d disgiunti, uno per nodo. Ogni volta che si a due componenti associate ad u e v sono disg Determinare le componenti connesse di un grafo non orientato dinamico Poiché utilizzando la compressione dei per costante e viene ripetuta m volte, e la creaz Algoritmo totaledadiunquesto algoritmo è O(m + n), n ✦ Si inizia con componenti connesse costituite unico vertice struttura M FSET ci permette di gestire facil ✦ L'operazione merge(find(u), find(v)) viene eseguita per ogni arco (u,v) in questa particolare versione di cc() la st ✦ Alla fine, avremo l'insieme delle componenti connesse chiamante. ✦ Complessità Esempio: come utilizzare Merge-Find mfset(6) 1 2 3 4 5 6 merge(1,2) 1 1, 2 2 3 4 5 6 merge(3,4) 1 1, 2 2 3 3,4 4 5 6 merge(5,6) 1 1, 2 2 3 3,4 4 5 5,6 6 merge(1,3) 1 1, 2 2, 3, 34 merge(1,5) 1 2 4 ✦ 5 5,6 6 ✦ ✦ 1, 2, 3 3, 4, 45, 6 5 6 M FSET cc(G RAPH G) M FSET M Mfset(|G.V |) foreach u ⇥ G.V () do foreach v ⇥ G.adj(u) do M.merge(u, v) Costo: O(n) + m operazioni merge() Questo algoritmo è interessante per la capacità di gestire grafi dinamici (in cui gli archi vengono aggiunti) return M 29 © Alberto Montresor Implementazione di Union-Find ✦ ✦ ✦ find(): O(1); merge(): O(n) merge(): O(1); find(): ✦ Ogni elemento nella lista contiene: O(n) ✦ un oggetto ✦ un puntatore all’elemento successivo ✦ un puntatore al rappresentante Algoritmi basati su euristiche di bilanciamento ✦ Realizzazione basata su liste + Euristica sul peso ✦ Realizzazione basata su alberi + Euristica sul rango # head tail Algoritmi finale ✦ Alberi + rango + compressione dei percorsi © Alberto Montresor 31 Reality check Code con priorità La simulazione è una Ogni insieme viene rappresentato con unafedelmente lista concatenata possibile, il comportamento di ✦ Il primo oggetto di una lista è il rappresentante sistemi dell’insieme che possono essere simulati con su Realizzazione basata su alberi ✦ ✦ ✦ Realizzazione basata su liste ✦ 10.3 Realizzazione basata su liste Algoritmi elementari: ✦ 30 © Alberto Montresor © Alberto Montresor c h e b Può essere visto come un albero di altezza 1 32 Realizzazione basata su liste ✦ L’operazione find(x) richiede tempo O(1) ✦ ✦ QuickFind: Esempio - merge(h, g) Si restituisce il rappresentante di x c L’operazione merge(x,y) è più complessa: ✦ Si “appende” la lista che contiene y alla lista che contiene x ✦ I puntatori ai rappresentanti della lista “appesa” devono essere modificati ✦ Costo nel caso pessimo per n operazioni: O(n2) ✦ Costo ammortizzato: O(n) ✦ merge(2,1), merge(3,1), merge(4,1), etc. h f 33 © Alberto Montresor Realizzazione basata su alberi (foreste) ✦ Si rappresenta ogni insieme tramite un albero radicato ✦ Ogni nodo dell'albero contiene ✦ ✦ ✦ ✦ l'oggetto ✦ un puntatore al padre Nota: generalmente migliore, non più veloce delle liste nel caso pessimo © Alberto Montresor d z f g d z c h e b 34 © Alberto Montresor ✦ Operazioni e costo ✦ find(x) ✦ h b ✦ f c Il rappresentante è la radice dell'albero La radice ha come padre un puntatore a se stessa g b Realizzazione basata su alberi Implementazione basata su foresta ✦ e e ✦ d Risale la lista dei padri di x fino a trovare la radice e restituisce la radice come oggetto rappresentante Costo: O(n) nel caso pessimo merge(x,y) ✦ Appende l'albero radicato in y ad x ✦ Costo: O(1) g 35 © Alberto Montresor 36 Realizzazione basata su alberi: Esempio – merge(c, f) f c h e b Realizzazione basata su alberi: Esempio – merge(c, f) c d h g b f c e f e d c d d g g g 37 © Alberto Montresor Considerazioni ✦ ✦ Quando le merge() sono rare e le find() frequenti Realizzazione basate su alberi? ✦ ✦ ✦ Realizzazione basata su liste? ✦ 38 © Alberto Montresor Realizzazione basata su liste: Euristica peso Quando usare.... ✦ f e Una strategia per diminuire il costo dell’operazione merge(): ✦ Memorizzare l’informazione sulla lunghezza della lista ✦ Appendere la lista più corta a quella più lunga ✦ La lunghezza della lista può essere mantenuta in tempo O(1) Quando le find() sono rare e le merge() frequenti E' importante sapere che esistono tecniche euristiche che permettono di migliorare questi risultati: ✦ 4 Fino a operazioni in tempo ammortizzato O(1) per tutte le utilizzazioni pratiche © Alberto Montresor 2 c 39 © Alberto Montresor h e b f g 40 Realizzazione basata su liste: Euristica peso ✦ Realizzazione basata su alberi: Euristica sul rango Una strategia per diminuire il costo dell’operazione merge(): Ogni nodo mantiene informazioni sul proprio rango ✦ ✦ Memorizzare l’informazione sulla lunghezza della lista ✦ Appendere la lista più corta a quella più lunga ✦ La lunghezza della lista può essere mantenuta in tempo O(1) il rango rango[x] di un nodo x è il numero di archi del cammino più lungo fra x e una foglia sua discendente ✦ rango ≡ altezza del sottoalbero associato al nodo ✦ Unione di due alberi con rango diverso ✦ c h 6 c h e b f e + f d = Unione di due alberi con rango uguale ✦ c h c e f h d b e + f d = c h e f g b b g b d g 41 © Alberto Montresor Compressione dei cammini ✦ ✦ ✦ Utilizzata durante le operazioni find(x) L'albero viene modificato in modo che ricerche successive di x possano completare in O(1) ✦ ✦ Esempio: operazione find(f) Quando si utilizzano entrambe le euristiche: ✦ Il rango non è più l'altezza del nodo ... ✦ ...ma il limite superiore all'altezza del nodo In altre parole: ✦ ✦ b e d 42 Compressione dei cammini + rango Compressione dei cammini: idea ✦ © Alberto Montresor b f d ✦ e Le operazioni di compressione dei cammini NON riducono il rango Sarebbe troppo complesso mantenere le informazioni di altezza corrette In ogni caso, non è necessario f © Alberto Montresor 43 © Alberto Montresor 44 y rango[ry ] = rango[rx ], si sceglie arbitrariamente quale albero rendere figlio dell’altro e si meglio. euristica, chiamata compressione dei percorsi, la l’indice rx Un’ulteriore e rendendo (ex) radice ry figlia radice rx . inconsiste 1 alladella relativa posizione rango. nel modificare Realizzazione basata suquindi alberi la + rango +aggiunge compressione dei cammini Complessità find() in modo che alteri l’albero durante la sua esecuzione, cosı̀ da rendere più vantaggiose le integer y) che viene incontrato da M FSET esecuzioni successive di find() e mergemerge (). In(integer pratica,x,ogni nodo find() ora la complessità di: Valutiamo r ⇥ find (x) x integer[ ] p nel percorso di risalita dal generico nodo x alla radice, viene reso figlio della radice. Per Liste far + Euristica sul peso r ⇥ find(y) y integer[ ] rango questo, la chiamata ricorsiva all’interno di find() è modificata introducendo un assegnamento al Alberi + Euristica sul rango if rx ⇤= ry then padre del nodo x,n)se questo è diverso da x. In tal modo, durante l’esecuzione, restano “sospesi” Mfset (integer if rango[rx ] > rango[ry ] then gli assegnamenti dei nodi nel percorso verso la radice. Quando conAlberi la + Euristica sul rango + Compressione dei cammini M FSET t sui newpadri M FSET p[ry ]di⇥risalita rx chiamata più.interna radice, la ricorsione di find () termina restituendo la if rango[r t.p ricorsiva integer[1 . . n] si giunge alla else y ] > rango[r x ] then Alla lavagna! p[r ] ⇥ r y radicet.rango stessa. La integer[1 radice viene “a ritroso”x ai padri in tutti gli assegnamenti “sospesi”, . . . assegnata n] else Ne segue via via si richiudono tutte È importante notare che il numero di che tutte queste implementazioni (e in particolare l'ultima) sono forche integer i 1 to n do le chiamate ricorsive. p[rx ] ⇥ ry ampiamente utilizzabili in pratica t.p[i] i operazioni della nuova versione di find() aumenta solo per un fattore costante rispetto alla rango[r y ] ⇥ rango[ry ] + 1 t.rango[i] vecchia versione e che 0pertanto la sua complessità nel caso pessimo non aumenta. return t Per giustificare la bontà di questa scelta, cerchiamo di calcolare una limitazione superiore integer find integer find(integer (integer x) x) u u u u u u ififp[x] p[x]⌅==x xthen then return x (p[x]) p[x] ⇤ find else p[x] return © Alberto Montresor return find(p[x]) all’altezza dell’albero. Teorema 10.1. Un albero M FSET con radice r ottenuto tramite euristica sul rango ha almeno 2rango[r] nodi. 45 © Alberto Montresor Dimostrazione. Procediamo per induzione sul numero di operazioni merge() effettuate. All’inizio (nessuna operazione merge()), tutti gli alberi hanno rango 0 ed esattamente 1 nodo; merge(integer x, integer y) poiché 20 = 1, il teorema è dimostrato nel caso base. Consideriamo l’(n + 1)-esima operaEsempio (Compressione dei percorsi). dell’esecuzione find() sia convera la comrx 10.10 find(x) zione mergeL’effetto (x, y), e supponiamo che la di proprietà per tutte le operazioni precedenti. Siano Y gli insiemi contenenti x e y, con X ⇤= Y ; siano rango[rx ] e rango[ry ] il rango delle pressione percorsi è mostrato nella Fig.X,10.6. ry dei find (y) radici r e x ry , rispettivamente; sia rango[r] il rango della radice risultante dalla (n + 1)-esima if rx ⇥= ry then operazione merge(). Sono dati tre casi: p[ry ] rx Il vantaggio introdotto dalla compressione dei percorsi si ha quando viene effettuata una rango[r abbiamo rango[r] = rango[rx ]; inoltre, per ipotesi induttiva: y ] < rango[r x ]: m sequenza di operazioni formata da m•find () e merge (), con ⇥ n.che Senza la compressione dei percorsi, la complessità dovuta all’esecuzione|X della sarebbe ovviamente x] y] ⌅ Y sequenza | = |X| + |Y | 2rango[r + 2rango[rO(n >+ 2rango[rx ] = 2rango[r] m log n), mentre con la compressione dei percorsi èrango[r possibile dimostrare che tale costo scende y] (in quanto 2 > 0) ad O(n +Euristica m (m, n)),del dove (m, n) è la funzione inversa della funzione di Ackermann. 10.2.3 rango rango[rx ] < rango[r y ]: simmetrico (m, n) è una funzione che cresce•lentissimamente, tanto che (m,del n) precedente; 4 per ogni m ed Nella realizzazione basata su foresta, la creazione di un M FSET ha costo O(n), che è ovvia• rango[rx ] =Ne rango[r la radice ry prende rx come nuovo figlio, e il suo rango viene n rappresentabile con un calcolatore elettronico! seguey ]:che ciascuna delle m operazioni mente ottimale; mentre il costo di find() e merge () dipende dall’altezza degli alberi. Purtroppo, incrementato di uno: rango[r] =+ rango[r 1. Dall’ipotesi y ] +n))/m), find() e merge() della sequenza ha complessità “ammortizzata” O((n m (m, che èinduttiva, abbiamo che: è possibile scegliere una sequenza particolarmente sfortunata di unioni, che renda l’albero una pressoché O(1) per ogni valore “realistico” di n |X ed ⌅ m.Y | = |X| + |Y | 2rango[ry ] + 2rango[ry ] = 2rango[ry ]+1 = 2rango[r] lista di lunghezza n; in questo caso, le operazioni find() and merge() hanno costo O(n). Esempio 10.9 (Alberi di altezza massima). Nella merge(x, y), l’albero contenente y diviene sempre figlio dell’albero contenente x; considerando il M FSET di Fig. 10.5(a), la sequenza di operazioni merge(5, 6), merge(4, 5), merge(3, 4), merge(2, 3), merge(1, 2) produce un albero 46