Strutture Di Dati e Algoritmi

374

Transcript of Strutture Di Dati e Algoritmi

Page 1: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 1/373

Page 2: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 2/373

Page 3: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 3/373

 

Pierluigi Crescenzi

Giorgio Gambosi

Roberto Grossi

Strutture di dati e algoritmiProgettazione, analisi e visualizzazione

Page 4: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 4/373

 

  Ad Antonella, Paola e Susanna  A Benedetta, Federica, Giorgia, Martina e Nicole

  A Roberta

Page 5: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 5/373

 

Sommario

Prefazione XIII 

1 Problemi computazionali 1 1.1 Indecidibilità di problemi computazionali 3 1.2 Trattabilità di problemi computazional i 6 

1.2.1 Rappresentazione e dimensione dei dati 10 1.2.2 Algoritmi polinomiali ed esponenziali 11 

1.3 Problemi NP-completi 14 1.4 Modello RAM e complessità computazionale 18 

2 Sequenze: array 23 2.1 Sequenze lineari 23 

2.1.1 Modalità di accesso 24 2.1.2 Allocazione della memoria 25 

2.1.3 Array di dimensione variabile 27 2.2 Opus libri: scheduling della CPU 28 

2.2.1 Ordinamento per selezione 30 2.2.2 Ord inamento per inserimento 31 

2.3 Complessità di problemi computazionali 32 2.3.1 Limiti superiori e inferiori 36 

2.4 Ricerca di una chiave 37 2.4.1 Ricerca binaria 37 2.4.2 Complessità della ricerca per confronti 40 

2.5 Ricorsione e paradigma del divide et impera 40 2.5.1 Equazioni di ricorrenza e teorema fondamentale 42 2.5.2 Moltiplicazione veloce di due numeri interi 43 

2.5.3 Ord inamento per fusione 46 2.5.4 Ordinamento e selezione per distribuzione 49 

2.5.5 Alternativa al teorema fondamentale delle ricorrenze 54 2.6 Opus libri: grafica e moltiplicazione di matrici 55 

Page 6: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 6/373

 

2.6.1 Moltiplicazione veloce di due matrici 60 

2.6.2 Sequenza ott ima di moltiplicazioni e paradigma della program-mazione dinamica 62

2.7 Paradigma della programmazione dinamica 69 

2.7.1 Sicurezza dei sistemi e sotto-sequenza comune più lunga . . . . 71 

2.7 .2 Sistemi di backup e partizione di un insieme di interi 75 

2.7.3 Problema della bisaccia 77 

2.7.4 Pseudo-polinomialità e programmazione dinamica 80 

3 Sequenze: liste 83 

3.1 Liste 83 

3.1.1 Ricerca, inserimento e cancellazione 84 

3.1.2 Liste doppie e liste circolari 86 

3.2 Opus libri: problema dei matrimoni stabili 89 

3.2.1 Strutture di dati utilizzate 91 

3.2.2 Implementazione dell'algoritmo 92 3.3 Liste randomizzate 94 

3.4 Opus libri: gestione di liste ammortizzate e ad auto-organizzazione . . . 99 

3.4.1 Unione e appartenenza a liste disgiunte 99 

3.4 .2 Liste ad auto-organizzazione 102 

3.4 .3 Tecniche di analisi ammortizzata 108 

4 Alberi 113 

4.1 Alberi binari 113 4.1.1 Algoritmi ricorsivi su alberi binari 116 

4.1.2 Inserimento e cancellazione 123 

4.2 Opus libri: min imo antenato comune 125 

4.2.1 Trasformazione da antenat i comuni a min imi in intervalli . . . 1 27

4.2 .2 Soluzione efficiente in spazio 129 

4.3 Visita per ampiezza e rappresentazione di alberi 131 

•4.3.1 Rappresentazione implicita di alberi binari 133 

4.3.2 Rappresentazione succinta per ampiezza 136 4.3 .3 Implementazione di rank e select 138 

4.3 .4 Limite inferiore allo spazio delle rappresentazioni succinte . . . 142

4.4 Alberi cardinali e ordinali, e parentesi bilanciate 143 

4.4.1 Rappresentazione succinta mediante parentesi bilanciate . . . . 146

Page 7: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 7/373

 

5 Dizionari 151 5.1 Dizionari 151 5.2 Liste e dizionari 152 5.3 Opus libri: funzioni hash e peer-to-peer 154 

5.3.1 Tabelle hash: liste di trabocco 157 5.3.2 Tabelle hash: indirizzamento aperto 158 

5.4 Opus libri: kernel Linux e alberi binari di ricerca 161 5.4.1 Alberi binari di ricerca 162 5.4.2 AVL: alberi binari di ricerca bilanciati 165 

5.5 Opus libri: basi dati e B-alberi 170 5.6 Opus libri: liste invert ite e trie 177 

5.6.1 Trie o alberi digitali di ricerca 183 5.6.2 Trie compatti e alberi dei suffissi 190 

6 Grafi 199 6.1 Grafi 199 

6.1.1 Alcuni problemi su grafi 205 

6.1.2 Rappresentazione di grafi 208 6.1.3 Cammin i minimi , chiusura transitiva e prodotto di matrici . . 2 1 3

6.2 Opus libri: colorazione di grafi e algoritmi golosi 215 6.2.1 II problema dell'assegnazione delle lunghezze d'onda 216 6.2.2 Grafi a intervalli 217 

6.2.3 Colorazione di grafi a intervalli 219 6.2.4 Massimo insieme indipendente in un grafo a intervalli 221 6.2.5 Paradigma dell'algoritmo goloso 224 

6.3 Grafi casuali e modelli di reti complesse 225 6.3.1 Grafi casuali alla Erdòs-Rényi 228 6.3.2 Grafi casuali con effet to di piccolo mondo 230 6.3 .3 Grafi casuali invarianti di scala 235 

6.4 Opus libri: motori di ricerca e classificazione 239 6.4.1 Significatività delle pagine con PageRank 241 6.4.2 Significatività delle pagine con HI TS 246 

6.4.3 Convergenza del calcolo iterativo di PageRank e HI TS 249 

7 Pile e code 253 7.1 Pile 253 

7.1.1 Implementazione di una pila mediante un array 254 7.1.2 Implementazione di una pila mediante una lista 255 

7.2 Opus libri: Postscript e notazione postfissa 256 

Page 8: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 8/373

 

7.3 Code 259 7.3.1 Implementazione di una coda mediante un array 260 7.3.2 Implementazione di una coda median te una lista 260 

7.4 Opus libri: Web crawler e visite di grafi 261 7.4 .1 Visita in ampiezza di un grafo 262 7.4 .2 Visita in pro fondi tà di un grafo 267 

7.5 Applicazioni delle visite di grafi 2707.5.1 Grafi diretti aciclici e ordinamento topologico 2707.5.2 Com ponent i (fortemente) connesse 273 

8 Code con priorità 281 8.1 Code con priorità 281 8.2 Heap 283 

8.2.1 Implementazione di uno heap implicito 285 8.2.2 Insolito caso di DecreaseKey 288 

8.2.3 Costruzione di heap e ord inamento 289 8.3 Opu s libri: routing su Internet e cammini minimi 293 

8.3.1 Problema della ricerca di cammini min imi su grafi 295 8.3.2 Cammin i minimi in grafi con pesi positivi 297 

8.3 .3 Ca mmin i minimi in grafi pesati generali 302 8.4 Opus libri: data mining e min imi alberi ricoprenti 306 

8.4.1 Problema della ricerca del min imo albero di ricoprimento . . . 3 0 88.4.2 Algoritmo di Kruskal 310 

8.4.3 Algoritmo di Jarnik-Prim 312 

9 NP-completezza 317 

9.1 Problemi NP-completi 317 9.1.1 Classi P e NP 318 9.1.2 Riducibilità polinomiale 322 9.1.3 Problemi NP-completi 324 9.1.4 Teorema di Cook-Levin 326 

9.1.5 Problemi di ottimizzazione 328 9.2 Esempi e tecniche di NP-completezza 329 

9.2.1 Tecnica di sostituzione locale 329 9.2.2 Tecnica di progettazione di component i 331 

9.2.3 Tecnica di similitudine 334 9.2 .4 Tecnica di restrizione 334 9.2 .5 Co me dimostra re risultati di NP-completezza 335 

9.3 Algoritmi di approssimazione 337 9.4 O pu s libri: il problema del commesso viaggiatore 338 

Page 9: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 9/373

 

9.4.1 Problema del commesso viaggiatore su istanze metriche . . . . 341

9.4.2 Paradigma della ricerca locale 344 

A Notazioni 351 

B Teorema delle ricorrenze 35 3 

Indice analitico 355 

Page 10: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 10/373

 

Prefazione

Ottimi testi su algoritmi presumono che il lettore abbia già sviluppato una capacità diastrazione tale da poter recepire la teoria degli algoritmi con un taglio squisitamente ma-tematico. Altri ottimi testi, ritenendo che il lettore abbia la capacità di intravedere qualisono gli schemi programmativi adatti alla risoluzione dei problemi, danno più spazio agliaspetti implementativi con un taglio pragmatico e orientato alla programmazione.

Il nostro libro cerca di combinare questi due approcci, ritenendo che lo studenteabbia già imparato i rudimenti della programmazione, ma non sia ancora in grado di

astrarre i concetti e di riconoscere gli schemi programmativi da utilizzare. Mirato ai corsidei primi due anni nelle lauree triennali, il testo segue un approccio costruttivistico cheagisce a tre livelli, tutti strettamente necessari per un uso corretto del libro:

• partendo da problemi reali, lo studente viene guidato a individuare gli schemiprogrammativi più adatti: gli algoritmi presentati sono descritti anche in unopseudocodice molto vicino al codice reale (ma comunque di facile comprensione);

• sviluppato il codice, ne vengono analizzate le proprietà con un taglio più astrat-to e matematico, al fine di distillare l'algoritmo corrispondente e studiarne la

complessità computazionale;

• utilizzando l'ambiente di visualizzazione ALVIE, viene mostrato l'algoritmo inazione e viene reso possibile sia eseguirlo su qualunque insieme di dati di esempiosia modificarne il comportamento, se necessario.

Gli argomenti classici dei corsi introduttivi di algoritmi e strutture di dati (comearray, liste, alberi e grafi, ricorsione, divide et impera, programmazione dinamica e al-goritmi golosi) sono integrati con argomenti e applicazioni collegate alle tecnologie piùrecenti. Uno degli obiettivi del libro è infatti quello di integrare teoria e pratica in modo

proficuo per l'apprendimento, fornendo al contempo agli studenti una chiara percezionedella significatività dei concetti e delle tecniche introdotte nella risoluzione di proble-mi attuali e ai docenti un insieme di motivazioni, nell'introduzione di tali concetti etecniche, le quali possono essere di ausilio nelle attività di didattica frontale.

Page 11: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 11/373

 

Quest'integrazione tra tecniche algoritmiche e applicazioni dà luogo nel testo a mo-menti di vera e propria opera di progettazione di strutture di dati e di algoritmi, deno-minata opus libri, in particolare, il libro esplora i seguenti domini applicativi, in ordinedi trattazione:

complessità dei giochi • scheduling della CPU • grafica computerizzata • si-curezza dei sistemi • matrimoni stabili e abbinamento di risorse • politicaLRU e ad auto-organizzazione • minimo antenato comune e flussi informa-

tivi • rappresentazione succinta di documenti XML • crittografia • sistemipeer-to-peer • kernel di Linux e gestione della memoria virtuale • sistemi digestione delle basi dati • sistemi di recupero delle informazioni • assegna-zione delle lunghezze d'onda nelle reti • reti complesse di piccolo mondo einvarianti di scala • motori di ricerca nel Web • link analysis e classificazionedei documenti • Postscript e notazione polacca • Web crawling ed esplo-razione di grafi • logistica e pianificazione di attività • routing di pacchettisu Internet • data mining e cluster analysis • ottimizzazione dei trasporti

I tre livelli di apprendimento costruttivistico, a cui abbiamo fatto riferimento inprecedenza, vengono applicati a tali argomenti, descrivendoli in modo semplice e mo-strandone l'impatto nella progettazione efficiente ai fini delle prestazioni ottenute, lequali sono misurate in relazione a un modello di calcolo di riferimento (nel nostro caso,la Random Access Machine).

La classificazione degli argomenti segue l'approccio moderno alla programmazione,in cui l'organizzazione delle strutture di dati è centrale (C+ + Standard Template Library,

  Java Collectionse Library of Efficient Data types and Algorithms, nota come LEDA). Latrattazione non è però vincolata a un linguaggio di programmazione specifico, ma risulta

comprensibile sia agli studenti con maggiore familiarità per i linguaggi strutturati di tipoprocedurale, sia a quelli che posseggono una buona conoscenza della programmazione aoggetti.

Inoltre, l'apertura e l'estendibilità dell'ambiente di visualizzazione ALVIE, rendendopossibile l'introduzione al suo interno di nuove visualizzazioni, consentono al docenteinteressato di introdurre le strutture di dati e gli algoritmi su esse operanti usando l'ap-proccio tipico della programmazione a oggetti. In ogni caso, il docente può decidere omeno se utilizzare tale paradigma programmativo, senza pregiudicare la fruibilità degliargomenti trattati nel testo.

Infine, siamo pienamente coscienti che non esiste il libro perfetto in grado di soddi-sfare tutt i i docenti: il sapere odierno è sempre più dinamico, variegato e distribuito e unsemplice libro non può catturare le mille sfaccettature di una disciplina scientifica in con-tinua evoluzione. Per questo al libro è associato il sito Web h t t p : / / a l g o r i t m i c a . o r gin cui i docenti possono trovare ulteriore materiale didattico (come integrazioni al testo

Page 12: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 12/373

 

e lucidi in PowerPoint) e a cui possono contribuire, in modo collaborativo e verificato,sullo stile di iniziative quali Wikipedia per ampliare e approfondire i contenuti del libro,mettendo a disposizione estensioni di ALVIE per quanto riguarda sia nuove funzionalitàofferte da tale sistema che nuove visualizzazioni. Il sito Web non è quind i un semplicearchivio in cui trovare lucidi o esempi, ma un'estensione a tutto campo del libro comemostrato dalla decisione di pubblicare, in forma elettronica e aperta a tutti, ALVIE e ilrelativo fascicolo didattico piuttosto che allegarlo al libro.

Guida per il docente. Versioni preliminari del libro e di ALVIE sono state sperimentatecon successo in corsi e laboratori di algoritmi e strutture di dati della laurea triennale inInformatica e in Matematica e della laurea specialistica in Fisica (ma riteniamo che iltesto sia perfettamente adatto anche alla laurea triennale in Ingegneria). "Scorrevole maimpegnativo" è stato il commento più diffuso tra i colleghi che cortesemente hanno lettouna versione preliminare dei capitoli. Per quanto riguarda gli studenti, questi ultiminon hanno segnalato particolari difficoltà degli argomenti principali e hanno apprezzatol'attualità delle applicazioni, presentate attraverso ciascun opus libri. Per questo motivo,auspichiamo che i docenti valutino l'approccio del nostro libro "sul campo", con i propristudenti, in aggiunta all'usuale valutazione "a priori" del testo.

Forniamo di seguito alcuni percorsi didattici che permettono al docente di orga-nizzare corsi da 4, 6, 9 e 12 crediti formativi universitari (CFU). Ognuna delle ultimequattro righe rappresenta uno di tali percorsi, mentre le colonne rappresentano i para-grafi del libro raggruppati per capitoli (ricordiamo di affiancare tali percorsi con l'uso delsoftware ALVIE e la consultazione del sito Web).

CFUCap.l Cap. 2 Cap. 3 Cap. 4 Cap. 5 Cap. 6 Cap. 7 Cap. 8 Cap. 9

CFU1.1-1.4'2.1^.5i2.6 2.7 3.1:3.2 3.313.4 4.1 4.2Ì4.3 4.4 5.1-5.4 5.5 5.6 6.1 6.2 6.3 6.47.1,7.2'7.3 7.47.5 8.1 8.2 8.3 8.49.1 9.2 9.3 9.4

4

69

12

In generale, il nostro libro non segue l'approccio consolidato (seguito anche da noistessi per anni) di introdurre prima un problema in termini formali e teorici, per poipresentarne la soluzione algoritmica, la dimostrazione di correttezza e l'analisi di com-plessità, mediante opportuni teoremi e lemmi. Abbiamo infatti preferito non presumereche i lettori siano esclusivamente motivati dalla pura speculazione teorica nello studiodegli algoritmi: invece, abbiamo cercato di compensare un divario tra le applicazioni

e i problemi, stimolando anche le abilità programmative degli studenti. Speriamo intal modo di catalizzare l'attenzione di tutti gli studenti dei primi anni, inclusi coloroche appaiono meno interessati per qualcosa che risulta loro essere astratto rispetto allatecnologia odierna, rendendoli curiosi e interessati a esplorare gli aspetti variegati e affa-scinanti dell'algoritmica teorica che hanno reso possibile il progresso di molte tecnologie

Page 13: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 13/373

 

informatiche. Per facilitare comunque la ricerca di definizioni, proprietà e dimostrazionicontenute nel libro, il nostro sito Web include un glossario delle definizioni e un indicedelle proprietà e delle dimostrazioni.Legenda. Nel libro impieghiamo tre icone a margine del testo per segnalare particolarisituazioni al lettore:

L'icona a sinistra indica che l'argomento t rat tato è ulteriormente dettagliato sul sito Web,mediante note bibliografiche, riferimenti a pagine Web ed eventuale materiale didatticoaggiuntivo. L'icona al centro indica che un esercizio a fine capitolo completa l'argo-mento trattato, per cui ne consigliamo lo svolgimento. Infine, l'icona a destra segnalaun'argomentazione più teorica del solito.

Ringraziamenti. Siamo profondamente grati a tutti gli studenti, che con il loro entusia-smo e con le loro osservazioni ci hanno permesso di migliorare il testo finale e l'ambiente

di visualizzazione. Quest'ult imo non avrebbe potuto essere fruibile senza il fondamentalelavoro di tesi di Carlo Nocentini, a cui va il nostro caloroso ringraziamento.

Ringraziamo i seguenti colleghi e amici per aver letto, con spirito critico, versionipreliminari di alcuni capitoli: Anna Bernasconi, Paolo Cignoni, Valentina Ciriani, An-drea Clementi, Miriam Di Ianni, Paolo Ferragina, Gianni Franceschini, Antonio Gullì,Michele Loreti, Fabrizio Luccio, Alessio Malizia, Donatella Merlini, Linda Pagli, PaoloPenna, Nadia Pisanti, Guido Proietti, Geppino Pucci, Romeo Rizzi, Gianluca Rossi eCecilia Verri.

Ringraziamo, inoltre, Lorenzo Davitti, per avere interpretato così bene le nozioni

di albero, grafo, pila, coda e così via, riuscendo a illustrarle nell'immagine di copertina,e per aver dato vita al personaggio di ALVIE, e Bruna Parra per averci assistito nelladefinizione della veste tipografica del libro.

Ringraziamo infine il team della Pearson Education Italia e, in particolare, MicaelaGuerra e Alessandra Piccardo, per il loro aiuto e la pazienza mostrata durante la stesuradel libro e per averci sempre perdonato le innumerevoli scadenze mancate.

Pierluigi CrescenziUniversità degli Studi di Firenze

Giorgio GambosiUniversità degli Studi di Roma "Tor Vergata"

 Roberto GrossiUniversità di Pisa

Giugno 2006 

Page 14: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 14/373

 

Capitolo 1

Problemi computazionali

S O M M A R I O

  Iproblemi computazionali possonoessere classificati in base alla complessità dei relativi algo-ritmi di risoluzione. Questo capitolo offre una visione d'insieme dei temi che riguardano lo

studio degli algoritmi e la difficoltà computazionale intrinseca dei problemi computazionali.

DIFFICOLTÀ

0,5 CFU

Questo libro è rivolto al lettore che ha acquisito i princìpi di base della programmazionee, forte di questa nuova abilità, ha iniziato a esplorare le possibilità offerte dal calcolatore.Tale lettore può oramai trovare naturale il fatto di numerare gli elementi a partire da 0invece che da 1, perché molti linguaggi moderni di programmazione adottano tale stiledi enumerazione; inoltre può aver acquisito dimestichezza con le potenze del 2, per cuiriesce a contare con le dieci dita da 0 fino a 1023 e considera un migliaio di elementi paria 210 = 1024 piuttosto che a 1000; o infine può essersi convinto che il termine settato(spesso affiancato nella sua opera di dissoluzione linguistica da termini come inizializza-re) sia sempre stato il participio passato del verbo settare (una variabile) e non invece unaggettivo che indichi qualcosa "provvisto di setti".

Ebbene, a un tale lettore indirizziamo in questo libro alcune sfide su problemi com-putazionali, ovvero problemi risolvibili al calcolatore mediante algoritmi:1 l'algoritmoè l'essenza computazionale di un programma ma non deve essere identificato con que-st'ultimo, in quanto un programma si limita a codificare in uno specifico linguaggio (di

1 II rermine "algoritmo" deriva dalla traslitterazione latina Algorismus del nome del matematico persianodel IX secolo, Muhammad al-Khwarizmi, che descrisse delle procedure per i calcoli aritmetici. Da notare cheun algoritmo non necessariamente richiede la sua esecuzione in un calcolatore, ma può essere implementato,per esempio, mediante un dispositivo meccanico, un circuito elettronico o un sistema biologico.

Page 15: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 15/373

 

programmazione) i passi descritti da un algoritmo, e programmi diversi possono realizza-re lo stesso algoritmo. Avendo cura di distillare gli aspetti rilevanti ai fini computazionali(trascendendo quindi dal particolare linguaggio, ambiente di sviluppo o sistema operati-vo adottato), possiamo discutere di algoritmi senza addentrarci nel gergo degli hacker edei geek , rendendo così alla portata di molti, concetti utili a programmare in situazionireali complesse.

La progettazione di algoritmi a partire da problemi provenienti dal mondo reale (ilcosiddetto problem solving) è un processo creativo e gratificante, la cui essenza cercheremodi trasmettere al lettore attraverso un apprendistato basato su esempi ragionati che saran-no presentati nei vari capitoli e che chiameremo ciascuno opus libri, intesa come opera diprogettazione algoritmica (considerata la miriade di anglicismi presenti nell'informatica,speriamo che il lettore ci perdonerà questo latinismo).

Gli ingredienti alla base di queste opere algoritmiche saranno semplici, ovvero array,

liste, alberi e grafi come illustrato nella Figura 1.1, ma essi ci permetteranno di struttura-re i dati elementari (caratteri, interi, reali e stringhe) in forme più complesse, in modo darappresentare le istanze di problemi computazionali reali e concreti nel mondo virtualedel calcolatore. Pur sembrando sorprendente che problemi computazionali complessi,come il calcolo delle previsioni metereologiche o la programmazione di un satellite, pos-sano essere ricondotti all'elaborazione di soli quattro ingredienti di base, ricordiamo chel'informatica, al pari delle altre scienze quali la chimica, la fisica e la matematica, cerca diricondurre i fenomeni (computazionali) a pochi elementi fondamentali.

Nel caso dell'informatica, come molti sanno, l'elemento costituente dell'informazio-

ne è il bit, componente reale del nostro mondo fisico quanto l'atomo o il quark: il bitrappresenta l'informazione minima che può assumere due soli valori (per esempio, 0/1,acceso/spento, destra/sinistra o testa/croce). Negli anni '50, lavorando presso i prestigiosilaboratori di ricerca della compagnia telefonica statunitense AT&T Bell Labs,2 il padredella teoria dell'informazione, Claude Shannon, definì il bit come la quantità di informa-zione necessaria a rappresentare un evento con due possibilità equiprobabili, e introdussel'entropia come una misura della quantità minima di bit necessaria a rappresentare uncontenuto informativo senza perdita di informazione (se possiamo memorizzare un filmin un supporto digitale, o se possiamo ridurre il costo per bit di certi servizi di telefonia

cellulare, lo dobbiamo a questo tipo di studi che hanno permesso l'avanzamento dellatecnologia).

L'informazione può essere quindi misurata come le altre entità fisiche e, come que-st'ultime, è sempre esistita: la fondamentale scoperta nel 1953 della "doppia elica" delDNA (l'acido desossiribonucleico presente nel nucleo di tutte le celle), da parte di JamesWatson e Francis Crick, ha infatti posto le basi biologiche per la comprensione della

2Presso gli stessi laboratori furono sviluppati, tra gli altri, il sistema operativo UNIX e i linguaggi di

programmazione C e C++, in tempi successivi.

Page 16: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 16/373

 

3 8 1 13

ao ai 12 in-l

 ZM 

Figura 1.1 Ingredienti di base dì un algoritmo: array, liste, alberi e grafi .

struttura degli esseri viventi da un punto di vista "informativo". Il DNA rappresenta in

effetti l'informazione necessaria alle funzionalità degli esseri viventi e può essere rappre-sentato all'interno del calcolatore con le strutture di dati elencate sopra. In particolare,la doppia elica del DNA è costituita da due filamenti accoppiati e avvolti su se stessi, aformare una struttura elicoidale tridimensionale. Ciascun filamento può essere ricondot-to a una sequenza (e, quindi, a un array oppure a una lista) di acidi nucleici (adenina,citosina, guanina e timina) chiamata struttura primaria: per rappresentare tale sequenza,usiamo un alfabeto finito come nei calcolatori, quaternario invece che binario, dove lelettere sono scelte tra le iniziali delle quattro componenti fondamentali: {A, C, G, T}.La sequenza di acidi nucleici si ripiega su se stessa a formare una struttura secondaria che

possiamo rappresentare come un albero. Infine, la struttura secondaria si dispone nel-lo spazio in modo da formare una struttura terziaria elicoidale, che possiamo modellarecome un grafo.

Nel seguito, citeremo varie fonti per illustrare alcuni concetti fondamentali alla com-prensione del resto del libro, invitando il lettore a visitare il sito web per eventuali appro-fondimenti e a iniziare da subito a usare ALVIE (  Algorithmic Visualization Environment),il nostro strumento software, per visualizzare il comportamento degli algoritmi e dellestrutture di dati in discussione.

1.1 Indecidibilità di problemi computazionaliNel libro Algorithmics: The Spirit of Computing, l'autore David Harel riporta un estrat-to di un articolo della rivista Time Magazine di diversi anni fa in cui il redattore di unperiodico specializzato in informatica dichiarava che il calcolatore può fare qualunquecosa: basta scrivere il programma adatto a tale scopo, in quanto le eventuali limitazionisono dovute all'architettura del calcolatore (per esempio, la memoria disponibile), e noncerto al programma eseguito. Probabi lmente al redattore sfuggiva l'esistenza del prò-

Page 17: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 17/373

 

blema della fermata, pubblicato nel 1937 dal matematico inglese Alan Turing, uno dei

padri dell'informatica che, con la sua idea di macchina universale, è stato il precursore

del moderno concetto di "software".3

Espresso in termini odierni, il problema della fermata consiste nel capire se un gene-rico programma termina (ovvero, finisce la sua esecuzione) oppure "va in ciclo" (ovvero,continua a ripetere sempre la stessa sequenza di istruzioni all'infinito), supponendo dinon avere limiti di tempo e di memoria per il calcolatore impiegato a tal proposito. Peresempio, consideriamo il problema di stabilire se un dato intero p > 1 è un numeroprimo, ovvero è divisibile soltanto per 1 e per se stesso: 2, 3, 5, 7, 11, 13 e 17 sono alcu-ni numeri primi (tra l'altro, trovare grandi numeri primi è alla base di diversi protocollicrittografici). Il seguente programma codifica un possibile algoritmo di risoluzione pertale problema.

Primo ( numero ): {pre: numero > 1)

fattore = 2;

 WHILE (numero °/0 fattore != 0 )

fattore = fattore + 1;

RETURN (fattore == numero);

Tale codice non è particolarmente efficiente: per esempio, potremmo evitare di ve-

rificare che nu me ro sia divisibile per f a t t o r e quand o quest'ultimo è pari. Tuttavia,

siamo sicuri che esso termina perché la variabile f a t t o r e viene incremen tata a ogni

iterazione e la guardia del ciclo nella riga 3 viene sicuramente verificata se f a t t o r e è

uguale a numero.

ALVIE: numeri primi

Osserva, sperimenta e verifica

PrimeNumber

Nel semplice caso appena discusso, decidere se il programma termina è quindi im-

mediato. Pur troppo , non è sempre così, come most rato dal seguente programma il cui

scopo è quello di trovare il più piccolo numero intero pari che non sia la somma di due

numeri primi.

3Per quanto riguarda il problema della fermata, lo stesso Turing nel suo lavoro del 1937 afferma di essersiispirato al primo teorema di incompletezza di Kurt Godei, il quale asserisce che esistono teoremi veri maindimostrabili in qualunque sistema formale che possa descrivere l'aritmetica degli interi.

Page 18: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 18/373

 

CongetturaGoldbach( ):

n = 2;

DO {

n = n + 2;controesempio = TRUE;

FOR  (p = 2; p <= n-2; p = p + 1) {

q = n - p;

IF (Primo(p) &4t Primo(q)) controesempio = FALSE

>> WHILE (¡controesempio);

RETURN n;

Se fossimo in grado di decidere se la funzione C o n g e t t u r a G o l d b a c h termina o

meno, allora avremmo risolto la congettura di Goldbach, formulata nel XVIII secolo, laquale afferma che ogni numero in tero n > 4 pari è la somma di due numeri pri mi pe q. In effetti, il programma tenta di trovare un valore di n per cui la conget tura nonsia vera: se la congettura di Goldbach è però vera, allora il programma non terminamai (ipotizzando di avere tutto lo spazio di memoria necessario). Nonostante il premiomilionario offerto nel 2000 dalla casa editrice britannica Faber&Faber a chi risolvesse lacongettura, nessuno è stato in grado ancora di provarla o di trovarne un controesempio.

Riuscire a capire se un programma arbitrario termina non è soltanto un'impresaardua (come nel caso del precedente programma) ma, in generale, è impossibile per i

calcolatori, come Turing ha dimostrato facendo riferimento al problema della fermata eusando le macchine di Turing (un formalismo alternativo a quello adottato in questolibro).

Ricordiamo che, nei calcolatori, un programma è codificato mediante una sequenza

di simboli che viene data in ingresso a un altro programma (tipicamente un compila-

tore): non deve quindi stupirci il fatto che una stessa sequenza di simboli possa essere

interpretata sia come un programma che come un dato d'ingresso di un altro programma.

Quest'osservazione è alla base del risultato di Turing, la cui dimostrazione procedeper assurdo. Supponiamo che esista un programma Termina(A, D), il quale, preso unprogramma A e i suoi dati in ingresso D, restituisce (in tempo finito) un valore di veritàper indicare che A termina o meno quando viene eseguito sui dati d'ingresso D.

Notiamo che sia A che D sono sequenze di simboli, e siamo noi a stabilire che Adebba essere intesa come un programma mentre D come i suoi dati d'ingresso: è quindiperfettamente legittimo invocare Termina(A, A), come accade all'interno del seguenteprogramma.

Paradosso( A ) :

 WHILE (Terminai A, A ))

Page 19: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 19/373

 

Poiché il corpo del ciclo WHILE è vuoto, per ogni programma A, osserviamo cheParadosso( A) termina se e solo se la guardia Terminai A, A) restituisce il valore FALSE,

ovvero se e solo se il programma A non termina quando viene eseguito sui dati d'in-gresso A. Possiamo quindi concludere che P a r a d o s s o ( P a r a d o s s o ) termina se e so-lo se la guardia Te rm in a( Pa r ad os so , P a r a d o s so ) restituisce il valore FALSE, ovve-ro se e solo se il programma P a r a d o s s o non termina quando viene eseguito sui da-ti d'ingresso P a r a d o s s o . In breve, P a r a d o s s o ( P a r a d o s s o ) termina se e solo seParadosso (Paradosso) non termina!

Questa contraddizione deriva dall'aver assunto l'esistenza di Termina, l'unico anellodebole del filo logico tessuto nell'argomentazione precedente. Quindi, un tale program-ma non può esistere e, pertanto, diciamo che il problema della fermata è indecidibile.Purtroppo esso non è l'unico: per esempio, stabilire se due programmi A e B sonoequivalenti, ovvero producono sempre i medesimi risultati a parità di dati in ingresso,

è anch'esso un problema indecidibile. Notiamo che l'uso di uno specifico linguaggionon influisce su tali risultati di indecidibilità, i quali valgono per qualunque modello dicalcolo che possa formalizzare il comportamento di un calcolatore (più precisamente diuna macchina di Turing).

1.2 Trattabilità di problemi computazionali

L'esistenza di problemi indecidibili restringe la possibilità di progettare algoritmi e pro-

grammi ai soli problemi decidibili. In questo ambito , non tutt i i problemi risultanorisolvibili in tempo ragionevole, come testimoniato dal noto problema delle Torri diHanoi, un gioco del XIX secolo inventato da un matematico francese, Edouard Lucas,legandolo alla seguente leggenda indiana (probabilmente falsa) sulla divinità Brahma esulla fine del mondo. In un tempio induista dedicato alla divinità, vi sono tre pioli di cuiil primo contiene n = 64 dischi d'oro impilati in ordine di diametro decrescente, con ildisco più ampio in basso e quello più stretto in alto (gli altri due pioli sono vuoti) . Deimonaci sannyasin spostano i dischi dal primo al terzo piolo usando il secondo come ap-poggio, con la regola di non poter spostare più di un disco alla volta e con quella di non

porre mai un disco di diametro maggiore sopra un disco di diametro inferiore. Quandoi monaci avranno terminato di spostare tutti i dischi nel terzo piolo, avverrà la fine delmondo.

La soluzione di questo gioco è semplice da descrivere usando la ricorsione. Suppo-niamo di avere spostato ricorsivamente i primi n — 1 dischi sul secondo piolo, usando ilterzo come appoggio. Possiamo ora spostare il disco più grande dal primo al terzo piolo,e quindi ricorsivamente spostare gli n — 1 dischi dal secondo al terzo piolo usando ilprimo come appoggio.

Page 20: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 20/373

 

TorriHanoi( n, primo, secondo, terzo ):

IF (n = 1) {

PRINT primo i—»terzo;

> ELSE {

TorriHanoi( n - 1, primo, terzo, secondo );

PRINT primo i—»terzo;

TorriHanoi( n - 1, secondo, primo, terzo );

>

Dimostriamo, per induzione sul numero n di dischi, che il numero di mosse effet-tuate eseguendo tale programma e stampate come "origine <—» destinazione", è pari a2 n — 1: il caso base n = 1 è immediato; nel caso n > 1 occorrono 2 n ~ ' — 1 mosse perciascuna delle due chiamate ricorsive per ipotesi induttiva, a cui aggiungiamo la mossanella riga 6, per un totale di 2 x (2 n~* — 1 ) + 1 = 2 n — 1 mosse. Pur troppo , non c'èsperanza di trovare un programma che effettui un numero di mosse inferiore a tale quan-tità, in quanto è stato dimostrato che le 2" — 1 mosse sono necessarie e non è possibileimpiegarne di meno.

Nel problema originale con n = 64 dischi, supponendo che ogni mossa richieda un

secondo, occorrono 264 — 1 = 18 446 744 07 3 70 9 551 615 secondi, che equivalgonoa circa 584 942 417 355 anni, ovvero quasi 585 miliardi di anni: per confronto, lateoria del big bang asserisce che l'Universo è stato creato da un'esplosione cosmica in unperiodo che risale a circa 10—20 miliardi di anni fa.

ALVIE: problema delle Torri di Hanoi

Osserva, sperimenta e verifica— — . _ D*oo6tteco5Osco4

HanoiTower

Le Torri di Hanoi mostrano dunque che, anche se un problema è decidibile ovveroè risolubile mediante un algoritmo, non è detto che l'algoritmo stesso possa sempre ri-solverlo in tempi ragionevoli: ciò è dovuto al fatto che il numero di mosse e quindi iltempo di esecuzione del programma, è esponenziale nel numero n di dischi (n appareall'esponente di 2 n — 1). Il tempo necessario per spostare i dischi diventa du nq ue ra-pidamente insostenibile, anche per un numero limitato di dischi, come illustrato nella

seguente tabella, in cui il tempo di esecuzione è espresso in secondi (s), minuti (m), ore(h), giorni (g) e anni (a).

n 5 10 15 20 25 30 35 40 45tempo 32 s 17 m 9 h 1 2 g l a 34 a 1089 a 34865 a 1115689 a

Page 21: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 21/373

 

L'esponenzialità del tempo di esecuzione rende anche limitato l'effetto di eventualimiglioramenti nella velocità di esecuzione dei singoli passi, perché, in tal caso, basta au-mentare di poco il nu mero n di dischi per vanificare ogni miglioramento. Suppon iamoinfatti di poter eseguire m = 2 S operazioni in un secondo, invece di una singola ope-razione al secondo: in tal caso, anziché circa 2 n secondi, ne occorrono 2 n  / m = 2n ~ s

per spostare gli n dischi. L'effetto di tale miglioramento viene però neutralizzato moltorapidamente al crescere del numero di dischi in quanto è sufficiente portare tale numeroa n + s (dove s = log m) per ottenere lo stesso tempo complessivo di esecuzione. In altreparole, un miglioramento delle prestazioni per un fattore moltiplicativo si traduce in unaumento solo additivo del nu mero di dischi trattabili . La tabella seguente esemplificaquesto comportamento nel caso n = 64, mostrando il numero di dischi gestibili in untempo pari a 18 446 744 073 709 551 615 secondi, al variare della velocità di esecu-zione: come possiamo vedere, miglioramenti anche molto importanti di quest'ultima sitraducono in piccoli incrementi del numero di dischi che il programma è in grado digestire.

operazioni /sec 1 10 100 IO3 IO4 IO5 IO6 IO9

numer o dischi 64 67 70 73 77 80 83 93

Di diversa natura è invece l'andamento polinomiale, come possiamo mostrare seconsideriamo la generalizzazione del problema delle Torri di Hanoi al caso in cui sianodisponibil i k > 3 pioli. A tale scopo, suppon iamo che i pioli siano numerati da 0 a k — 1e che il problema consista nello spostare i dischi dal piolo 0 al piolo k — 1 (rispettando

le regole sopra descritte). In tal caso, possiamo usare il codice T o r r i H a n o i come sotto-programma all'interno della seguente soluzione al problema generalizzato (per semplicitàdi esposizione, supponiamo che n > 0 sia un multiplo di k — 2).

TorriHanoiGen( n, k ) : (pre: n > 0 multiplo di k - 2)FOR  (i = 1; i <= k-2; i = i+1)

TorriHanoi(n/(k-2), 0, k-1, i);

FQR  (i = k-2; i >= 1; i = i-1)

TorriHanoiCn/(k-2), i, 0, k-1);

Intuit ivamente, il codice precedente divide gli n dischi in k - 2 gruppi di ^ dischiciascuno. Il primo ciclo sposta, per ogni i, l'v-esimo gruppo dal disco 0 al disco i, usandoil disco k — 1 come appoggio e invocando TorriHanoi, mentre il secondo ciclo spostatale gru ppo dal disco i al disco k - 1 usando il disco 0 come appoggio: noti amo che,per rispettare la regola di sovrapposizione dei dischi, il secondo ciclo scorre i gruppi inordine inverso rispetto al pri mo. Il nu mero di mosse è du nq ue pari a 2 x (k — 2) volteil nu me ro di mosse richiesto per spostare ^ ^ dischi usando tre pioli. Non è difficileestendere il suddetto codice in modo che funzioni per tutti i valori di n (anche quando

Page 22: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 22/373

 

n non è un multiplo di k — 2), osservando che il numero totale di mosse è pari a

M( n, k) = 2 x (k - 2) x ( 2 ^ - l )

Ponendo k* = Lnr^J P e r n ^ 5, possiamo verificare che M(n, k*) ^ n 2 . In tal caso,il problema delle Torri di Hanoi può quindi essere risolto con un numero di mossequadratico nel numero dei dischi (notiamo che è in generale un problema aperto stabilireil numero minimo di mosse per ogni n e per ogni k > 3). Per esempio, volendo spostaregli n = 64 dischi del problema originale usando k* = 10 pioli, sono sufficienti soltanto64 2 = 4096 secondi contro i 264 — 1 = 18 446 744 073 709 551 615 secondi necessarinel caso di tre pioli (supponendo di poter effettuare una mossa al secondo).

Il problema delle Torri di Hanoi con tre o più pioli illustra come una soluzione cherichiede un numero esponenziale di passi risulti irragionevole se confrontata con una

che ne richiede un numero polinomiale. In generale, il passaggio da un andamentoesponenziale a uno polinomiale ha due impor tanti conseguenze. In primo luogo, unpolinomio cresce molto più lentamente di una qualunque funzione esponenziale, comemostrato nella seguente tabella relativa al polinomio n 2 e analoga a quella vista nel casodella funzione 2 n .

n 5 10 15 20 25 30 35 40 45tempo 25 s 100 s 225 s 7 m 11 m 15 m 21 m 27 m 34 m

In secondo luogo, la polinomialità del tempo di esecuzione rende molto più efficacigli eventuali miglioramenti nella velocità di esecuzione dei singoli passi. Ad esempio,nel caso del problema generalizzato delle Torri di Hanoi con n dischi e k* pioli, poten-do eseguire m operazioni in un secondo, invece di una singola operazione al secondo,occorrerebbero ^ = ( n / i / m )2 secondi per spostare gli n dischi. L'effetto di tale miglio-ramento permane a lungo in quanto è necessario portare il numero di dischi a n x y/rnper ottenere lo stesso tempo complessivo di esecuzione. In altre parole, un migliora-mento di un fattore moltiplicativo nelle prestazioni si traduce in un aumento anch'essomoltiplicativo del numero di dischi trattabili. La tabella seguente (analoga a quella vista

nel caso della funzione 2n

) esemplifica questo comportamento nel caso n = 64, mo-strando il numero di dischi gestibili (con k* pioli) in un tempo pari a 4096 secondi, alvariare della velocità di esecuzione: come possiamo vedere, miglioramenti di quest'ulti-ma si traducono in incrementi significativi del numero di dischi che il programma è ingrado di gestire.

operazioni/sec 1 10 100 IO3 IO4 IO5 IO6 IO9

numero dischi 64 202 640 2023 6400 20238 64000 2023857

Page 23: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 23/373

 

1.2.1 Rappresentazione e dimensione dei dati

Volendo generalizzare la discussione fatta nel caso delle Torri di Hanoi a un qualunqueproblema computazionale, è anzitutto necessaria una breve escursione nella rappresenta-zione e nella codifica dei dati elementari utilizzati dal calcolatore. Secondo quanto detto

in riferimento alla teoria dell'informazione di Claude Shannon, il bit (binary digit) se-gnala la presenza (1) oppure l'assenza (0) di un segnale o di un evento con due possibilitàequiprobabili.4

La stessa sequenza di bit può essere interpretata in molti modi, a seconda del signi-ficato che le vogliamo assegnare nel contesto in cui la usiamo: può essere del codice daeseguire oppure dei dati da elaborare, come abbiamo visto nel problema della fermata.In particolare, gli interi nell'insieme {0,1,..., 2 k — 1} possono essere codificati con k bitb]C_]b)C_2 • • • bibo- La regola per trasformare tali bit in un numero intero è semplice:basta moltiplicare ciascuno dei bit per potenze crescenti di 2, a partire dal bit meno signi-

ficativo bo, ot tenendo bi x 2V

. Per esempio, la sequenza 0101 codifica il numerointero 5 = 0 x 2 3 +1 x 22 + 0 x 21 +1 x 2°. La regola inversa può essere data in vari modi,e l'idea è quella di sottrarre ogni volta la massima potenza del 2 fino a ottenere 0. Perrappresentare sia numeri positivi che negativi è sufficiente aggiungere un bit di segno.

I caratteri sono codificati come interi su k = 8 bit (ASCII) oppure su k = 16 bit{Unicode¡UTF8). La codifica riflette l'ordine alfabetico, per cui la lettera 'A' viene codi-ficata con un intero più piccolo della lettera 'Z' (bisogna porre attenzione al fatto che ilcarattere '7' non è la stessa cosa del numero 7). Le stringhe sono sequenze di caratterialfanumerici che vengono perciò rappresentate come sequenze di numeri terminate da

un carattere speciale oppure a cui vengono associate le rispettive lunghezze.I numeri reali sono codificati con un numero limitato di bit a precisione finita di 32 o

64 bit nello standard IEEE754 (quindi sono piuttosto dei numeri razionali). Il primo bitè utilizzato per il segno; un certo numero dei bit successivi codifica l'esponente, mentreil resto dei bit serve per la mantissa. Per esempio, la codifica di —0,275 x 218 è ottenutacodificando il segno meno, l'esponente 18, e quindi la mantissa 0,275 (ciascuno con ilnumero assegnato di bit).

Infine, in generale, un insieme finito è codificato come una sequenza di elementiseparati da un carattere speciale per quell'insieme: questa codifica ci permetterà, se ne-

cessario, di codificare anche insiemi di insiemi, usando gli opportuni caratteri speciali diseparazione.

Le regole di codifica discusse finora, ci consentono, per ogni dato, di ricavarne unarappresentazione binaria: nel definire la dimensione del dato, faremo riferimento allalunghezza di tale rappresentazione o a una misura equivalente.

4 II bit viene usato come unità di misura: 1 byte = 8 bit, 1 kilobyte (KB) = 210 byte = 1024 byte, 1 megabyte(MB) = 210 KB = 1 048 576 byte, 1 gigabyte (GB) = 210 MB = 1 073 741 824 byte, 1 terabyte (TB) = 2 10 GB,1 petabyte (PB) = 210 TB e cosi via.

Page 24: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 24/373

 

Fi gura 1.2 Una prima classificazione dei problemi computazional i decidibili.

1.2.2 Algoritmi polinomiali ed esponenziali

Abbiamo già osservato che, tranne che per piccole quantità di dati, un algoritmo cheimpiega un numero di passi esponenziale è impossibile da usare quanto un algoritmoche non termina! Nel seguito useremo il termine algoritmo polinomiale per indicareun algoritmo, per il quale esiste una costante c > 0, il cui numero di passi elementari

sia al massimo pari a nc

per ogni dato in ingresso di dimensione n. Questa definizioneci porta a una prima classificazione dei problemi computazionali come riportato nellaFigura 1.2 dove, oltre alla divisione in problemi indecidibili e decidibili, abbiamo l'ulte-riore suddivisione di quest'ultimi in problemi trattabili (per i quali esiste un algoritmorisolutivo polinomiale) e problemi intrattabili (per i quali un tale algoritmo non esiste):facendo riferimento alla figura, tali classi di problemi corrispondono rispettivamente aP e EXP — P, dove EXP rappresenta la classe di problemi risolubili mediante un algorit-mo esponenziale, ovvero un algoritmo il cui numero di passi è al più esponenziale nelladimensione del dato in ingresso.5

Talvolta gli algoritmi esponenziali sono utili per esaminare le caratteristiche di alcuniproblemi combinatori sulla base della generazione esaustiva di tutte le istanze di piccolataglia.

Discutiamo un paio di casi, che rappresentano anche un ottimo esempio di uso dellaricorsione nella risoluzione dei problemi computazionali. Nel primo esempio, vogliamo

'Volendo essere più precisi, i problemi intrattabili sono tutti i problemi decidibili che non sono inclusiin P: tra di essi, quindi, vi sono anche problemi che non sono contenuti in EXP. Nel resto di questo libro,tuttavia, non considereremo mai problemi che non ammettano un algoritmo esponenziale.

Page 25: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 25/373

 

generare tutte le 2 n sequenze binarie di lunghezza n, che possiamo equivalentementeinterpretare come tutti i possibili sottoins iemi di un insieme di n elementi.

Per illustrare questa corrispondenza, numeriamo gli elementi da 0 a n— 1 e associamo

il bit in posizione b della sequenza binaria all'e lemento b dell'insieme fornit o (dove 0 ^

b ^ n — 1): se tale bit è pari a 1, l'elemento b è nel sottoinsieme così rappresentato;altrimenti, il bit è pari a 0 e l'elemento non appartiene a tale sottoinsieme.

Durante la generazione delle 2 n sequenze binarie, memorizziamo ciascuna sequenzabinaria A e utilizziamo la procedura Elabora per stampare A o per elaborare il corri-spondente sottoinsieme. No tiamo che A viene riutilizzata ogni volta sovrascrivendoneil cont enut o ricorsivamente: il bit in posizione b, indicato con A[b — 1], deve valereprima 0 e, dopo aver generato tutte le sequenze con tale bit, deve valere 1, ripetendo lagenerazione.

Il seguente codice ricorsivo permette di ottenere tutte le 2 n sequenze binarie di

lunghezza n: inizialmente, dobbiamo invocare la funzione GeneraBinarie con inputb = n.

GeneraBinarie ( A, b ): {pre: i primi b bit in A sono da generare)

IF (b == 0) {

Elaborai A );

> ELSE {

A [b-1] = 0;

GeneraBinarie( A, b-1 );

A [b-1] = 1;

GeneraBinarie( A, b-1 );>

ALVIE: generazione ricorsiva delle sequenze binarie

Osserva, sperimenta e verifica

BinaryStringGeneration

Il secortdo esempio riguarda la generazione delle permutazioni degli n elementi con-

tenuti in una sequenza A. Ciascuno degli n elementi occupa, a turno, l'ultima posizione

in A e i rimanenti n — 1 elementi sono ricorsivamente permutati. Per esempio, volendo

generare tutte le permutazioni di n = 4 elementi a, b, c, d in modo sistematico, pos-

siamo generare prima quelle aventi d in ultima posizione (elencate nella prima colonna),

poi quelle aventi c in ultima posizione (elencate nella seconda colonna) e così via:

Page 26: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 26/373

 

a b c d

b a c d

a c b d

c a b d

c b a db c a d

a b d c

b a d ea d b c

d a b c

d b a c

b d a c

a d c b

d a c b

a c d b

c a d b

c d a bd c a b

d b c a

b d c a

d c b a

c d b. a

c b d ab c d a

Restringendoci alle permutazioni aventi d in ultima posizione (prima colonna), pos-siamo permutare i rimanenti elementi a, b, c in modo analogo usando la ricorsionesu questi tre elementi. A tal fine, not iamo che le permutazioni generate per i primin — 1 = 3 elementi, sono identiche a quelle delle altre tre colonne mostrate sopra. Peresempio, se ridenominiamo l'elemento c (nella prima colonna) con d (nella secondacolonna), otteniamo le medesime permutazioni di n — 1 = 3 elementi; analogamente,

possiamo ridenominare gli elementi b e d (nella seconda colonna) con d e e (nella terzacolonna), rispettivamente. In generale, le permutazioni di n — 1 elementi nelle colonnesopra possono essere messe in corrispondenza biunivoca e, pertanto, ciò che conta sonoil numero di elementi da permutare come riportato nel codice seguente. Invocando talecodice con parametro d'ingresso p = n, possiamo ottenere tutte le n! permutazioni deglielementi in A:

GeneraPermutazioni ( A, p ): {pre: iprimi p elementi di A sono dapermutare)IF (p == 0) {Elaborai A );

> ELSE {FOR  (i = p-1; i >= 0; i = i-1) {

Scambiai i, p-1 );

GeneraPermutazioni( A, p-1 );

Scambiai i, p-1 );

>>

Notiamo l'utilizzo di una procedura Scambia prima e dopo la ricorsione così damantenere l'invariante che gli elementi, dopo esser stati permutati, vengono riportati alloro ordine di partenza, come può essere verificato simulando l'algoritmo suddetto.

ALVIE: generazione ricorsiva delle permutazioni

Osserva, sperimenta e verifica

PermutaiionGeneration

Page 27: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 27/373

 

1.3 Problemi NP-completi

La classificazione dei problemi decidibili nella Figura 1.2 ha in realtà una zona grigialocalizzata tra i problemi trattabili e quelli intrattabili (le definizioni rigorose sarannodate nell'ultimo capitolo del libro). Esistono decine di migliaia di esempi interessanti di

problemi che giacciono in tale zona grigia: di questi ne riportiamo uno tratto dal campodei solitari e relativo al noto gioco del Sudoku.

In tale solitario, il giocatore è posto di fronte a una tabella di nove righe e novecolonne parzialmente riempita con numeri compresi tra 1 e 9, come nell'istanza mostratanella parte sinistra della Figura 1.3. Come possiamo vedere, la tabella è suddivisa innove sotto-tabelle, ciascuna di tre righe e tre colonne. Il compito del giocatore è quellodi riempire le caselle vuote della tabella con numeri compresi tra 1 e 9, rispettando iseguenti vincoli:

1. ogni riga cont iene tutt i i numeri compresi tra 1 e 9;2. ogni colonna contiene tutt i i numeri compresi tra 1 e 9;

3. ogni sotto-tabella contiene tut ti i numeri compresi tra 1 e 9.

Nella parte destra della Figura 1.3 mostriamo una soluzione ottenuta abbastanza fa-cilmente sulla base di implicazioni logiche del tipo: "visto che la sotto-tabella in altoa destra deve contenere un 3, che la prima riga e la seconda riga contengono un 3 eche la nona colonna contiene un 3, allora nella casella in terza riga e settima colonna cideve essere un 3". Tali implicazioni consentono al giocatore di determinare inequivoca-bilmente il contenuto di una casella: notiamo che, nel caso mostrato in figura, in ogni

passo del processo risolutivo, vi è sempre almeno una casella il cui contenuto può esseredeterminato sulla base di siffatte implicazioni.

Tuttavia, le configurazioni iniziali che vengono proposte al giocatore non sono sem-pre di tale livello di difficoltà: le configurazioni più difficili raramente consentono di

3 9 8

7 1 38 4 9 6

1 2 7 96 3

5 3 6 4

4' 1 5 99 8 2

9 4 7

3 9 6 5 1 2 4 7 8

4 7 1 6 8 3 5 9 2

2 5 8 7 4 9 3 6 1

1 3 4 2 7 5 6 8 96 8 7 4 9 1 2 5 3

5 2 9 8 3 6 7 1 4

8 4 2 1 5 7 9 3 6

7 1 3 9 6 4 8 2 5

9 6 5 3 2 8 1 4 7

Figura 1.3 Un esempio di istanza del gioco del Sudoku e la corrispondente soluzione.

Page 28: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 28/373

 

procedere in modo univoco fino a raggiungere la soluzione finale, costringendo pertantoil giocatore a operare delle scelte che possono talvolta rivelarsi sbagliate. Con riferimentoalla Figura 1.4, possiamo procedere inizialmente in modo univoco partendo dalla confi-gurazione nella parte sinistra fino a ottenere la configurazione nella parte destra: a questopunto, non esiste alcuna casella il cui contenuto possa essere determinato in modo uni-voco. Per esempio, la casella in basso a dest ra può contenere sia un 1 che un 3 e nonabbiamo modo di scegliere quale valore includere, se non procedendo per tentativi eannullando le scelte parziali che conducano a un vicolo cieco.

In questi casi, il giocatore è dunque costretto a eseguire un algoritmo di backtrack,in base al quale la scelta operata più recentemente (se non conduce a una soluzione delproblema) viene annullata e sostituita con un'altra scelta possibile (che non sia già stataanalizzata). Quest o modo di procedere è formalizzato nella seguente funzi one ricorsivaSudoku, la quale esamina tutte le caselle inizialmente vuote, nell'ordine implicitamen-

te specificato dalle funzioni P r i m a Vu ot a, S uc cV u ot a e U l t i m a V u o t a (ad esempio,scorrendo la tabella per righe o per colonne): supponendo che la configurazione inizialecontenga almeno una casella vuota, la funzione deve inizialmente essere invocata conargomento la casella restituita da PrimaVuota.

Sudoku( casella ): {pre: casella vuota)elenco = insieme delle cifre ammissibili per casella;

FOR (i = 0; i < I elenco I; i = i+1) {

Assegnai casella, elenco[i] );

IF (!UltimaVuota(casella) && !Sudoku(SuccVuota(casella))) {

Svuotai casella );> ELSE {

RETURN TRUE;

>>RETURN FALSE;

Per ogni casella vuota, il codice calcola l'elenco delle cifre (comprese tra 1 e 9) chein essa possono essere contenute (riga 2): prova dunque ad assegnare a tale casella una

dopo l 'altra tali cifre (riga 4). Se giunge all'ult ima casella della tabella (riga 5), il codicerestituisce il valore TRUE (riga 8):6 in questo caso, una soluzione al problema è stata tro-vata. Altrimenti, invoca ricorsivamente la funzione Sudoku con argomento la prossimacasella vuota e, nel caso in cui l'invocazione ricorsiva non abbia prodotto una soluzioneaccettabile, annulla la scelta appena fa tta (riga 6) e ne prova un'altra. L'intero procedi-

6 Notiamo che k congiunzione di due o più operandi booleani è valutata in modo pigro: gli operandi

sono valutati da sinistra verso destra e la valutazione ha termine non appena viene incontrato un operando

il cui valore sia FALSE. Inoltre, RETURN termina la chiamata di funzione restituendo il valore specificato.

Page 29: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 29/373

 

6 2 96

7 3 1 5 8

4 9 3 6 53 1

5 8 7 9 2

1 5 2 37

6  2 9 4

6 2 98 67 3 1 5 8

4 2 9 3 1 8 6 7 56 7 3 2 1 8 45 1 8 4 6 7 9 3 2

1 5 2 37 1 8 6

6 2 9 7 4

Figura 1.4 Un esempio di istanza difficile del Sudoku e una successiva configurazione senzaproseguimento univoco.

mento ha termine nel momento in cui il codice trova una soluzione oppure esaurisce lescelte possibili (riga 11).

Osserviamo che l'algoritmo sopra esposto esegue, nel caso pessimo, un numero dioperazioni proporzionale a 9 m , dove m ^ 9 x 9 indica il numero di caselle inizial-mente vuote: infatti, per ogni casella vuota della tabella vi sono al più 9 possibili ci-fre con cui tentare di riempire tale casella. In generale, usando n cifre (con n nume-ro quadrato arbitrariamente grande), il gioco necessita di una tabella di dimensionen x n, e quindi l'algoritmo suddetto ha complessità esponenziale, in quanto richiede

circa n m ^ n n X R = 2 n 2 | o 6 n operazioni.A differenza del problema delle Torri di Hanoi, non possiamo però concludere che

il problema del gioco del Sudoku sia intrattabile: nonos tante si conoscano solo algoritmiesponenziali per il Sudoku, nessuno finora è riuscito a dimostrare che tale problema pos-sa ammettere o meno una risoluzione mediante algoritmi polinomiali. Un'evidenza delladiversa natura dei due problemi dal punto di vista della complessità computazionale,deriva dal fatto che, quando il secondo problema ammette una soluzione, esiste sempreuna prova dell'esistenza di una tale soluzione che possa essere verificata in tempo poli-nomiale (al contrario, non esiste alcun algoritmo polinomiale di verifica per il problema

delle Torri di Hanoi).

Supponiamo infatti che, stanchi di tentare di riempire una tabella di dimensionen x n pubblicata su una rivista di enigmistica, incominciamo a nutrire dei seri dubbi sulfatto che tale tabella ammetta una soluzione. Per tale motivo, decidiamo di rivolgercidirettamente all'editore chiedendo di convincerci che è possibile riempire la tabella. Eb-bene, l'editore ha un modo molto semplice di fare ciò, inviandoci la sequenza delle cifreda inserire nelle caselle vuote. Tale sequenza ha chiaramente lunghezza m ed è quindipolinomiale in n: inoltre, possiamo facilmente verificare la correttezza del problema pro-

Page 30: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 30/373

 

posto dall'editore, riempiendo le caselle vuote con le cifre della sequenza come riportatonel codice seguente.

Verif icaSudoku( sequenza ): (pre: sequenza di m. cifre, con 0 < m ^ n2)casella = PrimaVuotaC );

FOR  (i = 0; i < m; i = i+1) {cifra = sequenza [i];

IF (cifra appare in casella.riga) RETURN FALSE;

IF (cifra appare in casella.colonna) RETURN FALSE;

IF (cifra appare in casella.sotto-tabella) RETURN FALSE;

Assegnai casella, cifra );

casella = SuccVuota(casella);

>RETURN TRUE;

Notiamo che le tre verifiche alle righe 5 - 7 possono essere eseguite in circa n passi, percui l'intero algoritmo di verifica richiede circa m x n ^ n 3 passi, ed è quindi polinomiale.In conclusione, verificare che una sequenza di m cifre sia una soluzione di un'istanza delSudoku può essere fatto in tempo polinomiale mentre, ad oggi, nessuno conosce unalgoritmo polinomiale per trovare una tale sequenza. Insomma, il problema del Sudokusi trova in uno stato di limbo computazionale nella nostra classificazione della Figura 1.2.

ALVIE: il problema del Sudoku

Osserva, sperimenta e verificaSudoku

Tale problema non è un esempio isolato ma esistono decine di migliaia di proble-mi simili che ricorrono in situazioni reali, che vanno dall'organizzazione del trasportoa problemi di allocazione ot tima di risorse. Questi problemi fo rmano la classe NP esono caratterizzati dall'ammettere particolari sequenze binarie chiamate certificati poli-nomiali: chi ha la soluzione per un'istanza di un problema in NP, può convincerci di

ciò fornendo un'opportuno certificato che ci permette di verificare, in tempo polinomia-le, l'esistenza di una qualche soluzione. No ti amo che chi non ha tale soluzione, puòcomunque procedere per tentativi in tempo esponenziale, provando a generare (più omeno esplicitamente) tutti i certificati possibili.

Come mostrato nella Figura 1.2, la classe NP include (non sappiamo se in sensostretto o meno) la classe P in quanto, per ogni problema che ammette un algoritmopolinomiale, possiamo usare tale algoritmo per produrre una soluzione e, quindi, uncertificato polinomiale.

Page 31: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 31/373

 

Il problema del Sudoku in realtà appartiene alla classe dei problemi NP-completi( N P C ) , che sono stati introdotti indipendentemente all'inizio degli anni '70 da due in-formatici, lo statunitense/canadese Stephen Cook e il russo Leonid Levin. Tali problemisono i più difficili da risolvere algoritmicamente all'interno della classe NP, nel senso chese scopriamo un algoritmo polinomiale per un qualsiasi problema NP-completo, alloratutti i problemi in NP sono risolubili in tempo polinomiale (ovvero la classe NP coincidecon la classe P). Se invece dimostriamo che uno dei problemi NP-completi è intrattabi-le (e quindi che la classe NP è diversa dalla classe P), allora risultano intrattabili tutti iproblemi in NPC.

I problemi studiati in questo libro si collocano principalmente nella classe NP, dicui forniremo una trattazione rigorosa nell'ultimo capitolo. Per il momento anticipiamoche, in effetti, il concetto di NP-completezza fa riferimento ai soli problemi decisionali(ovvero, problemi per i quali la soluzione è binaria — sì o no): con un piccolo abuso di

terminologia, indicheremo nel seguito come NP-completi anche problemi che richiedo-no la ricerca di una soluzione non binaria e che sono computazionalmente equivalenti aproblemi decisionali NP-completi.

I problemi in NP (e quindi quelli NP-completi) influenzano la vita quotidiana più diquanto possa sembrare: come detto, se qualcuno mostrasse che i problemi NP-completiammettono algoritmi polinomiali, ovvero che P = NP, allora ci sarebbero conseguenzein molte applicazioni di uso comune. Per esempio, diventerebbe possibile indovinare intempo polinomiale una parola chiave di n simboli scelti in modo casuale, per cui diversimetodi di autenticazione degli utenti basati su parole d'ordine e di crittografia basata su

chiave pubblica non sarebbero più sicuri (come il protocollo secure sockets layer adoperatodalle banche e dal commercio elettronico per le connessioni sicure nel Web).

Non a caso, nel 2000 è stato messo in palio dal Clay Mathematics Institute un pre-mio milionario per chi riuscirà a dimostrare che l'uguaglianza P = NP sia vera o meno (lamaggioranza degli esperti congettura che sia P ^ NP per cui possiamo parlare di apparen-te intrattabilità): risolvendo uno dei due problemi aperti menzionati finora (Goldbach eNP) è quindi possibile diventare milionari.

1.4 Modello RAM e complessità computazionaleLa classificazione dei problemi discussa finora e rappresentata graficamente nella Figu-ra 1.2, fa riferimento al concetto intuitivo di passo elementare: concludiamo questocapitolo con una specifica più formale di tale concetto, attraverso una breve escursionenella struttura logica di un calcolatore.

L'idea di memorizzare sia i dati che i programmi come sequenze binarie nella memo-ria del calcolatore, è dovuta principalmente al grande e controverso scienziato ungherese

Page 32: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 32/373

 

John von Neumann 7 negli anni '50, il quale si ispirò alla macchina universale di Turing.I moderni calcolatori mantengono una struttura logica simile a quella introdotta da vonNeumann, di cui il modello RAM ( Random Access Machine o macchina ad accesso di-retto) rappresenta un'astrazione: tale modello consiste in un processore di calcolo a cuiviene associata una memoria di dimensione illimitata, in grado di contenere sia i datiche il programma da eseguire. Il processore dispone di un'unità centrale di elaborazionee di due registri, ovvero il contatore di programma che indica la prossima istruzione daeseguire e l'accumulatore che consente di eseguire le seguenti istruzioni elementari:8

• operazioni aritmetiche: somma, sottrazione, moltipl icazione, divisione;

• operazioni di con fronto: minore, maggiore, uguale e così via;

• operazioni logiche: and, or, no t e così via;

• operazioni di trasferimento : lettura e scrittura da accumulatore a memoria;

• operazioni di controllo: salti condizionati e non condizionati.

Allo scopo di analizzare le prestazioni delle strutture di dati e degli algoritmi pre-sentati nel libro, seguiamo la convenzione comunemente adottata di assegnare un costouniforme alle suddet te operazioni . In particolare, supponiamo che ciascuna di esse ri-chieda un tempo costante di esecuzione, indipendente dal numero dei dati memorizzatinel calcolatore. Il costo computazionale dell'esecuzione di un algoritmo, su una specificaistanza, è quindi espresso in termini di tempo, ovvero il numero di istruzioni elementarieseguite, e in termini di spazio, ovvero il massimo numero di celle di memoria utilizzatedurante l'esecuzione (oltre a quelle occupate dai dati in ingresso).

Per un dato problema, è noto che esistono infiniti algoritmi che lo risolvono, per cuiil progettista si pone la questione di selezionarne il migliore in termini di complessità intempo e/o di complessità in spazio. Entrambe le complessità sono espresse in notazioneasintotica in funzione della dimensione n dei dati in ingresso, ignorando così le costantimoltiplicative e gli ordini inferiori.9 Solitamente, si cerca prima di minimizzare la com-plessità asintotica in tempo e, a parità di costo temporale, la complessità in spazio: lamotivazione è che lo spazio può essere riusato mentre il tempo è irreversibile.10

Nella complessità al caso pessimo o peggiore consideriamo il costo massimo su tuttele possibili istanze di dimensione n, mentre nella complessità al caso medio consideriamoil costo mediato tra tali istanze. La maggior par te degli algoritmi presentati in questo

7I1 saggio L'apprendista stregone di Piergiorgio Odifreddi descrive la personalità di von Neumann.8Notiamo che le istruzioni di un linguaggio ad alto livello come C, C++ e JAVA, possono essere

facilmente tradotte in una serie di tali operazioni elementari.9Gad Landau usa la seguente metafora: un miliardario rimane tale sia che possegga un miliardo di

euro che ne possegga nove, o che possegga anche diversi milioni (le costanti moltiplicative negli ordini digrandezza e gli ordini inferiori scompaiono con la notazione asintotica O, CI e 0).

10 In alcune applicazioni, come vedremo, lo spazio è importante quanto il tempo, per cui cercheremo di

minimizzare entrambe le complessità con algoritmi più sofisticati.

Page 33: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 33/373

 

libro saranno analizzati facendo riferimento al caso pessimo, ma saranno mostrati anche

alcuni esempi di valutazione del costo al caso medio.

Diamo ora una piccola guida per valutare al caso pessimo la complessità in tempo

di alcuni dei costrutti di programmazione più frequentemente usati nel libro (come ogni

buona catalogazione, vi sono le dovute eccezioni che saranno illustrate di volta in volta).

• Le singole operazioni logico-aritmetiche e di assegnamento hanno un costo co-

stante.

• Nel costru tto condizionale

IF (guardia) { bloccol } ELSE { blocco2 }

uno solo tra i rami viene eseguito, in base al valore di g u a r d i a . Non pot endo

prevedere in generale tale valore e, quindi, quale dei due blocchi sarà eseguito, il

costo di tale costrutto è pari a

• Nel cost rutto iterativo

FOR (i= 0; i < m ; i = i + l ) { corpo }

sia ti il costo dell'esecuzione di corpo all'iterazione i del ciclo (come vedremo nellibro, non è detto che corpo debba avere sempre lo stesso costo a ogni iterazione).

Il costo risultante è dato da

• Nei cost rutt i iterativi

 WHILE (guardia) { corpo }

DO { corpo } WHILE (guardia);

sia m il numer o di volte in cui g u a r d i a è soddisfatta. Sia t( il costo della sua

valutazione all'iterazione i del ciclo, e tt il costo di corpo all'iterazione i. Poichéguardia viene valutata una volta in più rispetto a corpo, abbiamo il seguente

costo totale:

costo (g u ar d i a) + max{costo(bloccol) , costo(bl occo2)}

m— 1

i = 0

m m— 1

i=0 i=0

(notiamo che, di solito, la parte difficile rispetto alla valutazione del costo per il

ciclo FOR, è fornire una stima del valore di m).

Page 34: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 34/373

 

• Il costo della chiamata a funzione è dato da quello del corpo della funzione stessapiù quello dovuto al calcolo degli argomenti passati al momento dell'invocazio-ne (come vedremo, nel caso di funzioni ricorsive, la valutazione del costo saràeffettuata nel libro mediante la risoluzione delle relative equazioni di ricorrenza).

• Infine, il costo di un blocco di istruzioni e cost rutti visti sopra è pari alla sommadei costi delle singole istruzioni e dei costrutti, secondo quanto appena discusso.

Per concludere, osserviamo che la valutazione asintotica del costo di un algoritmoserve a identificare algoritmi chiaramente inefficienti senza il bisogno di implementarlie sperimentarli. Per gli algoritmi che risultano invece efficienti (da un pun to di vista dianalisi della loro complessità), occorre tener conto del particolare sistema che intendiamousare (piattaforma hardware e livelli di memoria, sistema operativo, linguaggio adotta-to, compilatore e così via). Questi aspetti sono volutamente ignorati nel modello RAMper permettere una prima fase di selezione ad alto livello degli algoritmi promettenti, che

però necessitano di un'ulteriore indagine sperimentale che dipende anche dall'applicazio-ne che intendiamo realizzare: come ogni modello, anche la RAM non riesce a catturare lemille sfaccettature della realtà.

RIEPILOGO

  In questo capitolo abbiamo definito i concetti di decidibilità e di trattabilità dei problemicomputazionali, fornendo una prima classificazione dei problemi stessi in base alla comples-sità dei relativi algoritmi di risoluzione. In particolare, abbiamo visto come la trattabilitàvenga fatta coincidere con l'esistenza di algoritmi risolutivi con complessità polinomiale, for-

nendo una prima definizione dei problemi risolvibili efficientemente, e quindi delle classiNP f NPC. Abbiamo infine introdotto un modello di calcolo che utilizzeremo nel seguito

  per la loro valutazione.

ESERCIZI

1. Dimostrate che non esiste un programma TerminaZero, il quale, preso un pro-gramma A, restituisce (in tempo finito) un valore di verità per indicare che Atermina o meno quando viene eseguito con input 0.

2. Per ogni i ^ 1, l'i-esimo numero di Fibonacci F(i) è definito nel modo seguente:

i l se i = 1,2l l J ~ \ F(i — 1) + F(i — 2) altrimenti

Sulla base di tale definizione, scrivete una funzione ricorsiva Fi b o n a c c i che cal-coli il valore F(i). Valutate il numero di passi che tale funzione esegue al variare del

Page 35: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 35/373

 

numero i e, usando il fatto che F(i) = + J ^ — 1 dove (f) = è il rap-

  porto aureo, dimostrate che tale numero cresce esponenzialmente in i. Descrivete

poi un algoritmo iterativo polinomiale in i.

3. Progettate un algoritmo ricorsivo per generare tutti i sottoinsiemi di taglia nottenibili da un insieme di m elementi, in cui il numero di chiamate ricorsiveeffettuate è proporzionale al numero di sottoinsiemi generati.

4. Descrivete un algoritmo di backtrack per la risoluzione del problema delle n regineche può essere descritto nel modo seguente: n regine devono essere poste su unascacchiera di dimensione n x n in modo tale che nessuna regina possa mangiarneun'altra (ricordiamo che una regina può mangiare un'altra regina se si trova sullastessa riga, sulla stessa colonna o sulla stessa diagonale).

5. In alcuni casi, per l'analisi di algoritmi operanti su numeri, è necessario fare ameno dell'ipotesi che il costo di un'operazione sia costante: in particolare ciòrisulta necessario per rendere il costo di esecuzione di un'operazione aritmeticadipendente dal valore dei relativi argomenti . Una tipica ipotesi, in tal caso, èquella di considerare il costo di un'addizione nj + ri2 tra due interi proporzionalealla lunghezza della codifica del più grande tra i due, e quindi a log max{n 1,1x2}.Valutate, sotto tale ipotesi, il conseguente costo di una moltiplicazione nj x n2effettuata mediante il normale procedimento imparato alla scuola elementare.

Page 36: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 36/373

 

Capitolo 2

Sequenze: array

S O M M A R I O

 Il modo più semplice per aggregare dei dati elementari consiste nel disporli uno di seguito al-l'altro a formare una sequenza lineare, identificando ciascun dato con la posizione occupata.

  In questo capitolo studieremo tale disposizione descrivendo due diversi modi di realizzarla,l'accesso diretto e l'accesso sequenziale, che riflettono l'allocazione della sequenza nella me-moria del calcolatore. Successivamente, analizzeremo le sequenze lineari ad  accesso diretto(dette anche array), mostrando diverse loro applicazioni e operazioni fornite. Tra l'altro,studieremo il problema della ricerca e dell'ordinamento. Inoltre, mostreremo come risolverericorsivamente i problemi utilizzando il paradigma del divide et impera e la tecnica del-la programmazione dinamica, introducendo l'analisi degli algoritmi ricorsivi mediante leequazioni di ricorrenza.

DIFFICOLTÀ2 CFU

2.1 Sequenze lineari

Una sequenza lineare è un insieme finito di elementi disposti consecutivamente in cuiognuno ha associato un indice di posizione in modo univoco. Seguendo la convenzionedi enumerare gli elementi a partire da 0, indichiamo una sequenza lineare di n elementicon la notazione Qo, ai,..., a

n_i, dove la posizione j contiene il (j + l)-esimo elemento

rappresentato da a¡ (per 0 ^ j ^ n — 1).

Nel disporre gli elementi in una sequenza, viene ritenuto importante il loro or-dine relativo: quindi, la sequenza ottenuta invertendo l'ordine di due qualunque ele-menti è generalmente diversa da quella originale. Per esempio, consideriamo la parolaa l g o r i t m o , vista come sequenza di n = 9 caratteri ao, a j , . . . , as = a, 1, g, o, r , i ,

Page 37: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 37/373

 

transazione

deposita 1000deposita 1000preleva 1500

preleva 500

saldo0

10002000

50 0

0

transazione

deposita 1000deposita 1000preleva 500

preleva 1500

saldo0

1000

20001500

0

transazione

deposita 1000preleva 1500deposita 1000

preleva 500

Figura 2.1 Sequenze di transazioni bancarie.

t , m, o. Se invertiamo gli elementi ao e ai , ott eniamo la parola l a g o r i t m o che nelcorrente dizionario italiano non ha alcun significato. Se a partire da quest'ultima parolainvertiamo gli elementi aj e 03, otteniamo la parola logaritmo, che indica una notafunzione matematica.

Un altro esempio significativo sono le sequenze di transazioni bancarie. Supponia-mo che queste siano solo di due tipi, ovvero prelievi e depositi, e che non sia possibileprelevare una somma maggiore del saldo. A partire da un saldo nullo, consideriamo laseguente sequenza di transazioni (colonna a sinistra nella Figura 2.1): deposita 1000,deposita 1000, preleva 1500, preleva 500. Se invertiamo le ult ime due transazioni, lanuova sequenza non genera errori in quanto il saldo è sempre maggiore oppure ugualealla quantità che viene prelevata (colonna centrale nella Figura 2.1). Al contrario, se in-vertiamo la seconda e la terza transazione, otteniamo una sequenza che genera un errorein quanto cerca di prelevare 1500, quando il saldo è pari a 1000 (colonna a destra nellaFigura 2.1).

2.1.1 Modalità di accesso

L'operazione più elementare su una sequenza lineare consiste certamente nell'accesso aisuoi singoli elementi, specificandone l'indice di posizione. Per esempio, nella sequenzaao, a i , . . . , ag = a, 1, g, o, r, i , t , m, o, tale operazione restituisce 07 = m nel mo mentoin cui viene richiesto l'elemento in posizione 7.

L'accesso agli elementi di una sequenza lineare a viene generalmente eseguito in

due modalità . In quella ad accesso diretto, dato un indice 1, accediamo direttamenteall'elemento ai della sequenza senza doverla attraversare. In altre parole, l'accesso diret-to ha un costo computazionale uni forme, indipendente dall'indice di posizione i. Nelseguito chiameremo array le sequenze lineari ad accesso diretto e, coerentemente conla sintassi dei più diffusi linguaggi di programmazione, indicheremo con a[i] il valoredell'(i + 1)-esimo elemento di un array a.

L'altra modalità consiste nel raggiungere l'elemento desiderato attraversando la se-quenza a partire da un suo estremo, solitamente il primo elemento. Tale modalità, detta

Page 38: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 38/373

 

Figura 2. 2 Allocazione della memoria nel caso di un array.

ad accesso sequenziale, ha un costo O(i) proporzionale alla posizione i dell'elementoa cui si desidera accedere: d'ora in poi chiameremo liste le sequenze lineari ad accessosequenziale. Notiamo però che, una volta raggiunto l'elemento ai, il costo di accesso ad

Oi +i è 0(1). Generalizzando, il costo è 0(k) per accedere ad at+k partendo da ai.I due modi di realizzare l'accesso agli elementi di una sequenza lineare non devono

assolutamente essere considerati equivalenti, vista la differenza di costo computazionale.Entrambe le modalità presentano prò e contro per cui non è possibile dire in generale cheuna sia preferibile all'altra: tale scelta dipende dall'applicazione che vogliamo realizzare odal problema che dobbiamo risolvere.

2.1.2 Allocazione della memoria

La descrizione degli algoritmi che fanno uso di array e di liste dovrebbe prescindere dallaspecifica allocazione dei dati nella memoria fisica del calcolatore. Tuttavia, una brevedigressione su questo argomento permette di comprendere meglio la differenza di costoquando accediamo a un elemento di un array rispetto a un elemento di una lista. Inquesto paragrafo supponiamo che una parola di memoria e un indirizzo di memoriasiano composti da 4 byte ciascuno.

Gli array e le liste corrispondono a due modi diversi di allocare la memoria di uncalcolatore. Nel caso degli array, le locazioni di memoria associate a elementi consecutivisono contigue. Il nome dell'array corrisponde a un indirizzo che specifica dove si trovala locazione di memoria contenente l'inizio dell'array (tale inizio viene identificato conil primo elemento dell'array a[0]). Per accedere all'elemento a[i] è dunque sufficientesommare a tale indirizzo i volte il numero di byte necessari a memorizzare un singoloelemento. Per esempio, consideriamo l'array a mostrato nella Figura 2.2, il quale con-tiene 5 elementi di 4 byte ciascuno. Se x è l'indirizzo contenuto nella variabile a, ilprimo elemento dell'array si trova nella locazione di memoria con indirizzo x, il secondoelemento si trova nella locazione con indirizzo x + 4, il terzo elemento si trova nella lo-cazione con indirizzo x + 8, e così via. In altre parole, conoscendo il valore x, l'indirizzo

Page 39: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 39/373

 

a 0a 0n

a 3 44

» di

* £12

Figura 2.3 Allocazione di memoria nel caso di una lista.

della locazione di memoria contenente a[i] può essere calcolato in tempo costante, con laformula x+ i x 4. Ciò giustifica l'affermazione fatta in precedenza che, nel caso di accessodiretto, il costo dell'operazione di accesso è 0(1), in quanto è indipendente dall'indice

di posizione dell'elemento desiderato.

Differentemente dagli array, gli elementi delle liste sono allocati in locazioni di me-moria non necessariamente contigue. Quest'allocazione deriva dal fatto che la memoriaper le liste viene gestita dinamicamente durante la computazione, quando i vari elementisono inseriti e cancellati (in un modo che non è possibile prevedere prima della com-putazione stessa). Per questo motivo, ogni elemento deve memorizzare, oltre al propriovalore, anche l'indirizzo dell'elemento successivo. Il nome della lista corrisponde a un in-dirizzo che specifica dove si trova la locazione di memoria contenente il primo elementodella lista. Per accedere ad Q è dunque necessario partire dal primo elemento e scandireuno dopo l'altro tutti quelli chc precedono ai nella lista. Per esempio, consideriamo lalista a mostrata nella Figura 2.3: in questo caso, a contiene 4 elementi di 4 byte ciascunoe, per ciascun elemento, l'indirizzo di 4 byte necessario a individuare l'elemento succes-sivo. Per accedere al terzo elemento, ovvero ad Q2, è necessario partire dall' inizio dellalista, ovvero da a, per accedere ad ciò, P°i a d Qi e quindi ad aj- In altre parole, l'accessoa un elemento di una lista richiede la scansione di tutti gli elementi che lo precedono e,per questo motivo, ha un costo proporzionale all'indice della sua posizione: in generale,se partiamo da Qì, l'accesso ad ai+k richiede 0( k) passi. D'a ltra parte, le liste ben si

Page 40: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 40/373

 

Verif icaRaddoppio( ) : {pre: a. è un array di lunghezza d con n elementi)IF (N == d) {

b = NuovoArrayC 2 x d );

FOR  (i = 0; i < n; i = i+1)

b[i] = a[i] ;

a = b;>

VerificaDimezzamento ( ) : {pre: a. è un array di lunghezza d con n elementi)IF ((d > 1) tk (n == d/4)) {

b = NuovoArrayC d/2 );

FOR  (i = 0; i < n; i = i+1)

b[i] = a[i] ;

a = b;

Codice 2.1 Operazioni di ridimensionamento di un array dinamico.

prestano a implementare sequenze dinamiche, a differenza degli array per i quali, come

vedremo nel prossimo paragrafo, è necessario adottare particolari accorgimenti.

2.1.3 Array di dimensione variabile

Volendo utilizzare un array per realizzare una sequenza lineare dinamica, è necessarioapportare diverse modi fiche che consentano di effettuare il suo ridime nsionamento: di-versi linguaggi moderni, come C + + , C# e JAVA forniscono array la cui dimensione puòvariare dinamicamen te con il tempo. Prenderemo in considerazione l'inserimento e lacancellazione in fondo a un array a di n elementi per illustrarne la gestione del ridimen-sionamento. Allocare un nuovo array (più grande o più piccolo) per copiarvi gli elemen-ti di a a ogni variazione della sua dimensione può richiedere O(n) tempo per ciascu-na operazione, risultando particolarmente oneroso in termini computazionali, sebbenesia ottimale in termini di memoria allocata. Con qualche piccolo accorgimento, tutta-

via, possiamo fare meglio pagando tempo O(n) cumulativamente per ciascun gruppo diO(n) operazioni consecutive, ovvero un costo distribuito di 0(1) per operazione.

Sia d il numero di elementi dell'array a correntemen te allocati in memoria e n ^ dil numero di elementi effettivamente con tenu ti nella sequenza memorizzata in a. Og niqualvolta un'operazione di inserimento viene eseguita, se vi è spazio sufficiente (n + 1 ^d), aument iam o n di un 'unit à. Alt rimenti , se n = d, allochiamo un array b di taglia2d, raddoppiamo d, copiamo gli n elementi di a in b e poniamo a = b. Analogamente,ogni qualvolta un'operazione di cancellazione viene eseguita, diminuiamo n di un'unità.

Page 41: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 41/373

 

Quando n = d/4, dimezziamo l'array a: allochiamo un array b di taglia d/2, dimezzia-mo d, e copiamo gli n elementi di a in b (ponendo a = b). Queste operazioni di verificaed eventuale ridimensionamento dell'array sono mostrate nel Codice 2.1.

Contrariamente a prima, osserviamo che non è più possibile causare un ridimensio-namento di a (raddoppio o dimezzamento) al costo di O(n) tempo per ciascuna opera-zione. Dopo un raddoppio, ci sono n = d + 1 elementi nel nuovo array di 2d elementi.Occorrono almeno n— 1 richieste di inserimento per un nuovo raddoppio e almeno n/2richieste di cancellazione per un dimezzamento. In modo simile, dopo un dimezzamen-to, ci sono n = d/4 elementi nel nuovo array di d/2 elementi, per cui occorrono almenon + 1 richieste di inserimento per un raddoppio e almeno n/2 richieste di cancellazioneper un nuovo dimezzamento. In tutt i i casi, il costo di O( n) tempo richiesto dal ridi-mensionamento può essere virtualmente distribuito tra le O(n) operazioni che lo hannocausato (a partire dal precedente ridimensionamento). Tale costo può essere concettual-mente ripartito tra le operazioni, incrementando la loro complessità di un costo costantecosì distribuito per ciascuna operazione.

ALVI E: array di dimensione variabile

Osserva, sperimenta e verifica

DynamieAr ray

2.2 Opus libri: scheduling della CPU

I moderni sistemi operativi sono ambienti di multi-programmazione, ovvero consentonoche più programmi possano essere in esecuzione simultaneamente. Ques to non vuoldire che una singola unità centrale di elaborazione (CPU, da Central Processing Unit)esegua contemporaneamente più programmi, ma semplicemente che nei periodi in cuiun programma non ne fa uso, la CPU può dedicarsi ad altri programmi. A tal fine,

la CPU ha a disposizione una sequenza di porzioni di programma da dover eseguire,ciascuna delle quali è caratterizzata da un tempo di utilizzo della CPU in millisecondi(ms): per semplicità identifichiamo nel seguito le porzioni con i loro programmi.

Supponiamo che vi siano quattro programmi o task  PQ, P|, P2 e P3 in esecuzionesulla stessa CPU e che in un certo istante di tempo la sequenza dei tempi previsti diutilizzo della CPU da parte loro sia la seguente: 21ms per Po, 3ms per Pi, lm s per P2 e2ms per P3 (ipotizziamo che ciascun task vada completato prima di passare a elaborarneun altro come accade, per esempio, nella coda di stampa). La decisione di eseguire i

Page 42: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 42/373

 

Po Pi P2 P3

0 21 2425 27

P. P3 Po P2

0 3 5 2627

p2 P3 Pi Po0 1 3 6 27

Figura 2.4 Scheduling della CPU e tempi di attesa.

programmi in una determinata sequenza si chiama scheduling. La CPU può decideredi eseguire i programmi nell'ordine in cui essi appaiono nella sequenza, che di solitocoincide con l'ordine di arrivo. Per questo motivo, tale politica viene chiamata First ComeFirst Served  (FCFS). L'occupazione della CPU da parte dei programmi è quella mostratanella parte superiore della Figura 2.4. I loro tempi di attesa sono pari a 0, 21, 24 e 25,rispettivamente, ottenendo un tempo medio di attesa uguale a 0 + 2 1 = 17,5ms.

Se la sequenza dei programmi da eseguire fosse giunta in un ordine diverso, appli-cando la strategia FCFS il tempo medio di attesa risulterebbe diverso. Ad esempio, se la

sequenza fosse Pi, P3, Po e P2, l'occupazione della CPU sarebbe quella mostrata nella par-te centrale della Figura 2.4. I tempi di attesa sarebbero pari a 0, 3, 5 e 26, rispettivamente,ottenendo un tempo medio di attesa di °+3+5+26 = 8,5ms, che è significativamente piùbasso.

Dall'esempio risulta che il tempo di attesa medio diminuisce se i programmi contempi di utilizzo minore vengono eseguiti per primi . Per questo motivo, quando vienesempre eseguita per prima la porzione di programma con tempo di esecuzione più breve,la politica viene chiamata Shortest Job First  (SJF).

Per esempio, facendo riferimento alla sequenza precedente, la CPU dispone i quat-

tro programmi nell'ordine P2, P3, Pi e Po- Come mostrato nella parte inferiore dellaFigura 2.4, il tempo medio di attesa si riduce in tal caso a 0 + 1 + 3 + 6 = 2,5ms (che tral'altro è il minimo tempo di attesa medio possibile). La realizzazione della strategia SJFrichiede di poter eseguire l'ordinamento dei tempi previsti di utilizzo della CPU da partedei diversi programmi.

L'operazione di ordinamento di una sequenza lineare di elementi è una delle ope-razioni più frequenti in diverse applicazioni informatiche, e nel seguito ne discuteremoulteriormente. In questo paragrafo, mostriamo come ottenere un ordinamento mediante

Page 43: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 43/373

 

SelectionSort( a ) : (pre: la lunghezza di a è n)FOR  (i = 0; i < n; i = i+1) {

minimo = a[i];

indiceMinimo = i;

FOR  (j = i+1; j < n; j = j+1) {

IF (a[j] < minimo) {minimo = a[j];

indiceMinimo = j;

}>a[indiceMinimo] = a[i];

a[i] = minimo;

Codice 2.2 Ordinamento per selezione di un array a.

due semplici algoritmi, di cui valuteremo la complessità rispetto al numero n di elemen-ti della sequenza. Entrambi gli algoritmi richiedono un tempo proporzionale a 0(n 2 ) erisultano utili per la loro semplicità quando il valore di n è piccolo. Vedremo più avanticome sia possibile progettare algoritmi di ordinamento più efficienti, la cui complessitàtemporale è O(nlogn).

2.2.1 Ordinamento per selezioneL'algoritmo di ordinamento per selezione, detto selection sort , consiste nell'eseguire npassi: al generico passo i = 0,1,... ,n — 1, viene selezionato l'elemento che occuperàla posizione i della sequenza ordinata . In altre parole, al termine del passo i, gli i + 1elementi selezionati fino a quel momento coincidono con i primi i + 1 elementi dellasequenza ordinata. Per realizzare il passo i, l'a lgoritmo deve selezionare il mini mo tragli elementi che si trovano dalla posizione i in avanti, per poi sistemarlo nella posizionecorretta i. L'algoritmo di ordinamento per selezione è mostrato nel Codice 2.2.

Come possiamo vedere, l'algoritmo esegue due cicli annidati: il ciclo esterno (che vadalla riga 2 alla 13) corrisponde agli n passi dell'algoritmo, mentre il ciclo interno (cheva dalla riga 5 alla 10) corrisponde alla ricerca del minimo. Il posizionamento viene poieffettuato dalle righe 11 e 12, in cui il minimo da spostare viene scambiato con l'attualeelemento nella posizione i.

Analizziamo la complessità dell'algoritmo di ordinamento per selezione utilizzandolo schema di analisi illustrato nel Paragrafo 1.4. Abbiamo m = n iterazioni nel cicloesterno del Codice 2.2 (righe 2-13) e il costo ti dell'iterazione i è dato da un numerocostante di operazioni di costo 0( 1 ) p iù il costo del ciclo inte rno (righe 5-1 0) . Qui ndi,

Page 44: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 44/373

 

al passo i, l'algoritmo esegue un numero di operazioni proporzionale a ti = n — i: il

numero totale di operazioni al caso pessimo è pertanto proporzionale a

TI— 1 n Í  . -a \. \  V - . n n + 1 2

2 _ (n. - 1 ) = 2 _

1

= 2

=

i=0 i=l

In altre parole, il selection sort è un algoritmo di ordinamento con complessità quadra-tica rispetto al numero di elementi da ordinare. Tale compless ità in tempo è sempreraggiunta, per qualunque sequenza iniziale di n elementi: possiamo dunque dire che ilcosto computazionale dell'algoritmo è 0(n 2 ) . Per questo motivo, nel prossimo para-grafo descriveremo un algoritmo di ordinamento altrettanto semplice, le cui prestazionipossono essere in alcuni casi significativamente migliori.

ALVIE: ordinamento per selezione

Osserva, sperimenta e verifica

SelectionSort

a t e : :•

2.2.2 Ordinamento per inserimentoL'algoritmo di ordinamento per inserimento, detto insertion sort, consiste anch'esso nel-l'eseguire n passi: al passo i = 0,1,... ,n — 1, l'elemento in posizione i viene inseritoal posto giusto tra i primi i elementi. In altre parole, al termine del passo i, gli i + 1elementi sistemati fino a quel momento sono tra di loro ordinati ma non coincidononecessariamente con i primi i + 1 elementi della sequenza ordinata. Sia p r o s s i m o l'e-lemento in posizione i: per realizzare il passo i, l'algoritmo confronta prossimo con iprimi v elementi fino a trovare la posizione corretta in cui inserirlo: procedendo dalla po-sizione i— 1 verso l'inizio della sequenza, sposta ciascuno degli elementi di una posizionein avanti per far posto a quello da inserire.

L'algoritmo di ordinamento per inserimento è mostrato nel Codice 2.3, il qualeesegue un doppio ciclo di cui quello più esterno (dalla riga 2 alla 10) corrisponde aglin passi dell'a lgori tmo. Il ciclo w h i l e interno (dalla riga 5 alla 8), esamina le posizionii — 1, i — 2,..., fino a trovare il punto in cui prossimo deve essere inserito. Man manoche esamina gli elementi in tali posizioni, questi vengono spostati di una posizione inavanti. Al termine del ciclo interno (riga 9), avviene l'effettiva operazione di inserimentodi prossimo nella posizione corretta.

Page 45: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 45/373

 

InsertionSort ( a ): (pre: la lunghezza di a è n)

FOR  (i = 0; i < n; i = i+1) {

prossimo = a[i];

  j = i ;

 WHILE ((j > 0 ) tt (a[j-l] > prossimo)) {

a[j] = a[j-l] ;  j = j - i ;

>a[j] = prossimo;

Codice 2.3 Ordinamento per inserimento di un array a.

Al passo i-esimo l'algoritmo di ordinamento per inserimento esegue un numero dioperazioni proporzionale a i al caso pessimo, e il numero totale di operazioni eseguite è

proporzionale a

n ¿ i = ^ i l = 0(n2)

i=0

In altre parole, anche l'insertion sort è un algoritmo di ordinamento con complessitàquadratica rispetto al numero di elementi da ordinare. Tuttavia, a differenza del selectionsort, può richiedere un tempo significativamente inferiore per certe sequenze di elementi:

se la sequenza iniziale è già in ordine o soltanto un numero trascurabile di elementi èfuori ordine, l'insertion sort richiede O(n) tempo mentre la complessità del selectionsort rimane comunque 0(n 2 ) .

ALVIE: ordinamento per inserimento

K • » >• • • •

J h ^ f c ì Osserva , sper imenta e ver i f ica

^LBp^ InsertionSort

2.3 Complessità di problemi computazionali

Per illustrare i principi di base che guidano la metodologia di progettazione degli algo-

ritmi e la loro analisi di complessità in termini di tempo e spazio, consideriamo un pro-

blema "giocattolo", ovvero la ricerca del segmento di somma massima. Data una sequenza

Page 46: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 46/373

 

SommaMassimal( a ): {pre: a contiene n elementi di cui almeno uno positivo)max = 0;

FOR  (i = 0; i < n; i = i+1) {

FOR  (j = i; j < n; j = j+1) {

somma = 0;

FOR  (k = i; k <= j; k = k+1)somma = somma + a[k];

IF (somma > max) max = somma;

>>RETURN max;

Codice 2.4 Prima soluzione per il segmento di somma massima.

di n interi memorizzata in un array a, un segmento è una qualunque sotto-sequenza di

elementi consecutivi, di, Oi+i,..., aj, dalla posizione i fino alla j. Tale segmento viene

indicato con la notaz ione a[i, j], dove 0 ^ i ^ j ^ n — 1; in tal modo, l'intero array

corrisponde ad a[0, n — 1]. La somma di un segmento a[i, j] è data dalla somma dei suoi

componenti, somma[a[i, j]) = a[k]. Il problema consiste nell 'individuare in a un

segmento di somma massima, dove a parità di somma viene scelto il segmento più corto.

Notiamo che se a contiene solo elementi positivi, allora il segmento di somma mas-

sima coincide necessariamente con l'intero array: il problema diventa interessante se

l'array include almeno un elemento negativo. D'altra parte, se a contiene solo elementinegativi, allora il problema si riduce a trovare l'elemento dell'array il cui valore assoluto

è minimo: per questo motivo, possiamo supporre che a contenga almeno un elemento

positivo (è chiaro che, in questo caso, un segmento di somma massima deve avere gli

estremi positivi, in quanto altrimenti potremmo ottenere un segmento avente somma

maggiore escludendo un estremo negativo).

Nella prima soluzione proposta, generiamo direttamente tutti i segmenti calcolando

la somma massima. Un segmento a[i, )] è univocamente identificato dalla coppia di

posizioni, i e }, dei suoi estremi a[i] e a[j]. Generiamo quindi tutte le coppie i e j in cui

O ^ i ^ j ^ n — 1 e calcoliamo le somme dei relativi segmenti, ottenendo l'algoritmomostrato nel Codice 2.4. Il costo di tale algoritmo è 0(n 3 ) tempo: infatti, il corpo dei

primi due cicli f o r (dalla riga 5 alla 8) viene eseguito m eno di n 2 volte e, al suo interno,

il terzo ciclo f or calcola somma{a[i, j]) eseguendo j — i + 1 iterazioni, ciascuna in tempo

0( 1) . D'altra parte l'algoritmo richiede H( n 3 ) tempo, in quanto la riga 7 è eseguita

Li=o L U () " i •+ 1 ) volte, ovvero Z i = o L U ì > L i = o (n ~ > L ^ o =Q(n 3 ) volte. La complessità in spazio è 0 (1 ) in quant o usiamo soltanto un numerocostante di variabili di appoggio oltre ai dati in ingresso.

Page 47: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 47/373

 

SommaMassima2( a ): {pre: a contiene n elementi di cui almeno uno positivo)max = 0;

FOR  (i = 0; i < N; i = i+1) {

somma = 0;

FOR  (j = i; j < n; j = j+1) {

somma = somma + a[j] ;IF (somma > max) max = somma;

>>RETURN max;

Codice 2.5 Seconda soluzione per il segmento di somma massima.

Nella seconda soluzione proposta, una volta calcolata somma{a[i, j — 1]), evitiamo diripartire da capo per il calcolo di somma{ a[i, j]). Utilizziamo il fatto che somma{ a[i, j]) =somma[a[i, j — 1]) + a[j], ottenendo il Codice 2.5. Mantenendo l'invariante che, all'iniziodi ogni iterazione del ciclo f o r più interno (dalla riga 5 alla 8), la variabile sommacorrisponde asomma{a[\,) — 1]), è sufficiente aggiungere a[j] per ottenere somma{a[\,)]).

Il costo in tempo è ora 0(n 2 ) in quanto dettato dai due cicli for, mentre la complessitàin spazio rimane 0(1).

Nella terza e ultima soluzione proposta, sfruttiamo meglio la struttura combinatoriadel problema in quanto, a fronte di 0(n 2 ) possibili segmenti, ne possono esistere soltan-

to O(n) di somma massima, e questi sono disgiunti a seguito della seguente proprietàinvariante. Esaminando un segmento di somma massima, a[i, j], notiamo che deve ave-re lunghezza minima tra i segmenti di pari somma, e deve soddisfare le seguenti duecondizioni.

(a) Ogni prefisso di a[i, }] ha somma positiva: somma{a[\, k]) > 0 per ogni i ^ k < j.

Se così non fosse, esisterebbe un valore di k tale che

somma{a[\, j]) = somma{a[i, k]) + somma{a[k + 1, j]) ^ somma(a[k + 1, j])

ott enendo una contraddizione in quan to il segmento a [k + 1, j] avrebbe sommamaggiore o, a parità di somma, sarebbe più corto.

(b) Il segmento a[i , j] no n può essere esteso a sinistra: sommaia[k, i— 1]) < 0 per ogni

0 ^ k ^ i — 1. Se così no n fosse, esisterebbe una posizione k ^ i — 1 per cui

sommai a[k,j]) = sommaia[ k,i— 1]) + sommaia[x, j]) > sommaia[ i, j])

ottenendo una contraddizione in quanto il segmento a[k, j] avrebbe somma mag-

giore di quello con somma massima.

Page 48: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 48/373

 

SommaMassima3( a ): (pre: a contiene n elementi di cui almeno uno positivo)max = 0;

somma = max;

FOR  (j = 0; j < n; j = j+1) {

IF (somma > 0) {

somma = somma + a[j];> ELSE {

somma = a[j] ;

>IF (somma > max) max = somma;

>RETURN max;

Codice 2. 6 Terza soluzione per il segmento di somma massima.

Sfruttiamo le proprietà (a) e (b) durante la scansione dell'array a, come mostratonel Codice 2.6. In particolare, la riga 6 corrisponde all'applicazione della proprietà (a),che ci assicura che possiamo estendere a destra il segmento corrente. La riga 8, invece,corrisponde all'applicazione della proprietà (b), in base alla quale possiamo scartare il seg-mento corrente e iniziare a considerare un nuovo segmento disgiunto da quelli esaminatifino a quel punto.

Il costo di quest 'ultima soluzione è O(n) in quanto c'è un solo ciclo f o r , il cui

corpo richiede 0(1) passi a ogni iterazione. Lo spazio aggiuntivo richiesto è rimasto di0(1 ) locazioni di memor ia. In conclusione, pa rtendo dalla prima soluzione, abbiamoridotto la complessità in tempo da 0(n 3 ) a 0(n 2 ) con la seconda soluzione, per poipassare a O(n) con la terza. Invitiamo il lettore a eseguire sul calcolatore le tre soluzioniproposte per rendersi conto che la differenza di complessità non è confinata a uno studiopuramente teorico ma molto spesso incide sulle prestazioni reali.

Notiamo che l'algoritmo per la terza soluzione ha una complessità asintotica ottima

sia in termini di spazio che di tempo. Nel caso dello spazio, ogni algori tmo deve usarealmeno un numero costante di locazioni per le variabili di appoggio. Per il tempo, ogni •algoritmo per il problema deve leggere tutti gli n elementi dell'array a perché, se così nonfosse, avremmo almeno un elemento, a[r], non letto: in tal caso, potremmo invalidarela soluzione trovata assegnando un valore oppo rtun o ad a[r]. Qu indi ogni algoritmorisolutore per il problema del segmento di somma massima deve leggere tutti gli elementie quindi richiede un tempo pari a n ( n ) . Ne consegue che la terza soluzione è ott imaasintoticamente. In generale, pur essendoci un numero in finito di algoritmi risolutoriper un dato problema, possiamo derivare degli argomenti formali per dimostrare chel'algoritmo proposto è tra i migliori dal punto di vista della complessità computazionale.

Page 49: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 49/373

 

ALVIE: segmento dì somma massima

Osserva, sperimenta e verifica

SegmentSum

2.3.1 Limiti superiori e inferiori

Il problema del segmento di somma massima esemplifica l'approccio concettuale adotta-to nello studio degli algoritmi. Per un dato problema computazionale FI, consideriamo

un qualunque algoritmo A di risoluzione. Se A richiede t(n) tempo per risolvere unagenerica istanza di IT di dimensione n, diremo che 0(t(n)) è un limite superiore allacomplessità in tempo del problema Fi. Lo scopo del progettista è quello di riuscire atrovare l'algoritmo A con il migliore tempo t(n) possibile.

A tal fine, quando riusciamo a dimostrare con argomentazioni combinatorie chequalunque algoritmo A' richiede almeno tempo f (n) per risolvere FI su un'istanza gene-rica di dimensione n, asintoticamente per infiniti valori di n, diremo che 0(f(n)) è unlimite inferiore alla complessità in tempo del problema Fi. In tal caso, nessun algoritmopuò richiedere asintoticamente meno di 0(f(n)) tempo per risolvere FI.

Ne deriva che l'algoritmo A è ottimo se t(n) = 0(f(n)), ovvero se la complessitàin tempo di A corrisponde dal punto di vista asintotico al limite inferiore di Fi. Ciò cipermette di stabilire che la complessità computazionale del problema è 0( f( n) ). No-tiamo che spesso la complessità computazionale di un problema combinatorio FI vieneconfusa con quella di un suo algoritmo risolutore A: in realtà ciò è corretto se e solo seA è ottimo.

Nel problema del segmento di somma massima, abbiamo mostrato che la terza so-luzione richiede tempo O( n) . Quindi il limite superiore del problema è O(n) . Inoltre,abbiamo mostrato che ogni algoritmo di risoluzione richiede O(n) tempo, fornendo un

limite inferiore. Ne deriva che la complessità del problema è 0(n) e che la terza soluzioneè un algoritmo ottimo.

Per quanto riguarda la complessità in spazio, s(n), possiamo procedere analogamenteal tempo nella definizione di limite superiore e inferiore, nonché di ottimalità. Da ricor-dare che lo spazio s(u) misura il numero di locazioni di memoria necessarie a risolvere ilproblema FI, oltre a quelle richieste dai dati in ingresso. Per esempio, gli algoritmi in locosono caratterizzati dall'usare soltanto spazio s(n) = 0(1), come accade per il problemadel segmento di somma massima.

Page 50: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 50/373

 

RicercaSequenziale ( a, k ): (pre: la lunghezza di a è n)

trovato = FALSE;

indice = -1;

FOR (i = 0; (i<n) && (¡trovato); i = i+1) {IF (a[i] == k) {

trovato = TRUE;indice = i;

>>

RETURN i n d i c e ;

Codice 2.7 Ricerca sequenziale di una chiave k in un array a.

2.4 Ricerca di una chiaveUno degli usi più frequenti del calcolatore è quello di cercare una chiave, ovvero unvalore specificato, tra la mole di dati disponibili in forma elettronica. Data una sequenzalineare di n elementi, la ricerca di una chiave k consiste nel verificare se la sequenzacontiene un elemento il cui valore è uguale a k (nel seguito identificheremo gli elementicon il loro valore). Se la sequenza non rispetta alcun ordine, occorre esaminare tutti glielementi con il metodo della ricerca sequenziale, descritto nel Codice 2.7.

L'algoritmo non fa altro che scandire, uno dopo l'altro, i valori degli elementi con-

tenuti nell'array a: al termine del ciclo f o r (dalla riga 4 alla 9), se la chiave k è statatrovata, la variabile i n d i c e contiene la posizione della sua prima occorrenza. Nel casopessimo, quando la chiave cercata non è tra quelle nella sequenza ( i n d i c e = —1), l'al-goritmo richiede un numero di operazioni proporzionale al numero di elementi presentinella sequenza, e quindi tempo O(n).

2.4.1 Ricerca binaria

E possibile fare di meglio qua nd o la sequenza è ordinata? Usando la ricerca binaria (odicotomica) , il costo si riduce a O(l ogn ) al caso pessimo: invece di scandire miliardidi dati durante la ricerca, ne esaminiamo solo poche decine! Un mod o folkloris ticodi introdurre il metodo si ispira alla ricerca di una parola in un dizionario (o di unnumer o in un elenco telefonico). Prendiamo la pagina centrale del dizionario: se laparola cercata è alfabeticamente precedente a tale pagina, strappiamo in due il dizionarioe ne buttiamo via la metà destra; se la parola è alfabeticamente successiva alla paginacentrale, but tiamo via la metà sinistra. Alt riment i, l 'unica possibilità è che la parolasia nella pagina centrale. Con un numero limitato di operazioni possiamo d iminuire

Page 51: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 51/373

 

RicercaBinarialterativaC a, k ) : {pre: la lunghezza di a èn)sinistra = 0;

destra = n-1;

trovato = FALSE;

indice = -1; WHILE ((sinistra <= destra) && (¡trovato)) {

centro = (sinistra+destra)/2;

IF (a[centro] > k) {

destra = centro-1;

> ELSE IF (a[centro] < k) {

sinistra = centro+1;

> ELSE {

indice = centro;

trovato = TRUE;

}>

RETURN i n d i c e ;

Co di ce 2 .8 Ricerca binaria di una chiave k in un array a.

il numero di pagine da cercare di circa la metà . Basta ripetere il me todo per ridurreesponenzialmente tale numero fino a giungere alla pagina cercata o concludere che la

parola non appare in alcuna pagina.Analogamente possiamo cercare una chiave k quando l'array a è ordinato in modo

non decrescente, basandoci su operazioni di semplice con fronto tra due elementi. Co n-frontiamo la chiave k con l' elemento che si trova in posizione centrale nell'array, a[ n/ 2] ,e se k è minore di tale elemento, ripetiamo il procedimento nel segmento costituito da-gli elementi che precedono a [n /2 ]. Altrimenti, lo ripetiamo in quello costituito daglielementi che lo seguono. Il procedimento te rmina nel mom ento in cui k coincide conl'elemento centrale del segmento corrente (nel qual caso, abbiamo trovato la posizio-ne corrispondente) oppure il segmento diventa vuoto (nel qual caso, k non è presente

nell'array).Il Codice 2.8 mantiene implicitamente l'invariante, per le due variabili s i n i s t r a

e d e s t r a che delimitano il segmento in cui effettuare la ricerca, secondo la quale va-le a [ s i n i s t r a ] ^ k ^ a [ d e s t r a ] . Inizialmente, queste due variabili sono poste a 0 en— 1, rispet tivamente (righe 2 e 3). Possiamo escludere i casi limite in cui k < a[0] oppu-re a[n — 1] < k, per cui pre sumiamo senza perdita di generalità che a[0] ^ k ^ a[ n — 1].A ogni iterazione del ciclo w h i l e (dalla riga 6 alla 16), se la chiave k è minore dell'e-lemento centrale (il cui indice di posizione è dato dalla semisomma delle due variabilis i n i s t r a e d e s t r a ) , la ricerca prosegue nella parte sinistra del segmento: per questo

Page 52: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 52/373

 

motivo, la variabile d e s t r a viene modificata in modo da indicare l'elemento immedia-tamente precedente a quello centrale (riga 9). Se, invece, la chiave k è maggiore del valoredell'elemento centrale, la ricerca prosegue nella parte destra del segmento (riga 11). Se,infine, la chiave è stata trovata, l'indice di posizione della sua occorrenza viene memoriz-

zato nella variabile i n d i c e e il ciclo ha termine in quanto la variabile t r o v a t o assumeil valore vero (righe 13 e 14). Quando la chiave non appare, il segmento diventa vuotopoiché s i n i s t r a > d e s t r a .

Osserviamo che a ogni iterazione del ciclo while, la lunghezza del segmento in cuideve proseguire la ricerca viene all'incirca dimezzato. Pertanto, la prima iterazione riducela ricerca da n a circa n / 2 elementi; la seconda iterazione la riduce a circa n / 4 elementi;la terza la riduce a circa n/8, e così via. In generale, l'iterazione i-esima riduce la ricerca acirca n/2 l elementi. Al caso pessimo, la chiave viene trovata all'ultima iterazione i*, percui n/2 l = 1. Se la chiave non è inclusa nella sequenza, occorre un'ulteriore iterazione.

Quindi con un numero di iterazioni non superiore a i* + 1 = O(logn), la chiave specifi-cata viene trovata oppure l'algoritmo conclude che tale chiave non appare. La ricerca inuna sequenza ordinata richiede quindi O(logn) tempo.

ALVIE: ricerca binaria

Osserva, sperimenta e verifica

B i n a r y S e a r c h

La strategia dicotomica rappresentata dalla ricerca binaria è utile in tutti quei proble-mi in cui vale una qualche proprietà monotona boolena P(x) al variare di un parametro ,intero x: in altre parole, esiste un intero Xo tale che P(x) è vera per x ^ Xo e falsa perx > xo (o, viceversa, P(x) è falsa per x ^ xo e vera per x > xo). Usando uno schemasimile alla ricerca binaria possiamo trovare il valore di xo- Per esempio, ipotizziamo didover indovinare il valore di un numero n > 0 pensato segretamente da un nostro ami-co. E dispendioso chiedere se n = 1, n = 2, n = 3, e così via per tutta la sequenzadi numeri da 1 fino a n (che rimane sconosciuto fino all'ultima domanda), in quantorichiede di effettuare n domande.

Usando il nostro schema, possiamo porre concettualmente XQ = n e, come primacosa, effettuare domande del tipo "x ^ n?" per potenze crescenti x = 1,2,4,8,..., 2 H ecosì via. Pur non conoscendo il valore di n, ci fermiamo non appena 2 h _ 1 < n ^ 2 h (acausa della riposta affermativa del nostro amico), ef fet tuando in tal modo h. = O( logn)domande poiché h. < 1 +logn. A questo punto, sappiamo che n appartiene all'intervallo[2 h _ 1 + 1... 2h] e applichiamo una variante del Codice 2.8 in cui la chiave k è il valoresconosciuto di n (in altre parole, i due confronti alle righe 8 e 10 diventano domande

Page 53: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 53/373

 

da porre al nostro amico che ha pensato il numero k = n). Con ulteriori 0(log2 h _ 1 ) =O( logn) domande, riusciamo a indovinare il numero n. In totale, abbiamo ridotto ilnumero di domande da effettuare al nostro amico da n a O(logn) per indovinare ilnumero n da lui segretamente pensato.

2.4.2 Complessità della ricerca per confronti

Il costo della ricerca binaria rappresenta un limite superiore di O(logn) per il problemadella ricerca di una chiave usando confronti tra gli elementi di una sequenza ordinata.Seguendo le argomentazioni del Paragrafo 2.3.1, vogliamo ora stabilire un limite inferio-re al problema per dimostrare che l'algoritmo di ricerca binaria è asintoticamente ottimo,stabilendo così anche la complessità del problema della ricerca per confronti in una se-quenza ordinata: ogni algoritmo di ricerca per confronti (non soltanto la ricerca binaria)ne richiede Q(logn) al caso pessimo.

Usiamo un semplice approccio combinatorio basato sulla teoria dell'informazio-ne. Sia A un qualunque algoritmo di ricerca che usa conf ron ti tra coppie di elemen-ti. L'algoritmo A deve discernere tra n + 1 situazioni: la chiave cercata non apparenella sequenza ( i n d i c e = —1), oppure appare in una delle n posizioni della sequenza(0 ^ indice < n — 1). Durante l'esecuzione, A esegue dei confronti, ognuno dei qualidà luogo a tre possibili riposte in [<, =, >]: quindi il primo confronto permette di di-scernere tra al più tre situazioni, il secondo amplia il numero di situazioni di al più unfattore tre, e così via.

Procedendo in questo modo, possiamo concludere che dopo t confronti di chiavi(mai esaminate prima), l'algoritmo A può discernere al più 3 l situazioni. Poiché vie-ne richiesto di discernerne n + 1, deve valere 3 l ^ n + 1. Ne deriva che occorronot ^ log3(n + 1) = n(logn) confronti: ciò rappresenta un limite inferiore per il proble-ma della ricerca per confronti. Poiché limite superiore e inferiore coincidono asintotica-mente, abbiamo che la complessità del problema è 0(logn) e che la ricerca binaria è unalgoritmo ottimo.

2.5 Ricorsione e paradigma del divide et imperaIl paradigma del divide et impera è uno dei più utilizzati nella progettazione di algoritmiricorsivi e si basa su un principio ben noto a chiunque abbia dovuto scrivere programmidi complessità non elementare. Tale principio consiste nel suddividere un problema indue o più sotto-problemi, nel risolvere tali sotto-problemi, eventualmente applicandonuovamente il principio stesso, fino a giungere a problemi "elementari" che possanoessere risolti in maniera diretta, e nel combinare le soluzioni dei sotto-problemi in modoopportuno così da ottenere una soluzione al problema di partenza.

Page 54: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 54/373

 

RicercaBinariaRicorsiva( a, k, sinistra, destra ):

{pre: 0 sinistra destra n - 1)

IF (sinistra == destra) {

IF (k == a[sinistra]) {

RETURN sinistra;

> ELSE {RETURN -1;

>>centro = (sinistra+destra)/2;

IF (k <= a[centro]) {

RETURN RicercaBinariaRicorsiva( a, k, sinistra, centro );

} ELSE {

RETURN RicercaBinariaRicorsiva( a, k, centro+1, destra );

>

Codice 2 .9 Ricerca binaria di una chiave k in un array a con ¡1 paradigma del divide et impera.

Quello che contraddistingue il paradigma del divide et impera è il fatto che i sotto-problemi sono istanze dello stesso problema originale ma di dimensioni ridotte: il fattoche un sotto-problema sia elementare o meno dipende, essenzialmente, dal fatto che ladimensione dell'istanza sia sufficientemente piccola da poter risolvere il sotto-problemain maniera diretta.

Il paradigma del divide et impera può, dunque, essere strutturato nelle seguenti tre

fasi che lo caratterizzano.

Decomposizione: identifica un numero piccolo di sotto-problemi dello stesso tipo, cia-

scuno definito su un insieme dei dati di dimensione inferiore a quello di partenza.

Ricorsione: risolvi ricorsivamente ciascun sotto-problema fino a ottenere insiemi di dati

di dimensioni tali che i sotto-problemi possano essere risolti direttamente.

Ricombinazione: combina le soluzioni dei sotto-problemi per fornire una soluzione al

problema di partenza.

Osserviamo che l'algoritmo di ricerca binaria può essere interpretato come un'appli-cazione del paradigma del divide et impera. In effetti, la ricerca di una chiave all'internodi un array di n elementi ordinati, viene realizzata risolvendo il problema della ricercadella chiave in un array di dimensione pari a circa la metà di quella dell'array originale,fino a giungere a un array con un solo elemento, nel qual caso il problema diviene chiara-mente elementare. Più formalmente, tale descrizione della ricerca binaria è mostrata nelCodice 2.9, in cui le istruzioni alle righe 3-9 risolvono in maniera diretta il caso elemen-tare, mentre le istruzioni alle righe 12 e 14 riducono il problema della ricerca all'interno

Page 55: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 55/373

 

del segmento attuale a quello della ricerca nella metà sinistra e destra, rispettivamente,del segmento stesso. Nella chiamata iniziale, il parametro s i n i s t r a assume il valore 0e il parametro d e s t r a assume il valore n — 1.

Notiamo che, modificando semplicemente le righe 4-8 in modo che restituiscanola posizione s i n i s t r a se k < a [ s i n i s t r a ] e la posizione s i n i s t r a + 1 altrimenti(invece che il valore —1), la ricerca binaria così modificata restituisce il rango r di k(dove 0 ^ r < n) , definito come il numero di elementi in a che sono minori o uguali a k.Inoltre, nel caso che l'array contenga un multi-insieme ordinato, dove sono ammesse leoccorrenze multiple delle chiavi, il Codice 2.9 individua la posizione dell'occorrenza piùa sinistra della chiave k (non è difficile modificarlo per individuare quella più a destra).

2.5.1 Equazioni di ricorrenza e teorema fondamentale

La formulazione ricorsiva della ricerca binaria necessita di un nuovo strumento analitico

per valutarne la complessità temporale, facendo uso di equazioni di ricorrenza, ovvero diespressioni matematiche che esprimono una funzione T(n) sugli interi come una com-binazione dei valori T(i ), con 0 ^ i < n. A tale scopo, osserviamo che se il segmentoall'interno del quale stiamo cercando una chiave è costituito da un solo elemento, allorail Codice 2.9 esegue un numero costante c di operazioni. Altrimenti, il numero di ope-razioni eseguite è pari a una costante c' più il numero di passi richiesto dalla ricerca dellachiave in un segmento di dimensione pari alla metà di quello attuale. Pertanto, il nume-ro totale T(n) di passi eseguiti su un array di n elementi verifica la seguente equazione diricorrenza:

T ( n ) = { T(n /2 ) + c' altrimenti ( 2 - 1 )

Al fine di derivare una formulazione in forma chiusa di T(n) utilizzeremo il teoremafondamentale delle ricorrenze (master theorem), che consente di risolvere equazioni diricorrenza di questo tipo (e di cui diamo dimostrazione nell'appendice).

Il teorema fondamentale delle ricorrenze afferma che, se f(n) è una funzione e a, |3e TLQ sono tre costanti tali che a ^ l , | 3 > l e n o > 0 , allora l 'equazione di ricorrenza

-j-r ^ _ f 0( 1) se n i : n 0 2 )1 ' \ aT (n/ P) + f( n) altrimenti V ' '

(dove n/(3 va interpretato come | n /PJ o [n/ |3 ]) ha le seguenti soluzioni per ogni n:

1. T(n) = 0(f(n)) se esiste una costante y < 1 tale che af (n / |3 ) = yf ( n) ;

2. T(n) = 0(f(n) logp n) se af(n/(3) = f(n);

3. T(n) = Otn'06!5 a ) se esiste una costante y' > 1 tale af(n/|3) = y'  f(n).

Page 56: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 56/373

 

L'interpretazione dell'equazione (2.2) in termini del paradigma del divide et imperaè che applichiamo il caso base quando ci sono al più no elementi; altrimenti, dividiamogli n elementi in gruppi da n/(3 elementi ciascuno, effettuiamo a chiamate ricorsive (cia-scuna su n/ |3 elementi) e impieghiamo tempo f (n) per eseguire le fasi di decomposizionee di ricombinazione (ossia f (n) conteggia tut ti i costi tranne quelli dovuti alle chiamatericorsive).

Nel caso dell 'algoritmo di ricerca binaria, abbiamo che a = 1, (3 = 2 e f(n) = c' =0(1) per ogni n, per cui rientriamo nel secondo caso del teorema fondamentale dellericorrenze. Come già dimostrato nel paragrafo precedente, possiamo quindi concludereche la ricerca binaria in un array ordinato richiede O(logn) passi.

La ricerca binaria è un esempio molto particolare di applicazione del paradigma deldivide et impera, in quanto il problema originale viene decomposto in un solo sotto-problema e la fase di ricombinazione consiste semplicemente nel restituire esattamente

la soluzione prodot ta per il sotto-problema. Nel resto di questo paragrafo, forniamo degliesempi più articolati di applicazione del divide et impera per mostrarne l'uso generale.

2.5.2 Moltiplicazione veloce di due numeri interi

Un numero intero di n cifre decimali, con n arbitrariamente grande, può essere rappre-sentato mediante un array x di n + 1 elementi, in cui x[0] è un intero che rappresenta ilsegno (ossia +1 o —1) e x[i] è un intero che rappresenta l'i-esima cifra più significativa,dove 0 < x[i] ^ 9 e 1 ^ i ^ n . Ovviamente, tale rappresentazione è più dispendiosa

dal punto di vista della memoria utilizzata, ma consente di operare su numeri arbitraria-mente grandi.1 Vediamo ora come sia possibile eseguire le due operazioni di somma e diprodotto facendo riferimento a tale rappresentazione.

Nel seguito, senza perdita di generalità, supponiamo che i due interi da sommare omoltiplicare siano rappresentati entrambi mediante n cifre decimali dove n è una potenzadi due: in caso contrario, infatti, tale condizione può essere ottenuta aggiungendo allaloro rappresentazione una quantità opportuna di 0 nelle posizioni più significative.

Per quanto riguarda la somma, il familiare algoritmo che consiste nell'addizionare lesingole cifre propagando l'eventuale riporto, richiede 0 (n ) passi ed è quindi ott imo. Non

possiamo fare lo stesso discorso per l'algoritmo di moltiplicazione che viene insegnatonelle scuole, in base al quale viene eseguito il prodotto del moltiplicando per ogni cifradel moltiplicatore, eseguendo poi n addizioni di numeri di 0(n) cifre per ottenere ilrisultato desiderato: pertanto, la complessità di tale algoritmo per la moltiplicazione è0 ( n 2 ) tempo.

'Il problema di gestire numeri interi arbitrariamente grandi ha diverse applicazioni tra cui i protocollicrittografici: esistono apposite implementazioni per molti linguaggi di programmazione, come, ad esempio,la classe B i g l n t e g e r del pacchetto j av a .m a th .

Page 57: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 57/373

 

Facendo uso del paradigma del divide et impera e di semplici uguaglianze algebrichepossiamo mostrare ora come ridurre significativamente tale complessità. Osserviamoanzitutto che possiamo scrivere ogni numero intero w di n cifre come 10 n^2

x w s + w j ,dove w s denota il numero formato dalle n / 2 cifre più significative di w e w j denota ilnumero formato dalle n/2 cifre meno significative. Per moltiplicare due numeri x e y,vale quindi l'uguaglianza

xy = (10 n / 2 x s + x d ) ( 1 0 n / 2 y s + y d ) = 10 n x s y s + 10 n / 2 (x s y d + x d y s ) + x d y d

che ci conduce al seguente algoritmo basato sul paradigma del divide et impera.Decomposizione: se x e y hanno almeno due cifre, dividili come numeri x s , x d , y s e

y d aventi ciascuno la metà delle cifre.Ricorsione: calcola ricorsivamente le moltiplicazioni x s y s , x s y d , x d y s e x d y d .Ricombinazione: combina i numeri risultanti usando l'uguaglianza suddetta.

Quindi, indicato con T(n) il numero totale di passi eseguiti per la moltiplicazione didue numeri di n cifre, eseguiamo quattro moltiplicazioni di due numeri di n/2 cifre,dove ciascuna moltiplicazione richiede un costo di T(n/2), e tre somme di due numeridi n cifre. Osserviamo che per ogni k > 0, la moltiplicazione per il valore 10k puòessere realizzata spostando le cifre di k posizioni verso sinistra e riempiendo di 0 la partedestra. Pertanto, il costo della decomposizione e della ricombinazione è O(n) tempo e,riscrivendo tale termine come c ' n per una costante c' > 0 e indicando il costo per il casobase con la costante c > 0, possiamo esprimere T(n) mediante l'equazione di ricorrenza

f c se n = 1T ( n ) = { 4 T ( n / 2 ) + c ' n a ltrimenti ( 2 ' 3 )

Applicando il teorema fondamentale delle ricorrenze all'equazione (2.3), otteniamo a =4, (3 = 2 e f( n) = c ' n nell'equazione (2.2). Poiché af (n / |3 ) = 4c' (n /2) = 2c' n =2f(n), rientriamo nel terzo caso del teorema con y' = 2. Tale caso consente di affer-mare che il numero di passi richiesti è 0(n'°6 4) = 0(n 2 ), non migliorando quindi leprestazioni del familiare algoritmo di moltiplicazione precedentemente descritto.

Tuttavia, facendo uso di un'altra semplice uguaglianza algebrica possiamo migliorare

il costo computazionale dell'algoritmo basato sul paradigma del divide et impera. Osser-viamo infatti che il valore x s y d + x d y s presente nella precedente uguaglianza, può esserecalcolato.facendo uso degli altri due valori x s y s e x d y d nel modo seguente:

xsy<i + x d y s = x s y s + x d y d - (x s - x d ) x (ys - y d )

Quest'osservazione permette di formulare un nuovo algoritmo di moltiplicazione, de-scritto nel Codice 2.10, che richiede soltanto tre moltiplicazioni (righe 11, 14 e 16) eun numero costante di somme: a tal fine, utilizziamo la funzione Somma per calcolare

Page 58: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 58/373

 

MoltiplicazioneVeloceC x, y, n ): (pre: x e y interi di n cifre)IF (n == 1) {

prodottoti] = (x[l] x y[1]) /  10;

prodotto [2] = (x[l] x y[l]) '/. 10;

> ELSE {

xs[0] = xd[0] = ys [0] = yd[0] = 1;FOR  (i = 1; i <= n/2; i = i + 1) {

xs[i] = x[i] ; ys[i] = y[i] ;

xd[i] = x[i + n/2]; yd[i] = y[i + n/2];

}pi = MoltiplicazioneVeloceC xs, ys, n/2 );

FOR  (i = 0; i <= n; i = i+1)

{ prodottoti] = piti]; prodottoti+n] = 0; }

p2 = MoltiplicazioneVeloceC xd, yd, n/2 );

xdtO] = yd[0] = -1;

p3 = MoltiplicazioneVeloceC Somma(xs.xd), SommaCys,yd), n/2);P3t0] = -p310];

add = Sommai pi, p2, p3 );

parziale t0] = add[0];

FOR  (i = 1; i <= 3 x n/2; i = i+1)

{ parziale ti] = addti + n/2]; parziale ti + n/2] = 0; >

prodotto = Somma( prodotto, parziale, p2 );

>prodotto[0] = xtO] x y[0];

RETURN prodotto ; {post: prodotto intero di 2n cifre)

Codice 2. 10 Moltipl icazione mediante la tecnica del divide et impera, dove Somma calcolal'addizione di al più tre numeri interi rappresentati come array, in tempo lineare.

l'addizione di al più tre numeri interi in tempo lineare nel loro numero totale di cifredecimali e, attraverso Somma, possiamo ottenere la sottrazione complementando il segnodell'operando sottratto poiché x — y = x + (—y ). Seppur concettualmente semplice, l'al-goritmo richiede un'implementazione attenta delle varie operazioni, come mostrato nel

Codice 2.10, il quale prende in ingresso due numeri con n cifre decimali e ne restituisceil prodotto su 2n cifre.

Nel caso base (n = 1) effettuiamo il prodot to diretto delle singole cifre, ripo rtandoneil risultato su due cifre (righe 3—4) e il relativo segno (riga 24) . Nel passo indutt ivo,calcoliamo x s , Xd,y s e y^ pre nde ndo l 'op portuna metà delle cifre dal valore assoluto di xe y (righe 6- 10 ). Calcoliamo quindi p i = x s y s ricorsivamente su n/2 cifre (laddove pine ha n), e poni amo in p r o d o t t o (che ha 2n cifre) il valore di 10 n x x s y s (righe 11—13).Procediamo con il calcolo ricorsivo di p2 = Xjyd (riga 14) e p3 = (x s —Xd) x (y s — y d )

Page 59: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 59/373

 

(righe 15-16), entrambi di 2n cifre (notiamo che sia x s — Xd che y s — yd richiedonon/2 cifre). Poniamo il risultato di p 1 + p2 — p3 = x sy d +XdPs in a dd (righe 17-18), lacui moltiplicazione per 10 n^2 viene memorizzata in p a r z i a l e (righe 19-21). A questopunto, per ottenere il prodotto tra il valore assoluto di x e quello di y, è sufficientecalcolare la somma tra p r o d o t t o (che contiene 10 n

x x s y s ), parziale (che contiene1 0 n / 2 x (xsyd + x¿y s)) e p2 = Xdyd (riga 22). Il segno del prodotto viene infinecalcolato nella riga 24. Il numero totale di passi eseguiti è quindi pari a

{ 3 T(n /2 ) + c ' n altrimenti

dove c e c' sono due costanti positive. Applicando il teorema fondamentale delle ri-correnze alla (2.4), otteniamo a = 3, (3 = 2 e f(n) = c'n nell'equazione (2.2): an-che questa volta rientriamo nel terzo caso del teorema con y' = che consente di

affermare che il numero di passi richiesti è 0(n'°8 3) = 0(n 1 , 5 8 5 ), ottenendo un signi-ficativo miglioramento rispetto alla complessità quadratica dei precedenti algoritmi dimoltiplicazione.

L'idea alla base del Codice 2.10 può essere ulteriormente sviluppata spezzando i nu-meri in parti più piccole, per ottenere la moltipl icazione in O (n log n log log n) passi,analogamente a quanto accade nella trasformata veloce di Fourier, che è di grande im-portanza per una grande varietà di applicazioni, che vanno dall'elaborazione di segnalidigitali alla soluzione numerica di equazioni differenziali.

ALVIE: moltiplicazione veloce di numeri interi

Osserva, sperimenta e verifica

F a s t l n t e g e r P r o d u c t

2.5.3 Ordinamento per fusioneGli algoritmi di ordinamento che abbiamo descritto nel Paragrafo 2.2 hanno entrambiuna complessità quadratica nel numero di elementi d«l ordinare. Ragionando in modosimile a quanto fatto per determinare un limite inferiore alla complessità della ricerca perconfronti, un qualunque algoritmo di ordinamento, dopo avere effettuato t confronti,può discernere al più 3 1 situazioni distinte. Poiché il numero di possibili ordinamentidi n elementi è pari a n!, ovvero al numero di loro permutazioni, viene richiesto all'al-goritmo di discernere tra n! possibili situazioni: pertanto, deve valere 3 l ^ n! perché

Page 60: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 60/373

 

M§rgeSort( a, sinistra, destra ):

{pre: 0 sinistra, destra n — 1)

IF (sinistra < destra) {

centro = (sinistra+destra)/2;

MergeSort( a, sinistra, centro );

MergeSort( a, centro+1, destra );FusioneC a, sinistra, centro, destra );

Codice 2.11 Ordinamento per fusione di un array a.

altrimenti l'algoritmo certamente non sarebbe corretto. Dalla disuguaglianza

n ! = n ( n - l ) - - - l > n ( n - l ) - - - Q + l ) > = ( n / 2 ) " /  2

n/2 volte

deriva che 3* ^ ( n / 2 ) n / 2 e che quindi occorrono t ^ (n /2 ) lo g3 (n/2) = O(nlogn)

confronti: ciò rappresenta un limite inferiore per il problema dell'ordinamento per con-

fronti. Tale limite inferiore lascia quindi un margine per un potenziale miglioramen-

to delle prestazioni rispetto alla complessità 0(n 2 ) degli algoritmi di ordinamento per

inserimento e per selezione.

Facendo uso del paradigma del divide et impera, siamo ora in grado di formulare unalgoritmo ottimo, detto algoritmo di ordinamento per fusione {mergesort), che opera in

tempo O(nlogn) nel modo seguente.

Decompos izione: se la sequenza ha almeno due elementi, dividila in due sotto-sequenze

uguali (o quasi) in lunghezza (nel caso in cui abbia meno di due elementi non vi è

nulla da fare).

Ricorsione: ordina ricorsivamente le due sotto-sequenze.

Ricombinazione: fond i le due sotto-sequenze ordinate in un'unica sequenza ordinata.

L'algoritmo di ordinamento per fusione è descritto nel Codice 2.11. Per implemen-

tare l'algoritmo è però necessario specificare come le due sotto-sequenze ordinate possanoessere fuse. Esistono diversi modi per far ciò, sia mediante l'uso di memoria addizionaleche operando in loco: poiché in quest'ultimo caso l'algoritmo di fusione risulta piutto-sto compl icato, preferiamo fornire la soluzione che fa uso di un array aggiuntivo. Talesoluzione si ispira al metodo utilizzato per fondere due mazzi di carte ordinati in modocrescente. In tal caso, a ogni passo, per deter minare la carta di valore mini mo nei duemazzi è sufficiente confrontare le due carte in cima ai mazzi stessi: tale carta può esserequindi inserita in fondo al nuovo mazzo.

Page 61: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 61/373

 

Sia a un array e siano a[sx, ex] e a[cx + 1, dx] due segmenti adiacenti di a, ciascunoordinato in modo non decrescente. Per ottenere che l'intero segmento a[sx, dx] sia ordi-nato, possiamo utilizzare un array b d'appoggio che viene riempito nel modo seguente.Partendo da i = sx e j = ex + 1, memorizziamo il minimo tra a[i] e a[j] nella primaposizione libera di b: se tale minimo è a[i], allora incrementiamo di 1 il valore di i, altri-menti incrementiamo di 1 il valore di ). Ripetiamo questo procedimento fino quando idiviene maggiore di ex oppure j diviene maggiore di dx: nel primo caso, memorizziamoi rimanenti elementi del segmento a[cx+ 1, dx] (se ve ne sono) nelle successive posizionilibere di b, mentre, nel secondo caso, memorizziamo i rimanenti elementi del segmentoa[sx, ex] (se ve ne sono) nelle successive posizioni libere di b. Al termine di questo ciclo,b conterrà gli elementi del segmento a[sx, dx] ordinati in modo non decrescente, percui sarà sufficiente ricopiare b all'interno di tale segmento.

La fusione di due sequenze ordinate appena descritta è realizzata nel Codice 2.12.Poiché a ogni iterazione del ciclo while, l'indice t (che parte da sx e arriva al massimoa ex) oppure l'indice ) (che parte da cx+1 e arriva al massimo a dx) aumenta di 1, taleciclo può essere eseguito al più (ex — sx + 1) + (dx — ex) = dx — sx + 1 volte. Unosolo dei due cicli f or successivi (righe 16—18) viene eseguito, per al più ex — sx + 1 edx — ex iterazioni, rispettivamente. Infine, l'ultimo ciclo f or verrà eseguito dx — sx + 1volte: pertanto, la fusione dei due segmenti richiede un numero di passi 0(dx — sx),ovvero linearmente proporzionale alle lunghezze dei due segmenti da fondere.

Siamo adesso in grado di completare la descrizione dell'algoritmo di ordinamentoper fusione, che è mostrato nel Codice 2.11. Le prime tre istruzioni della struttura i f (che vengono eseguite solo se vi sono almeno due elementi da ordinare) corrispondonoalla fase di decomposizione del problema (riga 4) e a quella di soluzione ricorsiva deidue sotto-problemi (righe 5 e 6). L'invocazione della funzione Fusione realizza la fasedi ricombinazione (riga 7), fondendo in un unico segmento ordinato i due segmentiordinati prodotti dalla ricorsione.

Poiché ci sono due chiamate ricorsive sulla metà degli elementi e il calcolo dellafunzione Fusione richiede tempo lineare, possiamo affermare che il numero totale dipassi eseguiti dal Codice 2.11 su un array di n elementi è pari a

dove c e c' sono due valori costanti. Applicando il teorema fondamentale delle ricorrenzeall'equazione (2.5), otteniamo a = 2, (3 = 2 e f(n) = c 'n: anche questa volta rientriamonel secondo caso del teorema, in quanto af (n / |3 ) = 2 c ' n / 2 = c ' n = f( n) . Quindi,il numero di passi richiesti dall'ordinamento per fusione su un array di n elementi èO(nlogn), e abbiamo pertanto dimostrato che tale algoritmo è ottimo.

Osservando che uno stesso vettore b di n elementi può essere riutilizzato per tut-te le operazioni di fusione, possiamo concludere che l'algoritmo richiede O(n) spazio

se n = 1

altrimenti(2.5)

Page 62: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 62/373

 

Fusione( a, sx, ex, dx ): (pre: 0 sx ex dx n — 1)

i = sx;

j = cx+1;

k = 0;

 WHILE ((i <= ex) && (j <= dx)) {

IF (a[i] <= a[j] ) {b[k] = a[i] ;

i = i+1;

> ELSE {

b[k] = a[j] ;

j = j+i;

>k = k+1;

>FOR  ( ; i <= ex; i = i+1, k = k+1)

b [k] = a[i] ;FOR  ( ; j <= dx; j = j+1, k = k+1)

b[k] = a[j] ;

FOR  (i = sx; i <= dx; i = i+1)

a[i] = b[i-sx] ;

Codice 2.12 Fusione di due segmenti adiacenti ordinati.

aggiuntivo. Notiamo, infine, che la scansione sequenziale dei dati da fondere rende l'or-dinamento per fusione uno dei principali metodi di ordinamento per grandi quantitàdati che risiedono nella memoria secondaria del calcolatore (ad esempio, un disco rigi-do), notoriamente con accesso più lento della memoria principale (che non può ospitaretutti i dati in tale scenario).

ALVIE: ordinamento perfusione

Osserva, sperimenta e verifica

MergeSort

2.5.4 Ordinamento e selezione per distribuzione

L'algoritmo di ordinamento per distribuzione (quicksort ) segue il paradigma del divide

et impera come quello per fusione ma, al contrario di quest'ultimo, la fase di decompo-

Page 63: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 63/373

 

QuickSort( a, sinistra, destra ):

{pre: 0 < sinistra, destra n — 1)

IF (sinistra < destra) {

scegli pivot nell'intervallo [sinistra...destra];

perno = Distribuzione( a, sinistra, pivot, destra );

QuickSort( a, sinistra, perno-1 );QuickSort( a, perno+1, destra );

Codice 2 .13 Ordinamento per distribuzione di un array a.

sizione è più evoluta mentre quella di ricombinazione è immediata. In particolare, talealgoritmo opera nel modo seguente.

Decomposizione: se la sequenza ha almeno due elementi, scegli un elemento pivot e

dividi la sequenza in due sotto-sequenze, dove la prima contiene elementi minori

o uguali al pivot e la seconda contiene elementi maggiori o uguali.

Ricorsione: ordina ricorsivamente le due sotto-sequenze.

Ricombinazione: concatena (implicitamente) le due sotto-sequenze ordinate in un'uni-ca sequenza ordinata.

Il Codice 2.13 implementa l'algoritmo di ordinamento per distribuzione secondo loschema descritto sopra. Nella riga 4, viene scelta una posizione p i v o t : come vedre-

mo, la scelta della posizione è rilevante ma, per adesso, supponiamo di scegliere semprel'ultima posizione nel segmento a [ s i n i s t r a , d e s t r a ] . Nella riga 5, gli elementi sonodistribuiti all'interno del segmento in modo che l'elemento pivot vada in a[perno]: nerisulta che gli elementi del segmento a[sinistra, perno — 1] sono minori o uguali dia[ pe rn o] mentre quelli del segmento a[ per no + 1, d e s t r a ] sono maggiori o uguali dia[perno]. Come possiamo osservare, dopo la ricorsione (righe 6-7) la ricombinazione èpraticamente nulla.

Il passo fondamentale di distribuzione degli elementi in base al pivot è rappresentatoda Distribuzione, illustrato nel Codice 2.14. Utilizzando una primitiva Scambia per

scambiare il contenuto di due posizioni nel segmento, l'elemento pivot viene spostatonell' ultima posizione del segmento (riga 2). Le rimanent i posizioni sono scandite condue indici (righe 3 e 4): una scansione procede in avanti finché non trova un elementomaggiore del pivot (righe 6 e 7) mentre l'altra procede all'indietro finché non trova unelemento minore del pivot (righe 8 e 9). Un semplice scambio dei due elementi fuoriposto (riga 10) permette di procedere con le due scansioni, che terminano quando glielementi scanditi si incrociano, uscendo di fatto dal ciclo esterno (righe 5- 11 ). Unulteriore scambio (riga 12) permette di collocare il pivot nella posizione i che viene

Page 64: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 64/373

 

Distribuzione( a, sx, px, dx ): (pre: 0 sx px < dx < n — 1)

IF (px != dx) Scambiai px, dx );

i = sx;

j = dx-1;

 WHILE (i < j) {

WHILE ( ( i < j ) kk  ( A [i] <= A [dx]) )i = i+1; WHILE ((i < j) k& (A [j] => A [dx]))

j = j-i;

IF (i < j) Scambiai i, j );

>IF (i != dx) Scambiai i, dx );

RETURN I;

Scambiai i, j ): (pre: sx i, j ^ dx)

temp = a[j] ; a[j] = a[i] ; a[i] = temp;

Codic e 2. 14 Distribuzione in loco degli elementi di un segmento a[sx, dx] in base alla posizionepx scelta per il pivot.

restituita come perno nell'algoritmo di ordinamento (riga 13). Poiché le due scansionisono eseguite in un tempo complessivamente lineare, il costo di D i s t r i b u z i o n e è O(n)tempo e 0(1) spazio (in quanto usiamo un numero costante di variabili di appoggio).

ALVIF: ordinamento per distribuzione

Osserva, sperimenta e verifica

QuickSort

Notiamo che il tempo di esecuzione dell'algoritmo di ordinamento per fusione può

essere 0(n 2 ) se l'array è già ordinato: in tal caso, infatti, a ogni chiamata ricorsiva la di-stribuzione degli elementi è estremamente sbilanciata, risultando sempre n — 1 elementiancora da ordinare. Non è difficile vedere che in questa situazione, l'analisi del costo èsimile a quella dell'ordinamento per selezione o per inserimento. Se invece la distribuzio-ne è bilanciata, la ricorsione avviene su ciascuna metà e otteniamo un costo la cui analisiè simile a quella dell'ordinamento per fusione, forne ndo un costo di O( n l o g n ) tempo(che rappresenta il caso migliore che possa capitare). Per il caso medio, è possibile di-mostrare che l'algoritmo richiede O(nlogn) tempo perché si possono alternare, durante

Page 65: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 65/373

 

la ricorsione, situazioni che danno luogo a una distribuzione sbilanciata con situazioniche conducono a distribuzioni bilanciate. Tuttavia, tale costo medio dipende dall'ordineiniziale con cui sono presentati gli elementi nell'array da ordinare.

Mostriamo ora un'analisi al caso medio più robusta che risulta essere indipendente

dall'ordine iniziale degli elementi nell'array e si basa sull'uso della casualità per far sì chela distribuzione sbilanciata occorra con una probabilità trascurabile: semplicemente sce-gliamo p i v o t in modo aleatorio, equiprobabile e uniforme, nella riga 4 del Codice 2.13.Il risultato di tale scelta casuale è che il valore di pe r no restituito nella riga 5 è uniforme-mente distribuito tra le (equiprobabili) posizioni del segmento a [ s i n i s t r a , d e s t r a ] .Supponiamo pertanto di dividere tale segmento in quattro parti uguali, chiamate zone.In base a quale zona contiene la posizione perno restituita nella riga 5, otteniamo iseguenti due eventi equiprobabili:

• la posizione p e r n o ricade nella prima o nell'ul tima zona: in tal caso, p e r no è

detto essere esterno-,• la posizione p e r n o ricade nella seconda o nella terza zona: in tal caso, p e r n o è

detto essere interno.

Indichiamo con T(n) il costo medio dell'algoritmo QuickSort eseguito su n datiin ingresso. Osserviamo che la media ^y^ di due valori x e y può essere vista come laloro somma pesata con la rispettiva probabilità 5, ovvero jx + jy, considerando i duevalori come equiprobabili. Nella nostra analisi, x e y sono sostituiti da opportuni valoridi T(n) corrispondent i ai due eventi equiprobabili sopra introdot ti. Più precisamente,quando perno è esterno (con probabilità 5), la distribuzione può essere estremamente

sbilanciata nella ricorsione e, come abbiamo visto, quest'ultima può richiedere fino ax = T(n — 1) + c ' n ^ T(n) + O(n ) tempo, dove il termine O(n) si riferisce al costodella distribuzione effet tuata nel Codice 2.14. Qu ando invece pe r no è interno (conprobabilità 5), la distribuzione più sbilanciata possibile nella ricorsione avviene se pe r n ocorrisponde al minimo della seconda zona oppure al massimo della terza. Ne deriva unadistribuzione dei dati che porta alla ricorsione su circa j elementi in una chiamata diQu i c kS or t e | n elementi nell'altra (le altre distribuzioni in questo caso non possonoandare peggio perché sono meno sbilanciate). In tal caso, la ricorsione richiede al piùy = T( j ) + T( + O(n) tempo. Facendo la media pesata di x e y, otteniamo

per un'opportuna costante c' > 0. Moltiplicando entrambi i termini nella (2.6) per 2 erisolvendo rispetto a T(n), otteniamo

(2.6)

(2.7)

Page 66: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 66/373

 

QuickSelect( a, sinistra, r, destra ):

(pre: 0 sinistra r — 1 ^ destra n — 1)

IF (sinistra == destra) {

RETURN a[sinistra];

> ELSE {

scegli pivot nell'intervallo [sinistra...destra];perno = Distribuzione( a, sinistra, pivot, destra );

IF (r-1 <= perno) {

QuickSelectC a, sinistra, r, perno );

> ELSE {

QuickSelectC a, perno+1, r, destra );

>>

Cod ice 2.15 Selezione dell'elemento di rango r per distribuzione in un array a.

che è simile a un'equazione di ricorrenza se non per il fatto che al posto dell'uguaglianza

appare una disuguaglianza. Consideriamo allora la seguente equazione di ricorrenza

la cui soluzione dimostr iamo essere T' (n ) = O( n l o g n) nel Paragrafo 2.5.5. PoichéT(n) ^ T'(n), ne deriva che T(n) = O(nlogn) al caso medio e che questo dipendedalle scelte casuali di pivot piuttosto che dalla configurazione dei dati in ingresso: un talealgoritmo si chiama casuale o random perché impiega la casualità per sfuggire a situazionisfavorevoli, risultando più robusto rispetto a tali eventi (per esempio, in presenza di unarray già in ordine crescente). Inoltre, può essere impleme nta to usando solo O(l og n)celle di memoria aggiuntive in media.

Come il suo nome suggerisce, l'algoritmo di quicksort è molto veloce in pratica e vie-ne usato diffusamente per ordinare i dati in memoria principale (laddove l'ordinamento

per fusione Me r g e So r t è utile soprattu tto in memoria secondaria). La libreria standarddel linguaggio C usa un algoritmo di quicksort in cui il caso base della ricorsione si fermaquando n ^ no per una certa costante no > 1 : terminata la ricorsione, ogni segmento dial più no elementi va ordinato individualmente, ma basta una singola passata dell'ordi-namento per inserzione per ordinare tut ti questi segmenti in 0 ( n x UQ) = O(n) tempo(risparmiando la maggior parte delle chiamate ricorsive). In altre librerie standard, comequella del linguaggio C++, quando l'algoritmo di quicksort inizia a distribuire i dati inmaniera sbilanciata, viene sostituito dall'algoritmo di ordinamento per fusione.

se n = 1altrimenti

(2.8)

Page 67: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 67/373

 

Possiamo modificare lo schema ricorsivo del Codice 2.13 per risolvere il problemadella selezione dell'elemento con rango r in un array a di n elementi distinti, senza biso-gno di ordinarli (ricordiamo che a contiene r elementi minori o uguali di tale elemento):not iamo che tale problema diventa quello di trovare il minimo in a quando r = 1 e ilmassimo quando r = n . Per risolvere il problema per un qualunque valore di r con1 ^ T ^ n, osserviamo che la funzione Distribuzione del Codice 2.14 restituisce ilrango del pivot px e posiziona tutti gli elementi di rango inferiore alla sua sinistra e tuttiquelli di rango superiore alla sua destra. In base a tale osservazione, possiamo modificareil codice di ordinamento per distribuzione considerando che, per risolvere il problemadella selezione, è sufficiente proseguire ricorsivamente nel solo segmento dell'array con-tenente l'elemento da selezionare: otteniamo così il Codice 2.15, che determina talesegmento sulla base del confronto tra T — 1 e p e r n o (righe 8- 12 ). La ricorsione hatermine quando il segmento è composto da un solo elemento, nel qual caso, il codice re-stituisce tale elemento (notiamo che alcuni elementi dell'array sono stati spostati durante

l'esecuzione dell'algoritmo).

L'equazione di ricorrenza per il costo al caso medio è costruita in modo simile al-l'equazione (2.6), con la differenza che conteggiamo una sola chiamata ricorsiva (la piùsbilanciata) ottenendo T(n) ^ + |n) + c'n. Moltiplicando entrambi i termi-

ni per 2 e risolvendo rispetto a T(n), otteniamo T(n) < T(|n) + 2c'n, a cui associamoun'equazione di ricorrenza in cui T'(n) appare al posto di T(n) e la disuguaglianza diven-ta un'uguaglianza, come nell'equazione (2.8). Possiamo questa volta applicare il teoremafondamentale delle ricorrenze ponendo a = 1, |3 = | e f ( n ) = 2c ' n nell'equazione (2.2),

per cui rientr iamo nel primo caso (y = | ) ott enendo in media una complessità tempora-le O(n) per l'algoritmo random di selezione per distribuzione (osserviamo che esiste unalgoritmo lineare al caso pessimo, ma d'interesse più teorico).

2.5.5 Alternativa al teorema fondamentale delle ricorrenze

L'equazione di ricorrenza (2.8) non è risolvibile con il teorema fondamentale delle ricor-renze, in quanto non è un'istanza dell'equazione (2.2). In generale, quando un'equazio-ne di ricorrenza non ricade nei casi del teorema fondamentale delle ricorrenze, occorre

determinare tecniche di risoluzione alternative. Nello specifico dell'equazione (2.8), no-tiamo che il valore T'(n) (livello 0 della ricorsione) è ottenuto sommando a 2c'n i valorirestituiti dalle due chiamate ricorsive: quest'ultime, che costituiscono il livello 1 dellaricorsione, sono invocate l'una con input ^ e l'altra con input | n e, in corrispondenzadi tale livello, contribuiscono al valore T'(n) per un totale di 2c'^ + 2c'|n = 2c'n.

Passando al livello 2 della ricorsione, ciascuna delle chiamate del livello 1 ne genera

altre due, per un totale di quattro chiamate, rispettivamente con input ^ n , ^ n e

che contribuiscono al valore T'(n) per un totale di 2c'^ + 2 c ' ^ n + 2 c ' ^ n +

Page 68: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 68/373

 

2C|TT1 = 2c 'n in corrispondenza del livello 2. Non ci dovrebbe soprendere, a questopunto, che il contributo del livello 3 della ricorsione sia al più 2c'n (in generale qualchechiamata ricorsiva può raggiungere il caso base e terminare).

Per calcolare il valore finale di T'(n) in forma chiusa, occorre sommare i contri-buti di tutti i livelli. Il livello s più profondo si presenta quando seguiamo ripetuta-mente il "ramo ovvero viene soddisfatta la relazione ^ n = 1 da cui deriva ches = lo g ^ j n = O(logn) . Possiamo quindi limitare superiormente T' (n ) osservandoche ciascuno degli O(logn) livelli di ricorsione contribuisce al suo valore per al più2c 'n = O( n) e, pertanto, T' (n ) = O (nl og n) . Intuitivamente, dividere l'input n inproporzione a | e invece che (come accade nel caso dell 'algoritmo di ordina-

mento per fusione), fornisce comunque una partizione bilanciata perché la dimensionedi ciascuna parte differisce dall'altra soltanto per un fattore costante. La proprietà cheT' (n ) = O(n logn) può essere estesa a una partiz ione di n in proporzione a j e | e, ingenerale, in proporzione a 6 e 1 — 6 per una qualunque costante 0 < 6 < 1.

Da quanto discusso finora, possiamo dedurre una linea guida per lo sviluppo di unaforma chiusa della soluzione T(n) di un'equazione di ricorrenza del tipo

T ( r L Ì = / OH ) s e n ^ no ( J1 ' \ T(6 n) + T( pn ) + f( n) altrimenti K '

per due costanti positive 6 e p tali che 6 -I- p ^ 1. No n potendo applicare il teoremafondamentale delle ricorrenze, procediamo per passaggi intermedi con le corrispondentichamate ricorsive. La chiamata ricorsiva iniziale (livello 0) con input n contribuisce al

valore di T(n) per un totale di f(n) e dà luogo a due chiamate ricorsive di livello 1, unacon input n ' = ó n e l'altra con input ri " = p n , dove n ' + n " ^ n. Quest'ultime duechiamate contribuiscono per un totale di f(n') + f(n") e inoltre invocano ulteriori duechiamate ricorsive a testa, le quali costituiscono il livello 2 della ricorsione e ricevono ininput mo, mi, m.2 e m.3 t a h che mo + mi +m 2+m.3 ^ n . Dovrebbe essere chiaro a questopunto che queste chiamate contribuiscono per un totale di f( mo)+f(mi )+f(m.2) + f(m3)e inoltre invocano ulteriori due chiamate ricorsive a testa. La forma chiusa della soluzioneper l'equazione (2.9) è data dalla somma dei termini noti

T(n) = f(n) + [f(n') + f(n ")] + [f(mo) + f ^ ) + f(m 2 ) + f(m3)] + • • •

la cui valutazione dipende dal tipo della funzione f(n): per esempio, abbiamo visto chese f(n) = O(n), allora otteniamo T(n) = O(nlogn).

2.6 Opus libri: grafica e moltiplicazione di matrici

La definizione di sequenza lineare data all'inizio del capitolo può essere estesa anche alcaso in cui consideriamo un'organizzazione degli elementi di un insieme su un array

Page 69: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 69/373

 

A [0] [0] A[0] [1] A [0 ] [2] A[ 0] [3] A [0] [4]

A[ l ] [ 0 ] ACID [l] A[1] [2 ] A[1] [3] A [ l ] [4 ]

A [2] [0] A [2 ] [1] A [2] [2] A [2] [3 ] A [2 ] [4 ]

A [3] [0] A[ 3] [1] A [3] [2] A [3 ] [3 ] A [3] [4]

Fi gu ra 2. 5 Un array bidimensionale o matrice A 4 x 5 .

bidimensionale, o matrice. Anche nel caso degli array bidimensionali, gli elementi della

sequenza sono conservati in locazioni contigue della memoria: a differenza degli arraymonodimensionali, tali locazioni sono organizzate in due dimensioni, ovvero in righee colonne. Ogni riga contiene un numero di elementi pari al numero delle colonnedell'array e l'elemento contenuto nella colonna j della riga i di un array A viene indicatocon A[i][j], Ad esempio, nella Figura 2.5, viene mostrato un array bidimensionale A di 4righe e 5 colonne (indicato come A4x 5) e, per ogni elemento, viene mostrata la notazionecon cui esso viene indicato. L'organizzazione bidimensionale delle locazioni di memorianon modifica la principale proprietà degli array, ovvero la possibilità di accedere in mododiretto ai suoi elementi.2

Un tipo particolarmente importante di matrici è rappresentato dal caso in cui glielementi contenuti siano interi o reali: nate per rappresentare sistemi di equazioni linearinel calcolo scientifico, le matrici sono utilizzate, tra le altre cose, per risolvere problemi sugrafi e per classificare l'importanza delle pagine Web come vedremo nel seguito del libro.In questo paragrafo, discutiamo la loro importanza nel campo della grafica al calcolatore{computer graphics) e della visione artificiale. Una matrice A = A r x c può modellare ipunti luminosi {pixel) in cui è discretizzata un'immagine digitale contenuta nella memo-ria video ( frame buffer) avente risoluzione c x r pixel, dove 1024 x 768, 1280 x 1024,1400 x 1050, 1600 x 1200 e 1920 x 1200 sono alcuni dei formati digitali standard (da

notare che, nella risoluzione del video, indichiamo prima il numero delle colonne c e poiquello delle righe r). Il pixel che si trova in corrispondenza della riga i e della colonna jè rappresentato dall'elemento A[i][j], che specifica (direttamente o indirettamente) il co-

2Un array bidimensionale Ar><c di r righe e c colonne può sempre essere visto come un array monodi-mensionale b contenente r x c e lement i: per ogni i e j con 0 < i < r e 0 i j < c , l'accesso al l' elementoA[il[j] corrisponde semplicemente all'accesso a b[i x c + j] in tempo 0(1). Sebbene in questo libro non nefaremo mai uso, non è difficile immaginare come sia possibile estendere il concetto di array a k dimensionicon k > 2, cosi che l'accesso a un el emento dell'arr ay avvenga sulla base di k indici in temp o O(k).

Page 70: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 70/373

 

lore e la luminosità del pixel utilizzando uno certo numero di bit ( bit depth, solitamentepari a 24 o 32).

Quando usiamo un videogioco tridimensionale, stiamo più o meno inconsapevol-mente impiegando delle matrici nella rappresentazione delle scene che possiamo osser-vare muovendoci all'interno dello spazio di gioco. In genere queste scene sono realizzatemediante superfici composte da innumerevoli triangoli disposti nello spazio tridimen-sionale, i cui vertici di coordinate (x,y, z) sono rappresentati mediante vettori o array diquattro elementi [x, y, z, 1] (l'uso della quarta coordinata pari a 1 sarà chiarito fra breve).Per arrivare a mostrare sul frame buffer del video tali scene in forma digitale ( rendering)occorre che tutte le primitive geometriche che compongono ciascuna scena attraversinodiverse fasi di computazione:

1. una fase di trasformazione per scalare, ruotare e traslare le figure geometriche inbase alla vista attuale della scena;

2. una fase di illuminazione per calcolare quanta luce arrivi direttamente su ognivertice;

3. una fase di trasformazione per dare la prospettiva dell'occhio umano alla vistaattuale della scena;

4. una fase di ritaglio (clipping) per selezionare solo gli oggetti visibili nella vistaattuale;

5. una fase di proiezione della vista attuale in tre dimensioni in un piano bidimen-sionale (equivalente a un'inquadratura ottica simulata con la grafica vettoriale);

6. una fase di resa digitale (rastering) per individuare quali pixel del frame buffer

siano infine coperti dalla primitiva appena proiettata.La realizzazione efficiente di tali fasi di rendering richiede perizia programmativa e pro-fonda conoscenza algoritmica e matematica (ebbene sì, dobbiamo imparare molta ma-tematica per programmare la grafica dei videogiochi) e si basa sull'impiego massiccio dipotenti e specializzate schede grafiche. In particolare, la fase 1 effettua la trasformazionemediante operazioni di somma e prodotto di matrici, definite qui di seguito.

La somma A + B di due matrici A,-Xs e B r x s è la matrice C r x s tale che C[i][j] =A[i][j] + B[i][j] per ogni coppia di indici O ^ i ^ r — l e O ^ j ^ s — 1: ogni elemento dellamatrice C è quindi pari alla somma degli elementi di A e B nella medesima posizione.

Il prodotto A x B di due matrici A r x s e B s x t è la matrice C r x t tale che C[i][j] =A[i] [k] x B[k][j]) per ogni coppia di indici O ^ i ^ r — I e 0 < j ^ t - 1 : notiamo

che, contrariamente al prodotto tra due interi o reali, tale prodotto non è commutativomentre è associativo. L'elemento C[i][j] è anche detto prodotto scalare della riga i di Aper la colonna j di B.

Nella Figura 2.6 illustriamo come scalare, ruotare e traslare una figura mostrando, aifini della nostra discussione, tali operazioni solo per un punto bidimensionale [x,y, 1].Per scalare di un fattore a lungo l'asse delle ascisse e di un fattore (3 lungo quello delle

Page 71: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 71/373

 

Fi gu ra 2 .6 Operazioni su matrici per scalare, ruotare e traslare un punto di ascissa x e ordinata ynella fase 1 del rendering di un'immagine digitale.

ordinate, effettuiamo la moltiplicazione

[x,y, 1] x

a 0 0'

0 (3 0

0 0 1= [ax, |3y, 1]

Per ruotare di un angolo i>, osserviamo che la nuova posizione [x',y', 1] soddisfa larelazione trigonometrica x' = xcosO — y sin <D e y ' = xs in® + y cosO, per cui lacorrispondente moltiplicazione è la seguente:3

(x,y, 1] xcos a> sin O 0'

- sin Ì> cos Q 00 0 1

[x ' , y ' , l ]

'Se V è l'angolo che il punto (x, y ) forma con l'asse delle x e T è la sua distanza dall'origine (0,0), allora,usando le ben note uguaglianze trigonometriche cos(<D + M') = cos<Dcosf  — sinOsin V e sin(<t> + 4*) =sin O cosV+cos® sinf, otteniamo che x' = rcoslO+M 7) = rcos <D cosV—rsin <D sinH' ey' = r sin(C> -t-M7) =rsinOcosH' + rcos <t> sin M7. Poiché rcosM7 = x e rsin V = y, abbiamo che vale la relazione utilizzata per lamoltiplicazione.

Page 72: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 72/373

 

Infine, la traslazione di una quantità Ax sulle ascisse e di una quantità Ay sulle ordinate,necessita della dimensione fittizia per essere espressa come una moltiplicazione

[x,y, 1] x01

^X Ay

= [x + A x ,y + A y , 1]

Non è difficile estendere le suddet te trasformazioni al caso tridimensionale. Altre trasfor-mazioni possono essere espresse mediante la moltiplicazione di opportune matrici come,ad esempio, quella per rendere speculare una figura. Il vantaggio di esprimere tutte letrasformazioni in termini di moltiplicazioni risiede nel fatto che così esse possono esserecomposte in qualunque modo mediante la moltiplicazione delle loro corrispettive ma-trici. In altri termini, una qualunque sequenza di n trasformazioni applicate a un punto[x,y,z, 1] possono essere viste come la moltiplicazione di quest' ult imo per le n matriciAQ , A J , . . . , A N _ _ I che rappresentano le trasformazioni stesse:

[x,y,z, 1] x A0 x Aj x • • • x A n _ i

Tuttavia, dovendo ripetere tale sequenza per tutti i vertici delle figure che si voglionotrasformare è più efficiente calcolare la matrice A* = Ao x A[ x • • • x A n _ i una sola voltae quindi ripetere una singola moltiplicazione [x, y,z, 1] x A* per ogni vertice [x,y,z, 1]di tali figure (quest'ultime moltiplicazioni sono eseguite in parallelo dalla scheda graficadel calcolatore).

Secondo quanto discusso finora, le matrici coinvolte non hanno più di quattro righee quattro colonne. Tuttavia, per raggiungere un certo grado di realismo nel processo direndering è necessario simulare con buona approssimazione il comportamento della lucein una scena, calcolando ad esempio ombre portate, riflessioni ed effetti di illuminazioneindiretta. Nel calcolo dell'illuminazione indiretta, dobbiamo determinare quanta lucearrivi su di un punto, non direttamente da una sorgente luminosa come il sole, mariflessa dagli altri oggetti della scena.

Una delle soluzioni classiche a questo problema, noto come il calcolo della radiosità,consiste nel calcolare una matrice M m x m di vaste dimensioni per una scena composta

da m primitive elementari, in cui l'elemento M[i][j] della matrice descrive in percentualequanta luce che rimbalza sulla superficie i arriva anche sulla superficie j.

Tralasciando come ottenere tali valori, partiamo da un array Lo di m elementi in cuimemorizziamo quanta luce esce da ognuna delle m primitive (ponendo a 0 gli elementiper ogni primitiva eccetto che per le sorgenti luminose). Moltiplicando M per Lo siottiene un array L] in cui ogni elemento contiene l'illuminazione diretta della primitivacorrispondente. Cont inuando a moltiplicare per M, otteniamo una serie di array Li(dove i > 1) che rappresentano via via i contribu ti dei vari "rimbalzi" della luce sulle

Page 73: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 73/373

 

ProdottoMatrici ( A, B ) : (pre: A Ì B sono di taglia rxsisxt)FOR  (i = 0; i < r; i = i+1)

FOR  (j = 0; j < t; j = j+1) {

C[i] [j] = 0;

FOR  (k = 0; k < s; k = k+1)

C[i][j] = C[i][j] + A[i] [k] x B[k][j] ;>

RETURN C ; {post: C è di taglia r x t)

Co di ce 2. 16 Algoritmo per la moltiplicazione di due matrici in 0 ( r x s x t) tempo.

varie superfici, riuscendo così a determinare i contributi dell'illuminazione indiretta per

ogni primitiva.Nel resto del paragrafo ipotizziamo di avere un numero arbitrariamente grande di

righe e di colonne e studiamo la complessità dell'algoritmo di moltiplicazione di duematrici e di quello per determinare la sequenza ottima che minimizza il numero dioperazioni necessarie a calcolare il prodot to di n matrici .

2.6.1 Moltiplicazione veloce di due matrici

L'algoritmo immediato per calcolare la somma A + B di due matrici A e B, effettua r x s

operazioni di somma (una per ogni elemento di C): poiché dobbiamo sempre esaminareH(r x s) elementi nelle matrici, abbiamo che la somma di due matrici può essere quindieffettuata in tempo ottimo 0(r x s). Invece, l'algoritmo immediato per il prodotto didue matrici A r x s e B s x t , most rato nel Codi ce 2.16 , non è otti mo. Esso richiede dieffettuare O(s) operazioni per ognuno degli r x t elementi di C, richiedendo così untotale di 0(r x s x t) operazioni. A differenza della somma di matrici, in questo casoesiste una differenza, o gap di complessità, con il limite inferiore pari a O ( r x s + s x t ) .Restringendoci al caso di matrici quadrate, le quali hanno n = r = s = t righe e colonne,osserviamo che il limite superiore è 0(n 3 ) mentre quello inferiore è fl(n 2 ): ciò lascia

aperta la possibilità di trovare algoritmi più efficienti per il prodotto di matrici.Analogamente alla moltiplicazione veloce tra numeri arbitrariamente grandi (Para-

grafo 2.5.2), l'applicazione del paradigma del divide et impera permette di definire algo-ritmi per il prodotto di matrici con complessità temporale nettamente inferiore a quella0 ( n 3 ) dell'algoritmo descritto nel Codice 2.16. L'algoritmo di Strassen, che descriviamoper semplicità nel caso della moltiplicazione di matrici quadrate in cui n è una potenzadi due, rappresenta il primo e più diffuso esempio dell'applicazione del divide et impera:esso è basato sull'osservazione che una moltiplicazione di due matrici 2 x 2 può essere

Page 74: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 74/373

 

effettuata, nel modo seguente, per mezzo di 7 moltiplicazioni (e 14 addizioni), invece

delle 8 moltiplicazioni (e 4 addizioni) del metodo standard.

Consideriamo la seguente moltiplicazione da effettuare:

a b'X

e f 'c d .9 h.

ae + bg

ce + dg

af + bh'

cf + dh

Se introduciamo i seguenti valori

v0 = (b-d) (g + H)vi = (a + d)(e + h.)v2 = (a — c)(e + f)v3 =h(a + b)

v4 = a(f - h)

v5 = d(g - e)

v6 = e(c + d)

possiamo osservare che la moltiplicazione precedente può essere espressa come

"a b'x

e f" V 0 +Vi - v3 + V 5 v3 + V4

C d .9 h. v5 + v6 VI - V 2 + v 4 - v 6

Questa considerazione, che non pare introdurre alcuna convenienza nel caso di matrici2 x 2 , risulta invece interessante se consider iamo la moltiplicazione di matrici n x n pern > 2. In tal caso, ciascuna matrice di dimensione n x n può essere considerata comeuna matrice 2 x 2, in cui ciascuno degli elementi a, b , . . . , h. è una matrice di dimensionen / 2 x n / 2 : le relative operazioni di somma e moltiplicazione su di essi sono quindisomme e moltiplicazioni tra matrici.

Indicando con T(n) il costo temporale della moltiplicazione di due matrici n xn e applicando la considerazione precedente, osserviamo che T(n) è pari al costo diesecuzione di 7 moltiplicazioni tra matrici j x j , che esprimiamo come 7T(^), piùil costo di esecuzione di 14 somme di matrici anch'esse 7 X 7 , che possiamo stimarecome 0(n 2 ) . Da qua nt o det to deriva l'equazione di ricorrenza seguente, dove c e c'sono opportune costanti positive:

T ( n 7 T ( f ) + c ' n 2

se n ^ 2

altrimenti (2.10)

Applicando il teorema fondamentale delle ricorrenze all'equazione (2.10), con a = 7,

(3 = 2 e f(n) = c'n 2 nell'equazione (2.2), osserviamo che ci troviamo nel terzo caso

considerato dal teorema in quanto 7c'(y) 2 = | c ' n 2 (quindi, y'  = | > 1): pertanto,

T(n) = 0(n'°8 7) = 0(n 2 , 8 0 7 - ) - E tutto ra ignota la complessità della moltiplicazione

di due matrici quadrate e la congettura più diffusa è che sia 0(n e ) per una costante

2 < e < 3.

Page 75: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 75/373

 

ALVIE: moltiplicazione veloce di matrici

Osserva, sperimenta e verifica

StrassenMatr ixProduc t

f i !

2.6.2 Sequenza ottima di moltiplicazionie paradigma della programmazione dinamica

Il problema di trovare il modo più efficiente di moltiplicare una sequenza di matriciconsente di introdurre un altro importante paradigma algoritmico, la programmazionedinamica. Volendo illustrare tale paradigma, possiamo vederlo come un modo efficace

per tabulare un algoritmo ricorsivo: per esempio, nel calcolo dei numeri di Fibonacci,definiti ricorsivamente come Fo = 0. Fi = 1 e F n = + Fn 2 P e r n ^ 2 (quindi0,1, 1, 2,3, 5,7,12 e così via), l'algoritmo immediato ricorsivo per calcolare F n richiedeun tempo esponenziale in n perché ricalcola molte volte gli stessi valori F^ (k < n)già calcolati in precedenza, mentre un algoritmo iterativo impiega tempo lineare in nusando un array di appoggio f i b in cui scrive f ib [0] = Fo. f i"b[l] = Fi e i valoriintermedi f ib[k] = f ib [k —l ]+ f ib[k—2] ottenuti attraverso un ciclo per k = 2 , 3 , . . . , n(in realtà è possibile far di meglio, calcolando Fn in O(logn) tempo). Implicitamente,abbiamo applicato il paradigma della programmazione dinamica, che tratteremo più

specificatamente nel paragrafo successivo, implementando una regola di calcolo ricorsiva(di Fn) con un algoritmo iterativo che riempe gli elementi di un'opportuna tabella (fib)di cui restituiamo il valore nell'ultimo elemento (corrispondente a F n).

ALVIE: numeri di Fibonacci

Osserva, sperimenta e verifica

Fibonacci

Ripercorriamo il paradigma di programmazione dinamica brevemente illustrato so-pra, applicandolo al calcolo della sequenza ottima di moltiplicazioni di n > 2 matriciA* = Ao x Aj x • • • x A n _ j . Ai fini della nostra discussione, utilizziamo l'algoritmoimmediato (Codice 2.16) che moltiplica due matrici di taglia r x s e s x t con l'ipotesisemplificativa che tale algoritmo richieda un numero di operazioni esattamente pari ar x s x t, notando che quanto descritto si applica anche agli algoritmi più veloci comequello di Strassen.

Page 76: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 76/373

 

Dovendo eseguire n — 1 moltiplicazioni per ottenere A*, osserviamo che l'ordine concui le eseguiamo può cambiare il costo totale quando le matrici hanno taglia differente:nel seguito indichiamo con d i x d^+i la taglia della matrice Ai, dove 0 ^ i < n— 1. Datead esempio n = 4 matrici tali che do = 100, di = 20, d 2 = 1000, d3 = 2 e d4 = 50,nella seguente tabella mostriamo con le parentesi tutti i possibili ordini di valutazionedel loro prodotto A* = Ao x Ai x A2 x A3 (di taglia do x d 4), riportando il corrispettivocosto totale di esecuzione per ottenere A* :

(A 0 x (A I x (A2 x A3 )) d 2 d 3 d 4 + did 2 d 4 + dodid4 = 1.200.000(A 0 x ( (A, x A2 ) x A3 ) did 2 d 3 + did 3 d 4 + d 0 d i d 4 = 142.000((A0 x AI ) x (A2 x A3 ) ) d 0 d id 2 + d 2 d 3 d 4 + d 0 d 2 d 4 = 7.100.000( ( ( A 0 X A , ) X A 2 ) X A 3 ) d 0 di d 2 + d 0 d 2 d 3 + d 0 d 3 d 4 = 2.210.000((AQ x (AI x A2)) x A3 ) d i d 2 d 3 + d 0 d i d 3 + d 0 d 3 d 4 = 54.000

Per esempio, la quarta riga corrisponde all'ordine naturale di moltiplicazione da sini-stra verso destra: la moltiplicazione Ao x Ai richiede do x d] x d 2 operazioni e restituisceuna matrice di taglia do x d 2; la successiva moltiplicazione per A2 richiede do x d2 x d 3

operazioni e restituisce una matrice di taglia do x d 3); l'ultima moltiplicazione per A3

richiede do x d 3 x d 4 operazioni. Sommando tali costi e sostituendo i valori di do , . . . , d4 ,otteniamo un totale di 2.210.000 operazioni. Le altre righe della tabella mostrano cheil costo complessivo del prodotto può variare in dipendenza dell'ordine in cui sono ef-fettuate le singole moltiplicazioni per ottenere lo stesso risultato: in questo caso, apparemolto più conveniente effettuare le moltiplicazioni nell'ordine indicato nella quinta riga,

che fornisce un costo di sole 54.000 operazioni.Il problema che ci poniamo è quello di trovare la sequenza di moltiplicazioni che

minimizzi il costo complessivo del prodotto A* = Ao x Aj x • • • x A n _j per una datasequenza di n + 1 interi positivi do, d],..., d n (ricordiamo la nostra ipotesi che il costodella moltiplicazione di due matrici di taglia r x s e s x t sia pari a r x s x t) . Epossibile dimostrare che esiste un numero esponenziale di modi diversi di moltiplicare nmatrici,^ il che rende un esame esaustivo delle varie possibilità rapidamente impraticabileal crescere di n. Possiamo ottenere un modo più efficiente di affrontare il problemaconsiderando il sotto-problema di trovare il costo per effettuare il prodotto di un gruppo

consecutivo di matrici, Ai x A i + ) x • • • x Aj , dove 1, indicando conM(i, j) il corrispondente costo minimo-, chiaramente, in tal modo siamo anche in gradodi risolvere il problema iniziale calcolando M(0,n — 1).

Possiamo immediatamente notare che M(i, i) = 0 per ogni i, in quanto ha costonullo calcolare il prodotto della sequenza composta dalla sola matrice Ai. Se passiamo al

'Tale numero è il numero di Catalan C„ ] = ^ ( ¡ „ l i • ' ' c u ' andamento esponenziale sarà discusso

successivamente: anticipiamo comunque che C n _ i è il nu me ro di alberi binari distin ti con n — 1 nodi

interni, dove ogni nodo interno ha due figli e corrisponde a una coppia di parentesi.

Page 77: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 77/373

 

CostoMinimoRicorsivo( i, j ): (pre: 0 i, j ^ n - 1)

IF (i >= J) RETURN 0;

minimo = +oo;

FOR  (r = i; r < j ; r = r+1) {

costo = CostoMinimoRicorsivo( i, r );

costo = costo + CostoMinimoRicorsivoC r+1, j );costo = costo + d[i] x d[r+l] x d[j+l];

IF (costo < minimo) minimo = costo;

}

RETURN minimo;

Codice 2 .17 Algoritmo ricorsivo per il calcolo del costo minimo di moltiplicazione M(i, j).

caso di M (v, ) ) con i < j, osserviamo che possiamo ottenere la matrice Aix Ai + \ x • • • xAj

(di taglia di x dj+ i) fattorizzandola come una moltiplicazione tra At x • • • x A r (di tagliadi x dT+1) e A r + i x • • • x Aj (di taglia d r + i x dj + i), per un qualunque intero r taleche i ^ r < j. Il costo di tale mol tiplicazione è pari a di x d r + i x dj + i, a cui vannoaggiunti i costi minimi M( i, r) e M( r + 1, j) per calcolare rispettivamente A; x • • • x A r

e Ar+i x • • • x Aj. Supponiamo di aver già calcolato induttivamente quest'ultimi costiper tutti i possibili valori r con i ^ r < j: allora il costo mi ni mo M( i, j) sarà dato dalminimo, al variare di r, tra i valori M.(i, r) + M(r 4- 1, j) + did r + i dj + i. Da ciò deriva laseguente regola ricorsiva:

') - /  0 s e i > j (2 11)  \ mini^T.<j { M( i , r ) + M( r + l , j ) + d i d T + i d j + i } altrimenti

Il calcolo della soluzione della regola in (2.11) richiama l'utilizzo del paradigma del

divide et impera e, di conseguenza, suggerisce la definizione di un algoritmo ricorsivo

per il calcolo di M(i , j), come most rato nel Codice 2.17. La stru ttura dell'algoritmo

rispecchia quella della regola in (2.11): in particolare, il controllo alla riga 2 verifica se

siamo nel caso base in cui i ^ j, rest ituendo il valore 0. Altrimenti , il ciclo alle righe 4 -

9 determina, utilizzando le due chiamate ricorsive (righe 5 e 6), il costo minimo delprodot to aggiornandolo ogni qualvolta trova un valore più basso (riga 8): l'algoritmo

restituisce tale costo minimo nella riga 10.

Se analizziamo però con maggiore attenzione il comportamento dell'algoritmo, in

particolare dal punto di vista delle chiamate ricorsive effettuate, notiamo che esso pre-

senta una significativa replicazione di chiamate già effettuate. Ad esempio, la Figura 2.7

illustra come i valori M( 0, 1) , M ( l , 2 ) e M ( 2 , 3 ) vengano tutti calcolati due volte distin-

te nel corso del calcolo di M(0,3), mentre i valori M(l, 1) e M(2,2) sono calcolati ben

Page 78: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 78/373

 

M ( 0 , 0 )

M(l, 1)

M(l,l)|

M ( 2 , 2 ) |

M ( 0 , 0 ) |

M ( l , l ) |

M ( 0 , 0 )

M ( l , 3 )

M(0,2)

M(3,3)

Figura 2.7 Chiamate ricorsive per il calcolo di M(0,3 ).

cinque volte. Usare la ricorsione in tale situazione è estremamente inefficiente, al contra-rio di quanto succede con il paradigma del divide et impera: il motivo di tale inefficienzarisiede nel fatto che le chiamate ricorsive dell'algoritmo nel Codice 2.17 calcolano ripe-tutamente gli stessi valori, richiedendo così un numero esponenziale fl(2n) di passi dicalcolo come mostriamo di seguito.

Indichiamo con T(n) il numero di passi richiesti dall'algoritmo ricorsivo nel Codi-ce 2.17 per un'istanza di n matrici (usando i parametri iniziali i = 0 e j = n — 1). Nelcaso base n ^ 1, la complessità è una costante c > 0 (riga 2). Al triment i, abbiamo una

complessità costante c ' > 0 per le istruzioni alle righe 2, 3 e 10, a cui va aggiunta lacomplessità del ciclo alle righe 4-9. Per la generica iterazione r di quest'ultimo, abbiamodue chiamate ricorsive s u r + l e s u n — r + 1 matrici, rispettivamente, totalizzando unacomplessità di T(r + 1) + T(n — r — 1) passi, a cui va aggiunta la complessità costantec" > 0 per il resto delle istruzioni contenute nell'iterazione. Pertanto

T ( n ) - , . , 1 , + T ( n _ r _ 1 ) + C „ )S J ^ j , t i (2.12)

Page 79: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 79/373

 

, \ J o 1 2 3

0 0 2000000 44000 54000

1 - 0 40000 42000

2 - - 0 100000

3 - - - 0

Figura 2. 8 Tabella dei costi minimi per il prodotto di n = 4 matrici in cui d0 = 100, d, = 20, d2 =1000, d3 = 2,d4 = 50.

Ai fini della nostra discussione, possiamo porre c = c ' = c " = 1, esprimendo lacomplessità nel caso n > 1 dell'equazione (2.12) come

n-2

T(tv) = 1 + ^ ( T ( r + 1 )+ T( TI - T - 1) + 1)r=0

= n + (T(l) + T ( n - 1) + T(2) + T ( n - 2) + • • • + T ( n - 1 )+T(1) )

n— 1= n + 2 ^ T ( k )

k=l

A questo punto, possiamo mostrare che T(n) ^ 2 n _ 1 , per induzione su n > 0. Nel casobase, T( l) ^ 1 = 2 ° per definizione. Al passo induttivo, supponiamo che T(k) ^ 2 k _ 1

per ogni k < n: allora

n —1 n—1 n —2T(n) = n + 2 ^ T ( k ) ^ n + 2 ^ 2k ~ì = n + 2 2T  = n + 2(2 n "' - 1) >

k= 1 k= l r= 0

per n > 0, dove la prima disuguaglianza deriva dall'ipotesi indutt iva. Di conseguenza,il numero di passi e di chiamate ricorsive eseguite dal Codice 2.17 è esponenziale nelnumero n di matrici considerate.

Questo spreco di tempo di calcolo può essere eliminato ottenendo un algoritmopolinomiale: calcoliamo una sola volta i valori M(i, j) memorizzandoli in una tabella deicosti, realizzata mediante un array bidimensionale c o s t i di taglia n x n, in modo taleche costi[v][j] = M(i, j) per 0 ^ i ^ j ^ n— 1 (gli elementi della tabella corrispondentia valori di i e j tali che i > j non sono utilizzati dall'algoritmo). Illustriamo tale approccio

Page 80: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 80/373

 

CostoMinimoIterativoC ):

FOR  (i = 0; i < n; i = i+1) {

costi[i][i] =0;

>FOR (diagonale = 1; diagonale < n; diagonale = diagonale+ì) {

FOR (i = 0; i < n-diagonale; i = i+1) {j = i + diagonale;

costi [i] [j] = +oo;

FOR  ( R = i ; r < j ; r = r+1) {

costo = costiti] [r] + costi[r+1][j];

costo = costo + d[i] x d[r+l] x d[j+l];

IF (costo < costiti][j]) {

costi[i][j] = costo;

indice[i][j] = r;

>

>>>

RETURN c o s t i [ 0 ] [ n - 1 ] ;

Codice 2.1 8 Algor itmo iterativo per il calcolo dei costi minimi di moltipl icazione M(i, j).

nella Figura 2.8 con il nostro esempio in cui n = 4 e do = 100, d] = 20, d2 = 1000,

d3 = 2 e d4 = 50. Riempiamo la tabella dei costi per diagonale, dal basso in alto eda sinistra a destra, ut ilizzando la regola in (2.11). La diagonale 0 (ovvero quella percui i = j) contiene chiaramente tutti 0. Consideriamo ora la diagonale 1 (ovvero quellaper cui j = i + 1), che deve contenere i valori M(0 ,1), M ( l , 2 ) e M( 2,3 ): possiamocalcolare tali valori usando elementi della tabella che sono già stati riempiti (in effetti,sono sufficienti quelli che si trovano sulla diagonale precedente).

Possiamo quindi passare alla diagonale 2 (ovvero quella per cui j = i + 2) che devecontenere i valori M(0 ,2 ) e M( l , 3 ) : gli elementi della tabella di cui abbiamo bisognoper calcolare tali valori sono tutti già stati riempiti e si trovano nelle due diagonali già

esaminate. Nella diagonale 3, infine, dobbiamo inserire il valore M(0,3), che può esserecalcolato usando solo elementi delle diagonali precedenti. Come si può notare, abbiamoevitato di effettuare chiamate ricorsive, semplicemente memorizzando i valori secondoun ordine opportuno che dipende dalla regola in (2.11).

L'algoritmo descritto nel Codice 2.18 effettua il calcolo dei valori M(i, j) secondoquanto appena illustrato e, basandosi sulla regola in (2.11), opera in senso inverso rispet-to al Codice 2.17, in qua nto riempie l'array c o s t i dal basso in alto, a partire dai valoricosti[i][i], per 0 ^ i < n, fino a ottenere il valore costi[0][n — 1] da restituire.

Page 81: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 81/373

 

A partire dai valori noti costi[i][i] = 0 sulla diagonale 0 (righe 2-3), l'algoritmodetermina tutti i valori costi[i][j] sulla diagonale 1 (con 0 < i < n — l e j = i + l ) ,poi quell i sulla diagonale 2 (con 0 ^ i < n — 2 e j = i + 2), e così via, fino al valorec os t i [ 0 ] [ n — 1] sulla diagonale n — 1 (righe 5—18): come possiamo notare, gli elementisu una data diagonale sono identificati nel codice dai due indici i e j tali che 0 ^ i <n — d i a g on a l e e j = i + d i a g on a l e . A ogni iterazione, il ciclo alle righe 9-16 calcolail minimo costo analogamente a quanto viene fatto nel Codice 2.17.

Il Codice 2.18 no n solo calcola la tabella c o s t i , ma consente anche di costruireeffettivamente la sequenza di prodotti di costo minimo. A tale scopo utilizza un altroarray bidimensionale i n d i c e delle stesse dimensioni di c o s t i , che viene inizializzatocon indiceli][j] = r (riga 14) per il valore di r che conduce alla scelta ottima nel ciclopiù interno (righe 9-16): infatti, la sequenza di prodotti di costo minimo per Ai,..., Ajha inizio con la moltiplicazione tra due matrici ricorsivamente definite, per tale valoredi r, dalle parentesi in (Ai x • • • x A

r) x e(A

r+i x • • • x Aj).

Una volta costruito l'array i n d i c e dal Codi ce 2.18, possiamo usare tale array perricavare la sequenza di prodotti di costo minimo. Il codice seguente esegue tale opera-zione e deve essere invocato con input i = 0 e j = n — 1 : in esso, il simbolo x denota ilprodotto tra matrici, implementato ad esempio mediante l'algoritmo di Strassen.

ProdottoMinimoC i , j ) : {pre: O ^ i ^ j ^ n — 1)

IF (i == j) {

RETURN AI;

>r = indice[i] [j] ;

RETURN (ProdottoMinimoC i, r )) x (ProdottoMinimoC r+1, j ));

Osserviamo che la complessità dell'algoritmo nel Codice 2.18 è polinomiale, in

quanto esegue tre cicli annidati, ciascuno di n iterazioni al più, per un totale di 0(n 3 )

operazioni (chiaramente, le istruzioni all'interno di tali cicli possono essere eseguite in

tempo costante rispetto al numero n di matrici da moltiplicare). Nel corso della sua

esecuzione, inoltre, l'algoritmo usa le matrici c o s t i e i n d i c e , aventi ciascuna n righe

e n colonne, e una quantità costante di altre locazioni di memoria.

Da ciò possiamo concludere che la complessità temporale dell'algoritmo è 0(n 3 ) ,mentre la sua complessità spaziale è 0(n 2 ) : la diversa implementazione (da ricorsivaa iterativa) della soluzione della relazione di ricorrenza che descrive M(i , j) ha evitatodi ripetere più volte il calcolo di uno stesso valore, permettendo di ottenere un notevolerisparmio nel tempo di esecuzione (da esponenziale a polinomiale). Tale guadagno è par-zialmente compensato dalla necessità di utilizzare una maggiore quantità di memoria per"ricordare" i valori già calcolati, necessità che si traduce nel passaggio dalla complessitàspaziale 0(1) a 0(n 2 ) .

Page 82: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 82/373

 

ALVIE: sequenza di moltiplicazioni di matrici

Osserva, sperimenta e verifica

M a t r i x P r o d u c t O r d e r i n g

2.7 Paradigma della programmazione dinamica

La costruzione della tabella dei costi discussa nel paragrafo precedente è un esempio diapplicazione del paradigma di programmazione dinamica, dove tale termine indica ilmodo dinamico con cui riempiamo la tabella mediante una programmazione basata su

relazioni ricorsive. Il paradigma della programmazione dinamica è basato sullo stessoprincipio utilizzato per il paradigma del divide et impera, in quanto il problema vienedecomposto in sotto-problemi e la soluzione del problema è derivata da quelle di talisotto-problemi. In effetti, entrambi i paradigmi vengono applicati a partire da una defi-nizione ricorsiva che correla la soluzione di un problema a quella di un insieme di sotto-problemi: ricordiamo a tale proposito la definizione ricorsiva alla base dell'algoritmo diordinamento di fusione (Paragrafo 2.5.3) che correla l'ordinamento di una sequenza aquello di due sotto-sequenze disgiunte in cui la sequenza viene decomposta, così comela definizione ricorsiva del costo ottimo del prodotto di una sequenza di matrici (equa-

zione (2.11)) che correla il costo minimo a quello per un insieme di sotto-sequenze dellematrici.

La differenza fondamentale tra i due paradigmi è dovuta al fatto che il paradigmadella programmazione dinamica viene utilizzato per la soluzione di problemi di otti-mizzazione, come il prodotto ottimo di una sequenza di matrici, ossia ogni soluzioneammissibile per un dato problema ha un costo associato e vogliamo trovare quella dicosto ottimo (sia esso il minimo o il massimo a seconda della natura del problema),mentre il paradigma del divide et impera può essere applicato anche a problemi nonnecessariamente di ottimizzazione, come l'ordinamento.

Un'altra differenza è che, mentre la formulazione ricorsiva del divide et impera com-porta che un qualunque sotto-problema venga considerato una sola volta e, quindi, nonsi ponga il problema di evitare calcoli ripetuti di una medesima soluzione, la formula-zione ricorsiva della programmazione dinamica comporta che uno stesso sotto-problemarientri come componente nella definizione di più problemi. Tale differenza può essereschematizzata dalla Figura 2.9, dove vengono mostrati degli schemi di decomposizionericorsiva di un problema in sotto-problemi mediante il paradigma del divide et impera equello della programmazione dinamica.

Page 83: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 83/373

 

Divide et impera Programmazione dinamica

Fi gu ra 2 .9 Decomposizione in sotto-problemi mediante il paradigma del divide et impera e dellaprogrammazione dinamica.

Come possiamo vedere, nel caso del paradigma del divide et impera il problema Pviene decomposto in tre sotto-problemi Pi, P2 e P3, ognuno dei quali a sua volta èdecomposto in sotto-problemi elementari (risolubili in modo immediato senza decom-posizioni ulteriori) in modo tale che uno stesso sotto-problema compare soltanto inuna decomposizione: ad esempio, Pg compare soltanto nella decomposizione di P2, ela sua soluzione dovrà quindi essere calcolata una sola volta, nell'ambito del calcolo dellasoluzione di P2 (cui contribuisce insieme al calcolo della soluzione di P7 e di Ps).

Al contrario, nel caso del paradigma della programmazione dinamica possiamo ve-dere che uno stesso sotto-problema compare in più decomposizioni di problemi diversi,e quindi il calcolo della sua soluzione viene a costituire parte del calcolo delle soluzioni dipiù problemi. Ad esempio, Pg compare ora nella decomposizione sia di Pi che in quelladi P2, con l'effetto che, se i calcoli delle soluzioni di Pi e di P2 vengono effettuati senzatener conto di tale situazione, Pg deve essere risolto due volte, una per contribuire allasoluzione di Pi e l'altra per contribuire alla soluzione di P2.

In generale, la risoluzione mediante programmazione dinamica di un problema ècaratterizzata dalle seguenti due proprietà della decomposizione, la prima delle quali è

condivisa con il divide et impera.

Ottimalità dei sotto-problemi. La soluzione ottima di un problema deriva dalle solu-zioni ottime dei sotto-problemi in cui esso è stato decomposto. Ad esempio, comeabbiamo visto nella regola in (2.11), il problema della ricerca del costo minimodi moltiplicazione per la sequenza di matrici A^,..., Aj viene decomposto nellaricerca dei costi ottimi per tutte le sotto-sequenze A^,..., A r e A r + i , . . . , Aj, con

 j.

Page 84: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 84/373

 

Sovrapposizione dei sot to-problemi. Uno stesso sotto-problema può venire usato nellasoluzione di due (o più) sotto-problemi diversi. Ad esempio, nel caso della regolain (2.11), la soluzione del sotto-problema relativo alla sequenza Ai,..., Aj, conO ^ i ^ j ^ n — 1 viene utilizzata nella risoluzione di tutti i sotto-problemi relativia sequenze A i ' , . . . , Aj' tali che i ' ^ i. e j ^ j' .

La definizione di un algoritmo di programmazione dinamica è quindi basata suquattro aspetti:

1. caratterizzazione della struttura generale di cui sono istanze sia il problema inquestione che tutti i sotto-problemi introdotti in seguito;

2. identificazione dei sotto-problemi elementari e individuazione della relativa mo-dalità di determinazione della soluzione;

3. definizione di una regola ricorsiva per la soluzione in termini di composizione

delle soluzioni di un insieme di sotto-problemi;4. derivazione di un ordinamento di tutt i i sotto-problemi cosi definiti, per il calcolo

efficiente e la memorizzazione delle loro soluzioni in una tabella.

Il numero di passi richiesto dall'algoritmo è quindi limitato superiormente dal prodottotra il numero di sotto-problemi e il costo di ricombinazione delle loro soluzioni ottime.

Nel caso della ricerca della sequenza ottima di prodotti di matrici, tali aspetti corri-spondono, rispettivamente, all'introduzione del sotto-problema per il costo ottimo delprodotto di Ai,..., Aj, all'identificazione del caso i = j come sotto-problema elemen-tare avente costo ottimo pari a 0, alla definizione della relazione ricorsiva (2.11) e al

riempimento della tabella dei costi in ordine crescente di diagonale come indicato nelCodice 2.18. In questo caso, il numero di sotto-problemi è 0 ( n 2 ), corrispondenti atutte le coppie i e j tali che O ^ i ^ j ^ n — 1, e il costo di derivazione della soluzione diun sotto-problema è O(n ) per determinare la scelta ott ima di r nella regola in (2.11). Daciò consegue, come già mostrato esaminando il Codice 2.18, che la risoluzione medianteprogrammazione dinamica del problema in questione richiede tempo 0(n 3 ) .

2.7.1 Sicurezza dei sistemi e sotto-sequenza comune più lunga

I sistemi operativi mantengono traccia dei comandi invocati dagli utenti, memorizzan-doli in opportune sequenze chiamate log (i file nella directory / v a r / l o g dei sistemiUnix/Linux sono un esempio di tali sequenze). I sistemisti usano i log per verificareeventuali intrusioni (intrusion detection) che possano minare la sicurezza e l'integrità delsistema: quest'esigenza è molto diffusa a causa del collegamento dei calcolatori a In-ternet, che può permettere l'accesso remoto da qualunque parte del mondo. Uno deimetodi usati consiste nell'individuare particolari sotto-sequenze che appaiono nelle se-quenze di log e che sono caratteristiche degli attacchi alla sicurezza del sistema. Sia F la

Page 85: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 85/373

 

sequenza dei comandi di cui il log tiene traccia: quando avviene un attacco, i coman-di lanciati durante l'intrusione formano una sequenza S ma, purtroppo, appaiono in Fmescolati ai comandi legalmente invocati sul sistema. Per poter distinguere S all'internodi F, i comandi vengono etichettati in base alla loro tipologia (useremo semplici etichet-

te come A, B, C e così via), per cui S e F sono entrambe rappresentate come sequenzedi etichette: i singoli comandi di S sono probabilmente legali se presi individualmentementre è la loro sequenza a essere dannosa.

Dobbiamo quindi individuare S quando appare come sotto-sequenza di F: in altreparole, indicata con k la lunghezza di S e con n quella di F, dove k ^ n, vogliamo ve-rificare se esistono k posizioni crescenti in F che contengono ordinatamente gli elementidi S (ossia se esistono k interi io, i i , . . . , i^ - i tali che 0 ^ io < ii < • • • < ik-i ^ n — 1e S[j] = F[ij] per 0 ^ j ^ k — 1). Per esempio, S = A,D, C, A, A,B appare come sot to-sequenza di F = B, A, A, B, D, C, D, C, A, A, C, A, C, B, A (dove le lettere sottolineate contras-

segnano una delle possibili occorrenze, in quanto S può essere alternativamente vistacome il risultato della cancellazione di zero o più caratteri da F).

Il problema che in realtà intendiamo risolvere con la programmazione dinamica èquello di individuare la lunghezza delle sotto-sequenze comuni più lunghe per due se-quenze date a e b. Diciamo che x è una sotto-sequenza comune ad a e b se appare comesotto-sequenza in entrambe: x è una sotto-sequenza comune più lunga (LCS o longest 

common subsequence) se non ne esistono altre di lunghezza maggiore che siano comunialle due sequenze a e b (ne possono ovviamente esistere altre di lunghezza pari a quelladi x ma non più lunghe). Indicando con LCS(a, b) la lunghezza delle sotto-sequenze

comuni più lunghe di a e b, notiamo che questa formulazione generale del problemapermette di scoprire se una sequenza di comandi S appare come sotto-sequenza di unlog F: basta infatti verificare che sia k = LCS(S, F) (ponendo quindi a = S e b = F).

Le possibili sotto-sequenze comuni di due sequenze a e b, di lunghezza m e n ri-spettivamente, possono essere in numero esponenziale in m e n ed è quindi inefficientegenerarle tut te per poi scegliere quella di lunghezza massima. Applich iamo quindi ilparadigma della programmazione dinamica a tale problema per risolverlo in tempo poli-nomiale O(mn), seguendo la falsariga delineata dai quattro aspetti del paradigma discussiin precedenza. In prima instanza, definiamo

L(i, j) = LCS(a[0,i — l],b[0, j — 1])

come la lunghezza massima delle sotto-sequenze comuni alle due sequenze rispettiva-mente formate dai primi i elementi di a e dai primi j elementi di b, dove O ^ i ^ m e0 ^ j < n (adottiamo la convenzione che a[0, — 1] sia vuota e che b[0, —1] sia vuota).

In seconda istanza, osserviamo che L(m,n) fornisce la soluzione LCS(a,b) al no-stro problema e definiamo i sotto -prob lemi elementari come L(i, 0) = L(0, j) = 0, inquanto se (almeno) una delle sequenze è vuota, allora l'unica sotto-sequenza comune ènecessariamente la sequenza vuota (quindi di lunghezza pari a 0).

Page 86: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 86/373

 

In terza istanza, forniamo la definizione ricorsiva in termini dei sotto-problemi L(i, j)per i > 0 e j > 0, prend endo in considerazione le sotto-sequenze comu ni di lunghezzamassima per a[0, i — 1] e b[0, j — 1] secondo la seguente regola:

[ 0 s e t ^ O o j ^ O

L(i ,j ) = < L ( i - l , j - l ) + l se i , j > 0 e a[i — 1] = b[j — 1] (2.13){ max { L(i, j — 1 ), L(i — 1, j) } se i, j > 0 e a[i - 1] ± b[j - 1]

La prima riga della regola in (2.13) riporta i valori per i sot to-problemi elementari (i ^ 0o ) ^ 0). Le successive due righe in (2.13) descr ivono come ricombinare le soluzioni deisotto-problemi (i, j > 0):

• a[i — 1] = b[j — 1]: se k = L(i — 1,j — 1) è la lunghezza massima delle sotto-sequenze comuni ad a[0,i — 2] e b[0, j — 2], allora k + 1 lo è per a[0,i — 1] eb[0,j — 1], in quanto il loro ultimo elemento è uguale (altrimenti ne esisterebbeuna di lunghezza maggiore di k in a[0, i — 2] e b[0, j — 2], che è assurdo);

• a[i — 1] ^ b[j — 1]: se k = L(i , j — 1 ) è la lunghezza massima delle sotto-sequenze

comuni ad a[0,i — 1] e b[0,j — 2] e k' = L(i — 1, j) è la lunghezza massima delle

sotto-sequenze comuni ad a[0,i — 2] e b[0, ) — 1], allora tali sotto-sequenze appa-

riranno inalterate come sotto-sequenze comuni in a[0,i — 1] e b[0, j — 1], poiché

non possono essere estese ulteriormente: pertanto, L(i, j) è pari a max{k, k'}.

In quarta istanza, utilizziamo una tabella lunghezza di taglia (m+ 1 ) x ( n + 1 ), tale

che lu ngh ezz a [ i , j] = L(i, j). Do po aver inizializzato la prima colonna e la prima rigacon i valori pari a 0, riempiamo tale tabella in ordine di riga secondo quanto riportato nelCodice 2.19. Essendoci O(mn) sotto-problemi, ciascuno risolvibile in tempo costante(righe 8-14) in base alla regola in (2.13), otteniamo una complessità di O(mn) tempo espazio.

Come nel caso del problema della sequenza di moltiplicazioni di matrici, possiamoindividuare una delle sotto-sequenze comuni più lunghe, utilizzando un ulteriore arrayindice di taglia (m + 1) x (n + 1) nel Codice 2.19, per memorizzare quale delle sueistruzioni determina il valore dell'elemento corrente di l u n g h e z z a . Realizziamo ciò

assegnando a i n d i c e [ i ] [ j ] una delle seguenti tre coppie di indici: (i — 1, j — 1) nellariga 9, (i, j — 1) nella riga 11 e (i — l,j) nella riga 13. Il seguente algoritmo ricorsivo(che deve essere invocato con input i = m e j = n) usa i n d i c e per ricavare una sotto-sequenza comune più lunga (righe 3 e 5):

StampaLCS ( i, j ) : (pre:

IF ((i > 0) && (j > 0)) {

<i\ j'> = indiceli] [j];

StampaLCS( i', j' );

IF ((i' == i-1) && (j' == j-1))) PRINT a[i-l];

>

Page 87: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 87/373

 

LCS ( a, b ) : (pre:a e b sono di lunghezza min)FOR  (i = 0; i <= M; i = i+1)

lunghezza[i][0] =0;

FOR  (j = 0; j <= n; j = j+1)

lunghezza[0][j] =0;

FOR (i = 1; i <= m; i = i+1)FOR  (j = 1; j <= n; j = j+1) {

IF (a[i-l] == b[j-l]) {

lunghezza[i][j] = lunghezza[i-1][j-1] + 1;

> ELSE IF (lunghezza[i][j-1] > lunghezza[i-1][j]) {

lunghezza[i][j] = lunghezza[i][j-1];

> ELSE {

lunghezza[i][j] = lunghezza[i-1][j];

>>

RETURN lunghezza[M,N];

Codice 2.19 Algoritmo per il calcolo della lunghezza della sotto-sequenza comune più lunga.

Notiamo che possiamo ridurre a O(n) spazio la complessità dell'algoritmo descrit-to nel Codice 2.19, in quanto utilizza soltanto due righe consecutive di lunghezzaalla volta (in alternativa, possiamo modificare il codice in modo che riempia la ta-

bella l u n g h e z z a per colonne e ne utilizzi solamente due colonne alla volta). Tutta-via, tale riduzione in spazio non permette di eseguire l'algoritmo StampaLCS perchénon abbiamo più a disposizione l'array i n d i c e . Esiste un modo più sofisticato, im-plementato in alcuni sistemi operativi, che richiede spazio lineare 0(n + m) e tempo0((r + m + n) log(n + m)), dove r è il numero di possibili coppie di elementi uguali nel-le sequenze a e b. Pur essendo r = O(mn) al caso pessimo, in molte situazioni pratichevale r = 0 ( m + n) : in tal caso, trovare una sotto-sequenza comune più lunga in spaziolineare richiede 0((n + m) logn) tempo.

ALVIE: sotto-sequenza comune più lunga

Osserva, sperimenta e verifica

LongestCommonSubsequence

Page 88: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 88/373

 

2.7.2 Sistemi di backup e partizione di un insieme di interi

Consideriamo la situazione in cui abbiamo due supporti esterni, ciascuno avente capacità

di s byte, sui quali vogliamo memorizzare n file (di backup) che occupano 2s byte in

totale, seguendo la regola che ciascun file può andare in uno qualunque dei ducsupporti

purché il file non venga spezzato in due o più parti. Il paradigma della programmazionedinamica può aiutarci in tale situazione, permettendo di verificare se è possibile dividere i

file in due gruppi in modo che ciascun gruppo occupi esattamente s byte. Tale problema

viene detto della partizione (partition) ed è def ini to nel mo do seguente.

Suppon iamo di avere un insieme di interi positivi A = {do, Q i , . . . , a n - i l aventi

somma totale (pari) X^Jq 1 a  ì = 2s: vogliamo determinare se esiste un suo sottoinsieme

A' = (qì0, Qì, , . . . , a t k } C A tale che X!f=o Qí¡ = s> v a^ e a dire t a^e c h e somma degli

interi in A' è pari alla metà della somma di tutti gli interi in A. È chiaro che vogliamo

evitare di generare esplicitamente tutti i sottoinsiemi perché un tale metodo richiede

tempo esponenziale (come discusso nel Paragrafo 1.2.2).Definiamo il sotto-problema generale consistente nel determinare il valore booleano

T(i, j), per 0 ^ i ^ n e 0 ^ j ^ s, che poni amo pari a t r u e se e solo se esiste un

sottoinsieme di Qo, a i , . . . , a t_ i avente som ma pari a j: chiaramente , T(n , s) fornisce la

soluzione del problema originario. Se consideriamo ad esempio l'istanza rappresentata

dall'insieme A = {9,7, 5 , 4 , 1 , 2 , 3 , 8 , 4 , 3 } abbiamo che T(i, j) sarà definito per 0 < i <

10 e 0 ^ j ^ 23, e che T( 3, 12 ) = t r u e in quant o l'insieme A = {9,7 , 5} contiene il

sottoinsieme {7, 5} la cui somma è pari a 12.

I valori T( 0, j) , per 0 ^ j ^ s, costi tuiscono l'insieme degli s + 1 sot to-problemi

elementari, la cui soluzione deriva immedia tame nte osservando che T(0, 0) = t r u e eT(0 ,j) = f a l s e per j > 0 poiché il sottoinsieme in questione è l'insieme vuo to con

somma pari a 0. Nel caso generale, T(i, j) soddisfa la seguente regola ricorsiva:

t r u e se i = 0 e ) = 0

T(i j) = ^ t r U e s ei > 0 e T ( i - l , j ) = t r u e

' t r u e se i > 0, j ^ at _i e T(i - l , j - at _i ) = t r u efa lse a l t r iment i

Per quanto riguarda la definizione ricorsiva dei sotto-problemi T(i, j) con 1 < i ^ n nellaregola in (2.14), osserviamo che se T(i, j) = t r u e , allora ci sono due soli casi possibili:

• il sottoins ieme di {ÜQ, . . . , AT_I} la cui somma è pari a J non comprende ÜÍ_I : tale

insieme è dun que sottoinsieme anche di {a o, .. . , a ^ } e vale T(i — 1, j) = t r u e ;

• il sottoinsieme di {a.o, ... , at _ i} la cui somma è pari a j include at_ i : esiste quindiin {ao,..., ai_2) un sottoinsieme di somma pari a j — ai_ i per cui, se j —ai _i ^ 0,vale T(i — 1, j — ) = t r u e .

Page 89: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 89/373

 

Pa r t iz io ne ( a ) : (pre: a. è un array di n interi positivi la cui somma è 2s)FOR  ( i = 0; i <= n ; i = i+1)

FOR  ( j = 0; j <= s ; j = j+ 1) {p a r t i t i ] [ j ] = FALSE;

>

p a r t i t o ] [ 0 ] = TRUE;FOR  ( i = 1; i <= n ; i = i+1)

FOR  ( j = 0; j <= s ; j = j+ 1) {IF ( p a r t i t i - 1 ] t j ] ) {

p a r t i t i ] t j ] = TRUE;

>IF (j >= a[i-L] &fe partiti~L] t j -a[i-L] ] ) {

p a r t i t i ] t j ] = TRUE;

>>

RETURN p a r t i [ n ] t s ] ;

Codice 2.20 Algoritmo iterativo per il problema della partizione.

Co mbin ando i due casi sopra descritti (i soli possibili), abbi amo che T(i, j) = t r u e se esolo se T(i— 1, j) = t r u e oppure (j—QÌ_I ^ 0 eT(i— 1, j — ) = true), ottenendo cosìil corri spondente Codice 2.20, in cui la tabella booleana p a r t i di taglia (n + 1) x (s + 1 )viene riempita per righe in modo da soddisfare la relazione parti[i][j] = T(i, j) secondola regola in (2.14).

Osserviamo che abbiamo introdotto (n + 1) x (s + 1) sotto-problemi e che la so-luzione di un sotto-problema richiede tempo costante per la regola in (2.14): pertanto,abbiamo che l'algoritmo iterativo descritto nel Codice 2.20 richiede tempo O(ns), ilquale dipende dal valore di s piuttosto che dalla sua dimensione, come discuteremo nelParagrafo 2.7.4.

ALVIE: problema della partizione

Osserva, sperimenta e verifica

P a r t i t i o n

Come abbiamo discusso in precedenza, usare un algoritmo puramente ricorsivo per

un problema di programmazione dinamica è inefficiente. In effetti, esiste un modo di

evitare tale perdita di efficienza mantenendo la struttura ricorsiva dell'algoritmo, pren-

Page 90: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 90/373

 

dendo nota (tale accorgimento viene denominato memoization in inglese) delle soluzionigià calcolate e prevedendo, in corrispondenza a ogni chiamata ricorsiva, di verificare an-zitutto se la soluzione del sotto-problema in questione è già stata calcolata e annotata,così da poterla restituire immediatamente. Ciò comporta l'uso di un array di dimensio-ne opportuna per mantenere le soluzioni dei vari sotto-problemi considerati, prevedendoinoltre che gli elementi di tale array possano assumere un valore "indefinito", che indi-chiamo con il simbolo 0, per segnalare il caso in cui il corrispondente sotto-problemanon sia ancora stato risolto.

A titolo di esempio, possiamo sviluppare un algoritmo ricorsivo per il problema dellapartizione che utilizza il suddetto meccanismo (ricordando comunque che è consigliabileusare la programmazione dinamica). Tale algoritmo inizializza la pr ima riga dell'arrayp a r t i come nel Codice 2.20 e riempie le righe successive con il valore indefinito 0.Successivamente, l'algoritmo invoca la funzione ricorsiva che segue la regola in (2.14).

PartizioneMemoization( ):

FOR  (j = 0; j <= s; j = j+1)

partito][j] = FALSE;

parti[0][0] = TRUE;

FOR  (i = 1; i <= n; i = i+1)

FOR  (j = 0; j <= s; j = j+1) {

partiti][j] = 0;

>RETURN PartizioneRicNota( n, s );

PartizioneRicNota( i, j ) : (pre: O^i^n,IF (partiti][j] == 0) {

partiti]tj] = PaxtizioneRicNota( i-1, j );

IF (¡partiti][j] && (j >= a[i-l])) {

partiti]tj] = PartizioneRicNota( i-1, j-ati-1] );

}

>RETURN partiti]tj];

2.7.3 Problema della bisacciaMostriamo ora una generalizzazione del problema della partizione a un famoso pro-blema di ottimizzazione: tale problema, deno min ato p roblema della bisaccia (zaino oknapsack), può essere definito, in modo pittoresco, come segue.

Supponiamo che un ladro riesca a introdursi, nottetempo, in un museo dove sonoesposti una quantità di oggetti preziosi, più o meno di valore e più o meno pesanti.Tenuto conto che il ladro può trasportare una quantità massima di peso, il suo problemaè selezionare un insieme di oggetti da trafugare il cui peso complessivo non superi la

Page 91: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 91/373

 

possanza che il ladro è in grado di sopportare, massimizzando al tempo stesso il valorecomplessivo degli oggetti rubati.

Abbiamo quindi un insieme di elementi A = {ao, ai,..., a n _i) su cui sono definitele due funzioni v a l o r e e pe so , le quali associano il valore e il peso a ogni elementodi A (supponiamo che sia il valore che il peso siano numer i interi positivi). Conosciamoinoltre un intero positivo possanza, che indica il massimo peso totale che il ladro puòportare. Vogliamo determinare un sottoinsieme A' = {at 0, a ^ , . . . , aik_,} C A taleche il peso totale dei suoi elementi rientri nella possanza, ovvero X.jC=o P e s o ( a i j ) ^possanza, e tale che il valore degli oggetti selezionati, ovvero ^j^o' valorefa^ ), sia ilmassimo possibile.

Per applicare il paradigma della programmazione dinamica possiamo definire, co-me sotto-problema generico, la ricerca della soluzione ottima supponendo una minorepossanza e un più ristretto insieme di elementi. In altri termini, per 0 ^ i ^ n e

0 ^ j ^ p o s s a n z a , denotiamo con Pi j il sotto-problema relativo al caso in cui pos-siamo utilizzare i soli elementi A = {ao, a i , . . . , a ^ i } con il vincolo di non superare unpeso pari a j, e indichiamo con V(i, j) il massimo valore ottenibile in tale situazione.

Per caratterizzare i sot to-problemi elementari, osserviamo che V(i, 0) = 0, per 0 ^1 ^ n , in quanto il peso trasportabile è nullo e, quindi , il valore complessivo deve esserepari a 0, mentre V(0, j) = 0, per 0 ^ j ^ possanza, poiché non ci sono elementi dispo-nibili e il valore complessivo è necessariamente 0. Possiamo definire la decomposizionericorsiva osservando che la soluzione di valore massimo V(i, j) per il sotto-problema Pi,jpuò essere ottenuta a partire da tutte le soluzioni ottime che utilizzano i soli elementi

ao, a i , . . . , ai 2. in due soli possibili modi:

• il pr imo modo è che la soluzione ott ima di Pi,j non includa a i_i e che, in talcaso, abbia lo stesso valore della soluzione ottima del sotto-problema Pi-i,j, ossiaV(i,j) = V(i— l,j);

• il secondo modo è che la soluzione ottima di P t j includa a^-i e che, pertanto,il suo valore sia dato dalla somma di valore(ai_i ) con il valore della soluzioneottima di P i _ i , m , dove m = j - p e s o (a i_ i ) : in tal caso, quindi , V(i, j) = V(i -l , j - p e s o ( a i _ i ) ) + va lo r e ( a i _ i ) (se j ^ pe so (a i_ i ) ) .

La soluzione ot tima di P y sarà quella corri spondente alla migliore delle due (sole) pos-sibilità, ovvero V(i,j) = max{V(i - l, j) ,V (i - l , j - pe so (a i_ i) ) + va l o r e f a ^ i ) } .Per tornare al nostro esempio figurato, supponiamo che il ladro abbia a disposizione glielementi ao, a j , . . . , at _i e la possibilità di trasportare un peso massimo j: se egli decidedi non prendere l'e lemento a^ 1, allora il meglio che può ottenere è la scelta ott ima traao, a j , . . . , at_2 , sempre con peso massimo j; se invece decide di prendere a i_ i , allora ilmeglio lo ottiene trovando la scelta migliore tra ao, ai,..., a t ^2 tenendo presente che,

Page 92: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 92/373

 

Bisaccia( peso, valore, possanza ):

(pre: peso e valore sono array di n interi positivi, possanza è un valore intero positivo)FOR  (i = 0; i <= n; i = i+1) {

FOR (j =0; j <= possanza; j = j+1) {VCi] [j] = 0;

>>FOR  (i = 1; i <= n; i = i+1) {

FOR (j = 1; j <= possanza; j = j+1) {V[i][j] = V[i-1][j] ;

IF (j >= peso[i-1])) {

m = V[i-1][j-peso[i-l]] + valore[i-l];

IF (M > V[i] [j]) VCi] [j] = M;

>>

>

RETURN V[n][possanza];

Codice 2.21 Algoritmo iterativo per il problema della partizione.

dato che dovrà trasportare anche a t - i in aggiunta agli elementi scelti, si deve limitare aun peso massimo pari a j decrementato del peso di cii_i. La scelta migliore sarà quella

che massimizza il valore.L'algoritmo iterativo descritto nel Codice 2.21 realizza tale strategia facendo uso di

un array bidimensionale V di taglia (n + 1 ) x (possanza + 1 ), in cui l'elemento V[i][j]contiene il costo V(i, j) della soluzione ott ima di P y . Dat o che il numero di sotto-problemi è 0(n x possanza) e che derivare il costo di soluzione di un sotto-problemacomporta il calcolo del massimo tra i costi di due altri sotto-problemi in tempo costante,ne consegue che il problema della bisaccia può essere risolto mediante il paradigma dellaprogrammazione dinamica in tempo 0(n x possanza).

ALVIE: problema della bisaccia

Osserva, sperimenta e verifica

Knapsack

Page 93: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 93/373

 

2.1 A Pseudo-polinomialità e programmazione dinamica

Concludiamo il paragrafo sulla programmazione dinamica commentando la complessitàcomputazionale in tempo derivata con tale paradigma. Ricordiamo che la sequenza otti-ma per la moltiplicazione di n matrici richiede 0(n 3 ) tempo e la determinazione di una

sotto-sequenza comune più lunga tra due sequenze di lunghezza n e m richiede O(nm)tempo. Gli algoritmi risultanti sono polinomiali nella dimensione dei dati in ingressoavendo, rispettivamente, un'istanza di n matrici e di n + m elementi nelle due sequenze.

Per il problema della partizione di n interi di somma totale 2s, abbiamo un costopari a O(ns), il quale è polinomiale in n e s ma non lo è necessariamente nella dimen-sione dei dati di ingresso, secondo quanto discusso nel Capitolo 1. Pur avendo n interida partizionare, ciascuno di essi richiede k = O(logs) bit di rappresentazione. Quindila dimensione dei dati è nk mentre il costo dell'algoritmo è O(ns) = 0(n2 k ): tale costonon è polinomiale rispetto alla dimensione dei dati e, per questo motivo, l'algoritmo vie-

ne detto pseudo-polinomiale, in quanto il suo costo è polinomiale solo se si usano interipiccoli rispetto a n (per esempio, quando s = 0(n c ) per una costante c > 0). Anche l'al-goritmo discusso per il problema della bisaccia è pseudo-polinomiale in quanto richiede0 ( n x pos sa nza ) = 0( n2 k ) tempo mentre la dimensione dei dati in ingresso richiedeO(nk) bit dove k = O(logpossanza). Anche in questo caso, il costo dell'algoritmo nonè polinomiale, ma lo diviene nel momento in cui il valore della possanza è polinomialerispetto al numero di oggetti.

Da ciò consegue che, mentre i primi due algoritmi sono polinomiali a tutti gli effetti,gli ultimi due algoritmi sono solo apparentemente polinomiali perché hanno complessità

polinomiale rispetto al numero n di elementi, ma esponenziale rispetto alla lunghezza kdella rappresentazione dei valori numerici contenut i nell'istanza del problema. In altritermini, i due algoritmi dati per i problemi della partizione e della bisaccia avrebberocomplessità polinomiale se le istanze considerate dei due problemi avessero il vincoloaggiuntivo di non contenere valori numerici "eccessivamente grandi" rispetto a n. Laragione di tale anomalia è che, per valori numerici sufficientemente grandi, il problemadella partizione e quello della bisaccia sono NP-completi (da notare, però, che non èvero che ogni problema NP-completo ammetta un algoritmo pseudo-polinomiale, comevedremo nel seguito del libro).

RIEPILOGO

 In questo capitolo abbiamo discusso la gestione di una sequenza di elementi, distinguendo traaccesso diretto e accesso sequenziale. Abbiamo trattato approfonditamente la realizzazionedell'accesso diretto mediante array, abbiamo studiato il problema della ricerca e dell'ordi-namento mediante confronti e abbiamo mostrato come risolvere ricorsivamente i problemiutilizzando il paradigma del divide et impera e la tecnica della programmazione dinamica,introducendo l'analisi degli algoritmi ricorsivi mediante le equazioni di ricorrenza.

Page 94: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 94/373

 

ESERCIZI

1. Motivate perché lo schema dinamico illustrato nel Paragrafo 2.1.3 ha un co-sto maggiore se dimezziamo l'array quando il numero di elementi soddisfa lacondizione n = d/2, dove d è il numero di elementi allocati in memoria.

2. Dimostrate che la complessità in tempo dell'algoritmo selection sort è @(n2), perogni sequenza di n elementi. Fornite una sequenza di n elementi per cui l'algorit-mo insertion sort esegua 0(n 2 ) operazioni e una per cui l'algoritmo esegua O(n)operazioni.

3. Un algoritmo di ord inamento è stabile se, in presenza di elementi uguali, ne man-tiene la posizione relativa nella sequenza d'uscita (quindi gli elementi uguali ap-paiono contigui ma non sono permutati tra di loro): valutate quali algoritmi diordinamento discussi nel capitolo sono stabili.

4. Mostrate che, se un algoritmo per la risoluzione del problema del segmento disomma massima non legge un elemento a[r], è sempre possibile assegnare ad a[r]un valore tale da invalidare la soluzione calcolata dall'algoritmo.

5. Hannibal vi ha catturati insieme ad altre persone, per un totale di k persone. Dopoaver pensato a un numero n segreto, vuole che indoviniate il valore di n sapendosoltanto che è n > 0: le uniche domande ammesse sono i confronti con un valorex di vostra scelta, come x = n?, x < n?, x < n?, e così via. Una sola persona allavolta tra di voi può fare una domanda a Hannibal e, se per caso essa sceglie unintero x > n, esce dal gioco perché viene condotta a cena da Hannibal . Dovetedecidere quali confronti porre all'attenzione di Hannibal, in modo da effettuaresoltanto 0(n 1 / k ) domande in totale.

6. Il gioco di Rényi-Ulam consiste nell'indovinate il numero segreto n pensato daHannibal attraverso domande che coinvolgono confronti con un valore x di vostrascelta, come x = n?, x ^ n?, x < n?, e così via. L'unica anomalia è che Hannibalpotrebbe mentire sulla risposta e, se lo fa, può farlo una volta soltanto. Mostra-te come indovinare comunque il valore di n effet tuando soltanto logn + 0 ( 1 )domande.

7. Dato un intero positivo k e un array a di n interi positivi in ordine crescente,descrivete un algoritmo per trovare una coppia di posizioni distinte i e j, tali che0 ^ i < j ^ n - 1 e a[i] + a[j] = k. Il costo dell'algoritmo deve essere O( n) alcaso pessimo.

Page 95: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 95/373

 

8. Descrivete un algoritmo, basato sul paradigma del divide et impera, per trovare idue elementi più piccoli di un insieme di n elementi, fornendone un'analisi dellacomplessità temporale che faccia uso del teorema fondamentale delle ricorrenze.

9. Descrivete un algoritmo, basato sul paradigma del divide et impera, per risolvere

il problema del segmento di somma massima in tempo O(nlogn).

10. Descrivete un algoritmo, basato sul paradigma del divide et impera, per calcolarea n con O(logn) operazioni aritmetiche, dove a e n sono dati in ingresso, mo-tivandone la complessità temporale (notate che a n = ( a n / 2 ) 2 se n è pari e chea n = ( a n / 2 ) 2 x a se n è dispari).

11. Per le seguenti equazioni di ricorrenza e una costante c' > 0, mostrate che

• T(n) = 2T( n/2) 4- c' ha soluzione T(n) = O(n);

• T(n) = 2T(n /2 ) + c ' n l o g n ha soluzione T(n) = O( nlog 2 n);

• T(n) = Tfv 'n ) + c' ha soluzione T(n) = O(l ogl ogn ).

12. Date due matrici di dimensione n x n, dove n è una potenza di due, scomponeteciascuna di esse come una matrice 2 x 2 in cui ciascun elemento è una matrice didimensione n / 2 x n / 2 : indicando con a, b, c, d, e, f, g e h. tali matrici, mostrate laseguente uguaglianza, dove le operazioni di somma e prodotto sono quelle definitesu matrici:

ae + bg Qf  + bh.

ce -I- dg cf + dh.

13. Mostrate un controesempio per il quale la scelta di r che massimizza il valoredT + i nell'equazione (2.11) non conduce alla sequenza ottima di moltiplicazionitra matrici.

14. Progettate gli algoritmi per stampare uno dei due insiemi ottenut i nel problemadella partizione e il contenuto ottimo della bisaccia degli elementi, analogamentea quanto visto per il prodotto di costo minimo per una sequenza di matrici e per

le sotto-sequenze comuni più lunghe.

15. Formulate il problema della partizione come un'istanza del problema della bisac-cia. Progettate un algoritmo ricorsivo con presa di nota per il problema dellabisaccia.

"a b'X

e f

c d .9 h

Page 96: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 96/373

 

Capitolo 3

Sequenze: liste

SOMMARIO In questo capitolo analizzeremo il secondo modo di realizzare una sequenza lineare facendoriferimento alla tipologia di accesso sequenziale: le sequenze così ottenute sono anche dette

liste. In particolare, mostreremo i vantaggi e gli svantaggi di una tale realizzazione rispettoa quella ad  accesso casuale descritta nel capitolo precedente. Successivamente, mostreremoun'applicazione di array e di liste alla risoluzione efficiente di un importante problema,ovvero il problema dei matrimoni stabili. Mostreremo inoltre un ulteriore uso della casualitàdescrivendo la gestione efficiente di un particolare tipo di liste randomizzate, ovvero le listea salti. Infine, presenteremo come, sotto determinate condizioni, le prestazioni di una lista

 possono essere migliorate facendo in modo che si adatti alla sequenza di operazioni su di essa

eseguita: a tale scopo, faremo per la prima volta un uso esplicito del concetto di complessitàammortizzata.

DIFFICOLTÀ

1 CFU.

3.1 Liste

Abbiamo visto nel Capitolo 2 come l'organizzazione sequenziale dei dati può, in genera-le, essere realizzata in due diverse modalità, quella ad accesso diretto e quella ad accesso

sequenziale. Nel primo caso, il tipo di dati utilizzato è l'array, di cui pregi e difetti sonogià stati analizzati. In questo capitolo, invece, ci concentriamo sul tipo di dati lista, cherealizza l'organizzazione dei dati con la modalità di accesso sequenziale. Ricordiamo chela caratteristica essenziale di questa realizzazione consiste nel fatto che i dati non risie-dono in locazioni di memoria contigue e, pertanto, ciascun dato deve includere, oltreall'informazione vera e propria, un riferimento al dato successivo.

Page 97: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 97/373

 

Qo ai a 2 û n - 2 Qn-1

Figura 3.1 Esempio di lista di dimensione n.

Dato un elemento x di una lista in posizione i, nel seguito indicheremo con x. datol'informazione associata a tale elemento e con x. succ il riferimento che lo collega all'e-lemento in posizione (i+ l)-esima. A tale proposito, presumiamo l'esistenza di un valorenuli, utilizzato per indicare un riferimento "nullo", vale a dire un riferimento a nessuna

locazione di memoria, e che per l'ultimo elemento x della lista sia x . su c c = n u l i . Nelseguito, inoltre, denoteremo con a il riferimento al primo elemento della lista, ovveroalla locazione di memoria che lo contiene: evidentemente, nel caso in cui la lista sia vuo-ta, tale riferimento avrà valore n u l i . Nella Figura 3.1 viene rappresentata una lista di nelementi: i riferimenti sono rappresentati in modo sintetico, senza evidenziare le relativelocazioni di memoria, e il valore n u l i è indicato con il simbolo 4>.

Le istruzioni seguenti mostrano come accedere all'elemento in posizione i di una listaa in tempo O(i), dove nella variabile p viene memorizzato, al termine del ciclo while,il rifer imento a tale elemento oppure n u l i se tale elemento non esiste:

p = a ;

  j = 0 ;

W H I L E ( ( p ! = n u l i ) && ( j < i ) ) {

p = p . s u c c ;

  j = j+i;>

3.1.1 Ricerca, inserimento e cancellazione

Dato che una lista rappresenta una diversa realizzazione, rispetto a un array, di una se-quenza lineare di elementi, possiamo definire su di essa le operazioni di ricerca, inseri-mento e'cancellazione già considerate per gli array.

Per quanto riguarda la ricerca di una chiave k, il relativo algoritmo (Codice 3.1), presenta la stessa struttura di quello considerato per gli array (Paragrafo 2.4) con la dif-

ferenza che l'accesso all'elemento successivo avviene utilizzando il riferimento p.succdell'elemento p corrente, invece che incrementando un indice di scorrimento. Inoltre, laterminazione della scansione della sequenza viene determinata, oltre che dall'aver trova-

Page 98: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 98/373

 

RicercaSequenzialeC a, k ):

p = a;

W HI LE ( C p ! = n u l i ) kk ( p . d a t o ! = k ) )

p = p . s u c c ;

R E T U R N P ;

Codice 3.1 Ricerca sequenziale di una chiave k in una lista a.

to la chiave desiderata (p.dato uguale a k), dalla verifica del raggiungimento dell'ultimoelemento della lista, avente riferimento n u l i al successore (p .s uc c uguale a n u l i ) .

A differenza degli array (Paragrafo 2.1.3), le liste si prestano molto bene a gestiresequenze lineari dinamiche, in cui il numero degli elementi presenti può variare nel

tempo a causa di operazioni di inserimento e di cancellazione. In effetti, l'inserimentodi un nuovo elemento all'interno di una lista può consistere semplicemente nel porlo incima alla lista stessa, eseguendo le seguenti istruzioni, in cui ipotizziamo che x indichi ilriferimento all'elemento da inserire (Figura 3.2):

x . s u c c = a ;

a = x ;

L'inserimento dopo l'elemento indicato da un riferimen to p ^ n u l i è una semplicevariazione dell'operazione precedente, in cui p . s u c c sostituisce la variabile a, comemostrato nelle seguenti istruzioni:

x . s u c c = p . s u c c ;

p . s u c c = x ;

Leggermente più complicata è la cancellazione di un elemento, operazione che dovràrendere l'elemento e la lista mutuamente non raggiungibili attraverso i relativi riferimen-ti. Avendo a disposizione il riferimento x all'elemento da cancellare, un caso particolareè rappresentato dalla situazione in cui x coincida con a, vale a dire in cui l'elemento da

cancellare sia il primo della lista. In tal caso la cancellazione viene effettuata modificandoil riferimento iniziale alla lista in modo da andare a "puntare" al secondo elemento, comemostrato nelle istruzioni seguenti:

a = x . s u c c ;

x . s u c c = n u l i ;

Per la cancellazione di un elemento diverso dal primo è necessario non solo avere adisposizione il suo riferimento x, ma anche un riferimento p all'elemento che lo precede.

Page 99: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 99/373

 

lx è inserito in cima alla lista

4>

Figura 3 .2 Inserimento in testa a una lista.

In questo modo, possiamo cancellare l'elemento desiderato creando un "ponte" tra il suopredecessore e il suo successore (Figura 3.3). Questo ponte può essere realizzato mediantele seguenti istruzioni:

p .succ = x.succ;x . succ = nu l i ;

Ignorando il costo di allocazione e deallocazione e quello per determinare i riferi-menti x e p nella lista a, in quanto esso dipende dall'applicazione scelta, le operazionidi inserimento e cancellazione appena descritte presentano un costo computazionale di0(1) tempo e spazio, indipendente cioè dalla dimensione della lista. Ricordiamo a taleproposito come le medesime operazioni su un array richiedano tempo lineare 0(n).

3.1.2 Liste doppie e liste circolariLa struttura di una lista può essere modificata in modo tale da effettuare più efficien-temente-determinate operazioni. In questo paragrafo int rodurremo le due più diffusevariazioni di questo tipo: le liste doppie e le liste circolari.

In una lista doppia l'elemento x in posizione i ha, oltre al riferimento x.succall'elemento in posizione i + 1, un riferimento x.pred all'elemento in posizione i — 1,con x . p r e d uguale a n u l i se i = 0. Tale estensione consente di spostare in tempo 0( 1)un rifer imento x agli elementi della lista sia in avanti (con l'istruzione x = x . s u c c )

Page 100: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 100/373

 

P x

Ix

Figura 3.3 Cancellazione di un elemento da una lista.

che all'indietro (con l'istruzione x = x . p r e d ) . La Figura 3.4 fornisce un esempio dilista doppia: in questa figura, come in quelle successive, non saranno evidenziati, per

semplicità, i campi relativi ai riferimenti.L'aggiunta del riferimento "all'indietro", sebbene complichi leggermente l'operazionedi inserimento di un nuovo elemento, semplifica in modo sostanziale quella di cancel-lazione, in quanto consente di accedere, a partire dall'elemento da cancellare, ai dueelementi circostanti, il cui contenuto va modificato nell'operazione di cancellazione. Alcontrario, in una lista semplice la cancellazione di un elemento richiede un riferimentoall'elemento precedente. Nello specifico, detto x il riferimento all'elemento da cancellare,possiamo considerare i seguenti quattro casi che determinano l'insieme delle istruzioninecessarie per eseguire la cancellazione.

Caso 1. x fa riferimento al pr imo elemento della lista, cioè x è uguale a a. In questo ca-so, non avendo un predecessore, la cancellazione determina la modifica del riferi-mento pred del successore x. succ; inoltre, è necessario aggiornare il riferimentoiniziale a, come mostrato nelle seguenti istruzioni:

x.succ.pred = nuli;

a = x.succ;

x.succ = nuli;

Page 101: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 101/373

 

Figura 3. 4 Lista doppia.

Caso 2. x fa riferimento all'ultimo elemento della lista, cioè x . s u c c è uguale a n u l i .In questo caso, non avendo un successore, la cancellazione richiede la modifi-ca del riferimento succ del predecessore x.pred, come mostrato nelle seguentiistruzioni:

x.pred.succ = nuli;

x.pred = nuli;

Caso 3. x è l'unico elemento nella lista, cioè x è uguale ad a e x . su c c è uguale a n u l i .In questo caso, l'effetto della cancellazione è quello di rendere la lista vuota, equindi di assegnare al riferimento iniziale il valore n u l i , medi ante la seguenteistruzione:

a = nuli;

Caso 4. x fa riferimento a un elemento "interno" della lista. In questo caso, vanno

aggiornati sia il riferimento succ del predecessore che il riferimento pred del

successore, come mostrato nelle seguenti istruzioni:

x.succ.pred = x.pred;

x.pred.succ = x.succ;

x.succ = nuli;x.pred = nuli ;

Notiamo che tutt i i casi appena discussi garantiscono correttamente che x . s u cc =x . p r e c = n u l i dop o una cancellazione, evitando così di lasciare un riferimento pen-dente {dangling pointer).

In una lista circolare, l'ultimo elemento fa riferimento, come suo successore, al pri-mo, creando così una struttura appunto circolare (Figura 3.5). Tale proprietà permette

Page 102: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 102/373

 

Figura 3.5 Lista circolare.

di rappresentare, mediante la lista, un insieme di elementi da scandire ripetutamente.Inserimenti e cancellazioni in una lista circolare possono essere effettuati come in unalista semplice, facendo attenzione a mantenere l'invariante del riferimento dall'ultimoelemento al primo.

Una classica applicazione di tale tipo di struttura è rappresentata dall'algoritmo round 

robin di assegnazione della CPU in un sistema operativo, algoritmo alternativo a quellivisti nel Paragrafo 2.2. In accordo a tale metodo, la CPU viene assegnata a turno a ogniprogramma, per un tempo massimo pari a un intervallo di tempo predefinito (dettoquanto)-, se al termine del quanto il programma non ha finito la sua esecuzione, dovràattendere il suo prossimo turno.

La realizzazione di un algoritmo round robin fa uso di una lista circolare i cui ele-menti corrispondono ai programmi che necessitano della CPU, lista scandita da un rife-rimento al programma attualmente in esecuzione. L'assegnazione della CPU a un altroprogramma, in corrispondenza allo scadere del quanto di tempo, comporterà quindi l'in-

dividuazione del programma successivo, cui spetta ora la CPU: esso viene identificato dalsuccessivo elemento nella lista circolare.

Dato che l'insieme dei programmi varia nel tempo, è inoltre necessario gestire l'in-serimento e la cancellazione di elementi dalla lista circolare. Mentre all'attivazione di unnuovo programma, un semplice inserimento in testa alla lista sarà sufficiente, per quantoriguarda la terminazione di un programma la corrispondente cancellazione del relativoelemento sarà più semplice ed efficiente, per quanto illustrato sopra, se si tratta di unalista circolare doppia, percorribile quindi sia in senso orario che antiorario in quanto ilcampo p r e d del primo elemento è un riferimento all'ul timo (Figura 3.6). In tal mo-

do, ciascuna operazione richiederà tempo costante: la realizzazione dell'algoritmo roundrobin utilizzando array o liste semplici avrebbe delle prestazioni nettamente inferiori.

3.2 Opus libri: problema dei matrimoni stabili

Un'agenzia matrimoniale ha n clienti di sesso maschile, t r io, .. ., m n _i, e altrettanteclienti di sesso femminile, fo, . . . , f n _ i , che desiderano essere abbinati al meglio delleloro preferenze. Ognuno (maschio e femmina) esprime le proprie preferenze specifican-

Page 103: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 103/373

 

Figura 3.6 Lista circolare doppia.

do un ordinamento dei clienti del sesso opposto. In ciascun ordinamento, il cliente cheappare in una posizione, o rango, è da preferire a quelli che si trovano in posizioni succes-sive. L'abbinamento {match) tra le n coppie deve essere perfetto, ovvero deve abbinare

tutti i clienti, e stabile rispetto agli ord inamenti delle preferenze. Un abb inamento èstabile se non esistono due coppie, (a, b) e (c, d), le cui preferenze si incrociano comeindicato nella Figura 3.7: il cliente a preferisce la cliente d a b (in quanto a assegna a drango minore rispetto a quello di b) mentre la cliente d preferisce il cliente a a c. Emolto probabile che a e d si separino da b e d, rispettivamente, per abbinarsi tra di loro.

Il problema appena descritto è detto problema dei matrimoni stabili e ha in realtàapplicazioni molto più serie di quella appena descritta (come, ad esempio, l'assegnazionedi tirocini esterni agli studen ti dif fusa in ambi to scientifico, medico e giuridico). L'ap-proccio che esamina esaustivamente tutti gli n! abbinamenti possibili tra mo,..., m.n_i

e f o , . . . , f n - i richiede un tempo esponenziale nel numero di clienti. Nel nostro caso,vediamo come risolvere il problema in tempo 0(n 2 ) utilizzando array e liste: consideratoche la semplice lettura delle preferenze richiede Q(n 2 ) tempo, ne possiamo dedurre chel'algoritmo è ottimo in questo contesto.

L'idea dell'algoritmo che ora svilupperemo è abbastanza semplice. Innanzitutto, oc-corre rompere la simmetr ia dei due insiemi di clienti. Assegniamo ai clienti di sessomaschile il ruolo di proponenti mentre le clienti di sesso femminile accettano o rifiu-tano le proposte in base al rango determinato dalle loro preferenze (alternativamente, iruoli possono essere invertiti e l'abbinamento perfetto trovato può differire). I clienti si

propongono alle clienti seguendo il proprio ordine di preferenza (che non è detto checorr isponda a quello delle clienti). Fin tan to che vi sono clienti non abbinati, che chia-meremo celibi, permettiamo a uno di essi, m, di proporsi alla cliente f che lui preferiscetra quelle a cui non si è già propos to. Se f non è abbinata, la proposta viene accettata,almeno temporaneamente. Altrimenti, se f preferisce m al cliente m' a cui è attualmenteabbinata, allora accetta la propos ta di m, e m ' ritorna celibe. Se nessuno dei casi pre-cedenti si verifica, e quindi f rifiuta la proposta di m, quest'ultimo passa alla successivacliente. Il procedimento ha termine nel momento in cui tutti i clienti sono abbinati.

Page 104: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 104/373

 

Figura 3.7 Un esempio di

preferenze di a preferenze di d

• •

0 E0 È• •

non stabile.

3.2.1 Strutture di dati utilizzate

Per realizzare l'algoritmo, decidiamo quali strutture di dati adottare: a tale scopo os-serviamo che le preferenze espresse da ciascun cliente sono utilizzate dall'algoritmo inmaniera differente a seconda del ruolo svolto dal cliente stesso.

Per i clienti my , . . . , m-n-i usiamo un array di liste, p r e f e r i t a , ovvero un array icui elementi sono dei riferimenti a delle liste. In particolare, preferita!)] è il riferimen-to a un elemento della lista delle preferenze espresse dal cliente mj , per 0 ^ j ^ n — 1.Man mano che m¡ si propone alle clienti, scorrendo la propria lista, preferitati] vieneaggiornato in modo da far riferimento alla prossima cliente cui eventualmente proporsi.

Per le clienti f o , . . . , f i, adot tiamo una diversa rappresentazione delle relative pre-ferenze: la motivazione risiede nel fatto che una cliente deve accettare o rifiutare unaproposta verificando nelle proprie preferenze il rango del cliente proponente e di quellocui è attualmente abbinata . In generale, dato un cliente mj vogliamo sapere in modoefficiente qual è il suo rango nelle preferenze di fi , per 0 i, j ^ n — 1: necessitiamoquindi di un accesso diretto. In particolare, usiamo un array bidimensionale, r ango ,tale che rango[i] [j) contiene il rango di m.j tra le preferenze di fi . Non è difficile costrui-re rango in tempo lineare: per ogni fi, leggiamo le relative preferenze incrementando,per ogni elemento letto m.j, un contatore inizialmente nullo, e ponendo il valore di talecontatore in rango[i][j].

Per implementare l'algoritmo sopra descritto, necessitiamo di due ulteriori struttureausiliarie. La lista c e l i b e contiene i celibi in attesa di abbinamento: una lista è adattaa tale scopo, in quanto l'insieme dei celibi può variare durante lo svolgimento dellaprocedura, anche se non può aumentare di dimensione in quanto un nuovo celibe vieneaggiunto alla lista solo se un altro cliente, precedentemente celibe, è stato abbinato.

Page 105: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 105/373

 

MatrimoniStabili- (pre: vedi §3.2.1 per celibe, preferita, abbinato, rango) WHILE (celibe != nuli) {

m = celibe.dato;

f = preferita[m];

preferita[m] = preferita[m].succ;

IF (abbinatoti] < 0) {abbinatoti] = m;

celibe = celibe.succ;

} ELSE IF (rangotf][m] < rangotf][abbinato[f]]) {

celibe.dato = abbinatoti];

abbinatoti] = m;

>>

Codice 3. 2 Algoritmo per la risoluzione del problema dei matrimoni stabili.

Inizialmente, la lista c e l i b e contiene tutti i clienti di sesso maschile. Infine, perpoter tenere traccia degli abbinamenti attivi in un certo istante, utilizzeremo l'arraya b b i n a t o , tale che ab b in at o! )] = i se e solo se la coppia (m i, f j) fa attualmen-te parte dell'abbinamen to: suppon iamo che i valori di a b b i n a t o siano inizializza-ti a un valore "nullo", per convenzione pari a —1. Anche in questo caso necessitia-mo di un accesso diretto in quanto, per ogni fj, vogliamo sapere se fj non è ancora

stata abbinata (a bb in at o !) ] < 0), oppu re qual è il cliente mi attualmente abbina-to a f j (a bb in at o !) ] = i). Alla fine della computazione, a b b i n a t o specificherà unabbinamento, che mostreremo essere perfetto e stabile.

3.2.2 Implementazione dell'algoritmo

Avendo introdotto e inizializzato le liste e gli array necessari all'esecuzione dell'algoritmo,siamo ora in grado di illustrare la sua implementazione nel Codice 3.2. A ogni iterazionedel ciclo w h i l e (righe 2- 13) , il cliente m in testa alla lista dei celibi si propone allacliente f da lui preferi ta e a cui non si è ancora proposto (righe 3—4): successivamente,alla riga 5 viene aggiornato il riferimento all'eventuale futura cliente preferita da m. Se f non è abbinata, allora la propos ta viene accet tata e m viene r imosso dalla lista dei celibi(righe 6^8) . Alt riment i viene verificato se m è prefer ito all'attuale cliente abbinato a f ,confrontandone il rango: in tal caso, la proposta viene accettata e avviene lo scambiocon m nella lista dei celibi (righe 9-12). Se nessuno dei casi sopra si presenta, l'iterazionesuccessiva del ciclo esamina la prossima cliente preferita da m.

Non è difficile analizzare il numero di passi eseguiti dall'algoritmo, per una qua-lunque istanza del problema dei mat rimoni stabili. A tale scopo, possiamo anzitu tto

Page 106: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 106/373

 

osservare che, poiché la rimozione e l'inserimento nella lista dei celibi avviene sempre intesta alla lista stessa, ogni iterazione del ciclo while richiede l'esecuzione di un numerocostante di passi. Pertanto, per valutare la complessità temporale dell'algori tmo è suffi-ciente stimare il numero di volte che viene eseguito il corpo del ciclo w h i l e . Not iamoche, a ogni iterazione di tale ciclo, l'istruzione della riga 5 fa scorrere di una posizione inavanti un riferimento all'interno di una delle liste memorizzate in p r e f e r i t a . Questiriferimenti sono spostati esclusivamente in avanti per cui il numero totale di iterazionidel ciclo w h i l e non può superare la lunghezza totale di tali liste, ovvero n 2 . Ne derivache l'algoritmo per il problema dei matrimoni stabili ha un costo pari a 0(n 2 ) tempo.

Rimane ora da dimostrare che l'algoritmo termina avendo calcolato un abbinamentoperfetto e stabile. A tale scopo, osserviamo anzitutto che, per ogni cliente f, dal momen-to in cui riceve la prima proposta, f sarà sempre abbinata a qualcuno (anche se quelqualcuno può cambiare durante l'esecuzione dell'algoritmo). In base a tale osservazio-ne, possiamo concludere che se, a un certo istante, un cliente m è incluso nella lista deicelibi, allora deve esistere una cliente a cui m non si è ancora proposto. Se così nonfosse, infatti, tutte le clienti sarebbero abbinate, contraddicendo il fatto che m sia ancoracelibe. Quindi, al momento in cui il ciclo while termina la sua esecuzione, tutti i clien-ti sono abbinati: in altre parole, abbiamo appena dimostrato che la soluzione calcolatadall'algoritmo è un abbinamento perfetto.

Per dimostrare che tale abbinamento è anche stabile, supponiamo per assurdo chenon lo sia, ovvero che esistano due coppie (a, b) e (c, d) tali che a preferisce d a b mentred preferisce a a c (Figura 3.7). In base al Codice 3.2, l'ultima proposta effe ttuata da a

deve essere quella fatta a b. Due soli casi sono allora possibili.

Caso 1: a non ha mai fatto una proposta a d. Questo implica che d segue b nella listadelle preferenze di a, contraddicendo il fatto che a preferisce d a b.

Caso 2: a ha fatto una proposta a d ma, in quel momento oppure in uno successivo, dha preferito ad a un altro cliente u. Quindi a segue u nelle preferenze di d. Seu = c, ciò contraddice il fat to che d preferisce a a c. Altrimenti ( u ^ c), u devenecessariamente seguire c nelle preferenze di d, in quanto c è nella coppia finaleper d. Per la proprietà transitiva su u, anche a deve seguire c nelle preferenze di

d, contraddicendo il fatto che d preferisce a a c.

In entrambi i casi, abbiamo raggiunto un assurdo: in conclusione, abbiamo che lasoluzione calcolata dall'algoritmo è un abbinamento perfetto e stabile.

Osserviamo, infine, che l'abbinamento perfetto non è necessariamente unico: peresempio, è sufficiente che i clienti abbiano tutti la stessa lista di preferenze e, in tal caso,l'ordine con cui vengono esaminati i clienti determina uno dei differenti abbinamenti.Inoltre, i clienti possono accordarsi per non lasciare alcuna scelta alle clienti in quanto

Page 107: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 107/373

 

basta che ogni cliente specifichi come prima scelta nelle preferenze una cliente diversa

dalla prima scelta degli altri: in effetti, il problema dei matrimoni stabili presenta innu-

merevoli varianti che sono state studiate per garantire ulteriori proprietà oltre a quella di

avere un abbinamento perfetto.

ALVIE: problema dei matrimoni stabili

Osserva, sperimenta e verifica

StableMarriage

3.3 Liste randomizzateUna configurazione sfavorevole dei dati o della sequenza di operazioni che agisce su diessi può rendere inefficiente diversi algoritmi se analizziamo la loro complessità nel ca-so pessimo. In generale, la strategia che consente di individuare le configurazioni chepeggiorano le prestazioni di un algoritmo è chiamata avversariale in quanto suppone cheun avversario malizioso generi tali configurazioni sfavorevoli in modo continuo. In talecontesto, la casualità riveste un ruolo rilevante per la sua caratteristica imprevedibilità:vogliamo sfruttare quest'ultima a nostro vantaggio, impedendo a un tale avversario di

prevedere le configurazioni sfavorevoli (in senso algoritmico). Abbiamo già discusso nelParagrafo 2.5.4 come la casualità possa essere impiegata in tal senso, applicandola all'al-goritmo di ordinamento per distribuzione (quicksort) nella scelta del pivot. Ricordiamoche un algoritmo random o casuale (di cui l'algoritmo quicksort  costituisce un esempio)fa uso di sequenze di scelte casuali.

Nel seguito descriviamo un algoritmo random per il problema dell'inserimento edella ricerca di una chiave k in una lista e dimostriamo che la strategia da esso adottataè vincente, sotto opportune condizioni. In particolare, usando una lista randomizzata din elementi ordinati, i tempi medi o attesi delle operazioni di ricerca e inserimento sono

ridotti a O(logn): anche se, al caso pessimo, tali operazioni possono richiedere tempoO(n), è altamente improbabile che ciò accada.

Descriviamo una particolare realizzazione di liste randomizzate, chiamate liste a salti(skip list), la cui idea base (non random) può essere riassunta nel modo seguente (secondoquan to illustrato nella Figura 3.8). Part iamo da una lista ordinata di n + 2 elementi,Lo = eo, e i , . . . , e n + i , la quale costituisce il livello 0 della lista a salti: poni amo cheil primo e l'ultimo elemento della lista siano i due valori speciali, —oo e +oo, per cuivale sempre —oo < et < +oo, per 1 ^ i ^ n . Per ogni elemento et della lista LQ

Page 108: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 108/373

 

L [ 2 ] [^OO

L [ L ] ' '—oo • 10

18

18 -41

+ 0O

+00

' rL[0] — 00 5 10 16 18 30 41 80 +oo

Fi gu ra 3. 8 Un esempio di lista a salti.

(1 ^ i < n) creiamo r^ copie di e se r^ > 0, dove 2Ti è la massima potenza di 2 chedivide i (nel nostro esempio, per i = 1, 2, 3, 4, 5, 6, 7, abbiamo ti = 0 , 1 , 0 , 2 , 0 , 1 , 0 ).

Ciascuna copia ha livello crescente t — 1,2,..., r^ e punta alla copia di livello inferiorel — 1: supponiamo inoltre che —oo e +oo abbiano sempre una copia per ogni livellocreato. Chiaramente, il massimo livello o altezza h. della lista a salti è dato dal massimovalore di r^ incrementato di 1 e, qui ndi , h = O( lo gn ).

Passando a una visione orizzontale, tutte le copie dello stesso livello l (0 ^ t ^ H)sono collegate e fo rman o una sottolis ta L , tale che L C C • • • C LQ:1 come possia-mo vedere nell'esempio mostrato nella Figura 3.8, le liste dei livelli superiori "saltellano"(skip in inglese) su quelle dei livelli inferiori. Osserviamo che, se la lista di partenza, LQ,contiene n + 2 elementi ordinati, allora Lj ne contiene al più 2 + n/2, L 2 ne contiene

al più 2 + n / 4 e, in generale, L contiene al più 2 + n/ 2^ e lementi ordinat i. Pertanto ,il numero totale di copie presenti nella lista a salti è limitato superiormente dal seguentevalore:

H(2 + n) + (2 + n/2) + • • • + (2 + n/2 h ) = 2 ( h + l ) + ^ n / 2 £

e=oh

= 2 ( h + 1) + n ^ l/ 2 e  <  2 ( h + 1) + 2ne=o

In altre parole, il numero totale di copie è 0(n).Per descrivere le operazioni di ricerca e inserimento, necessitiamo della nozione di

predecessore. Data una lista L^ = e^, ej,..., l di elementi ordinati e un elemento x,diciamo che e' € L^ (con O ^ j < m — 1) è il predecessore di x (in L^) se e' è il massimotra i minoranti di x, ovvero e- ^ x < e j + [ : osserviamo che il precedessore è sempre bendefinito perché il primo elemento di L^ è —oo e l'ultimo elemento è +oo.

'Con un piccolo abuso di notazione, scriviamo L C [_' se l'insieme degli elementi di L è un sottoinsiemedi quello degli elementi di L'.

Page 109: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 109/373

 

ScansioneSkipList ( k ) : (pre: gli elementi —oo e +00 fungono da sentinelle)

p = L[h] ;

W H IL E ( p ! = n u l i ) {

 WHILE ( p . s u c c . k e y < = k )

p = p . s u c c ;

p r e d e c e s s o r e = p ;p = p . i n f ;

}

RETURN p r e d e c e s s o r e ;

Co di ce 3. 3 Scansione di una lista a salti per la ricerca di una chiave k.

La ricerca di una chiave k è concettualmente semplice. Ad esempio, supponiamo di

voler cercare la chiave 80 nella lista mostrata nella Figura 3.8. Partendo da I_2, troviamoche il predecessore di 80 in L2 è 18: a questo punto, passiamo alla copia di 18 nella listaLi e troviamo che il predecessore di 80 in ques t'u ltima lista è 41 . Passando alla copiadi 41 in Lo, troviamo il predecessore di 80 in questa lista, ovvero 80 stesso: pertanto, lachiave è stata trovata.

Tale modo di procedere è mostrato nel Codice 3.3, in cui supponiamo che gli ele-menti in ciascuna lista Lg abbi amo un r iferimento i n f per raggiungere la corrispondentecopia nella lista inferiore Partiamo dalla lista Lh (riga 2) e troviamo il predecessorePh. di k in tale lista (righe 4—6). Poiché Lh C L^-i, possiamo raggiungere la copia di

Ph in L h _i (riga 7) e, a partire da questa posizione, scandire ques t'u ltima lista in avantiper trovare il predecessore P h-i di k in L^-i- Ripetiamo questo procedimento per tuttii livelli £ a decrescere: partendo dal predecessore p^ di k in L^, raggiungiamo la sua copiain e percorri amo quest'ult ima lista in avanti per trovare il predecessore p^ ] di kin L/>_] (righe 3—8). Qu ando raggiungiamo Lo (ovvero, p è uguale a n u l i nella riga 3),la variabile p r e d e c e s s o r e memorizza po, che è il predecessore che avremmo trovato seavessimo scandito Lo dall'inizio di tale lista.

Il lettore attento avrà certamente notato che l'algoritmo di ricerca realizzato dal Co-dice 3.3 è molto simile alla ricerca binaria descritta nel caso degli array (Paragrafo 2.4.1):in effetti, ogni movimento seguendo il campo succ corrisponde a dimezzare la porzionedi sequenza su cui proseguire la ricerca. Per questo motivo, è facile dimostrare che ilcosto della ricerca effettuata dal Codice 3.3 è O(logn) tempo, contrariamente al tempoO(n) richiesto dalla scansione sequenziale di Lo-

Il problema sorge con l'operazione di inserimento, la cui realizzazione ricalca l'al-goritmo di ricerca. Una volta trovata la posizione in cui inserire la nuova chiave, però,l'inserimento vero e proprio risulterebbe essere troppo costoso se volessimo continuarea mantenere le proprietà della lista a salti descritte in precedenza, in quanto questo po-

Page 110: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 110/373

 

trebbe voler dire modificare le copie di tutti gli elementi che seguono la chiave appenainserita. Per far fronte a questo problema, usiamo la casualità: il risultato sarà un algorit-mo random di inserimento nella lista a salti che non garantisce la struttura perfettamentebilanciata della lista stessa, ma che con alta probabilità continua a mantenere un'altezza

media logaritmica e un tempo medio di esecuzione di una ricerca anch'esso logaritmico.Notiamo che, senza perdita di generalità, la casualità può essere vista come l'esito diuna sequenza di lanci di una moneta equiprobabile, dove ciascun lancio ha una possibilitàsu due che esca testa (codificata con 1) e una possibilità su due che esca croce (codificatacon 0). Precisamente, di remo che la probabili tà di ott enere 1 è q = j e la probabil ità diottenere 0 è 1 — q = j (in generale, un truf faldino potrebbe darci una moneta per cui

Attraverso una sequenza di b lanci, possiamo ottenere una sequenza random di b bitcasuali.2 Ciascun lancio è nella pratica simulato mediante la chiamata a una primitiva

rand om() per generare un valore reale r pseudocasuale appartenente all'intervallo 0 ^r < 1, in modo un iforme ed equiprobabile (tale generatore è disponibile in mol te librerieper la programmazione e non è semplice ottenerne uno statisticamente significativo, inqua nto il programma che lo genera è deterministi co): il numero r generato pseudo-casualmente fornisce quindi il bit 0 se 0 ^ r < j e il bit 1 se 5 ^ r < 1.

Osserviamo che i lanci di moneta sono eseguiti in modo indipendente, per cui otte-niamo una delle quattro possibili sequenze di b = 2 bit (00, 01, 10 oppure 11) in modocasuale, con probabilità ^ x \ = in generale, le probabili tà dei lanci si mol tiplicano inquanto sono eventi indipendenti, ottenendo una sequenza di b bit casuali con probabi-

lità l/2b

. Osserviamo inoltre che prima o poi dobbiamo incontrare un 1 nella sequenzase b è sufficientemente grande.

Utilizziamo tale concetto di casualità per inserire una chiave k in una lista a salti.Una volta identificati i suoi predecessori P0,Pi,• • • ,Pk> in maniera analoga a quantodescritto per l'operazione di ricerca, eseguiamo una sequenza di lanci di moneta finchénon ott eniamo 1. Sia r ^ 1 il nu mero di bit casuali (lanci) così generati per k, per cui iprimi r— 1 bit sono 0 e l'u ltimo è 1. Se r ^ h + 1 , creiamo r copie di k e le inseriamo nelleliste Lo, L], l_2,..., L r: ciascuna inserzione richiede tempo costante in quanto va creatoun nodo immediatamente dopo ciascuno dei predecessori Po.Pi, • • •, Pr- Altrimenti,

creiamo h-l-1 copie della chiave k, aggiorniamo tutte le liste già esistenti secondo quantodetto prima e creiamo una nuova lista L h + i composta dalle chiavi —oo, k e +oo . Co mevedremo, il costo dell'operazione è, in media, O(Iogn).

2La nozione di sequenza random R è stata formalizzata nella teoria di Kolmogorov in termini di incom-pressibilità, per cui qualunque programma che generi R non può richiedere significativamente meno bit perla sua descrizione di quanti ne contenga R. Per esempio, R = 010101 • • • 01 non è casuale in quanto unprogramma che scrive per b/2 volte 01 può generarla richiedendo solo O(logb) bit per la sua descrizio-ne. Pur troppo è indecidibi le stabilire se una sequenza è rando m anch e se la strag rande maggioranza dellesequenze binarie lo sono.

Page 111: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 111/373

 

ALVIE : liste a salti

Osserva, sperimenta e verifica

SkipList

Per stabilire la complessità media delle operazioni di ricerca e inserimento su unalista a salti (random), valutiamo un limite superiore per l'altezza media e per il nume-ro med io di copie create con il pro ced imento appena descritto. La lista di livello piùbasso, Lo, contiene n + 2 elementi ord inati. Per ciascun inserimento di una chiave k,indipendentemente dagli altri inserimenti abbiamo lanciato una moneta equiprobabileper decidere se Li debba contenere una copia di k (bit 0) o meno (bit 1): quindi Licontiene circa n/2 + 2 elementi (una frazione costante di n in generale), perché i lancisono equiprobabili e all'incirca metà degli elementi in LQ ha ottenuto 0, creando unacopia in Li, e il resto ha ottenuto 1. Ripetendo tale argomento ai livelli successivi, risultache L2 contiene circa n/4 + 2 elementi, L3 ne cont iene circa n / 8 + 2 e così via: in ge-nerale, con tiene circa n/2e + 2 elementi ordinati e, quando t  = h, l'ultimo livello necontiene un numero costante c > 0, ovvero n / 2 h + 2 = c. Ne deriviamo che l'altezza h.è in media O(logn) e, in modo analogo a quanto mostrato in precedenza, che il numerototale medio di copie è O(n) (la dimostrazione formale di tali proprietà sull'altezza e sulnumero di copie richiede in realtà strumenti più sofisticati di analisi probabilistica).

Mostriamo ora che la ricerca descritta nel Codice 3.3 richiede tempo medio 0(logn).Per un generico livello t nella lista a salti, indichiamo con T(^) il numero medio dielementi esaminati dall'algoritmo di scansione, a partire dalla posizione corrente nellalista L( fino a giungere al predecessore po di k nella lista Lo: il costo della ricerca è quindiproporzionale a 0(T(h)).

Per valutare T(h) , osserviamo che il cammino di attraversamento della lista a saltisegue un profilo a gradino, in cui i tratti orizzontali corrispondono a porzioni dellastessa lista ment re quelli verticali al passaggio alla lista del livello inferiore. Percorriamoa ritroso tale ca mm in o at traverso i predecessori Po. Pi >P2> • • • >Ph> al fine di stabilire

induttivamente i valori di T(0) ,T( 1) ,T(2) , . . . ,T(h.) (dove T(0) = 0(1), essendo giàposizionati su po): notiamo che per T(£) con i ^ 1, lungo il percorso (inverso) nel tra ttointerno a L^, abbiamo solo due possibilità rispetto all'elemento corrente e €

1. Il percorso inverso proviene dalla copia di e nel livello inferiore (riga 7 del Codi-ce 3.3) , nella lista a cui siamo giunti con un costo medio pari a — 1).Tale evento ha probabilit à j in qua nto la copia è stata creata a seguito di un lanciodella moneta che ha fornito 0.

Page 112: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 112/373

 

2. Il percorso inverso proviene dall'elemento è a destra di e in L (riga 5 del Codi -ce 3.3), a cui siamo giunti con un costo medio pari a 1{£)\ in tal caso, è nonpuò avere una corrispettiva copia al livello superiore (in L^+1 ). Tale evento haprobabilità j a seguito di un lancio della moneta che ha fornito 1.

Possiamo quindi esprimere il valore medio di T{£) attraverso la media pesata (come per ilquicksort) dei costi espressi nei casi 1 e 2, ottenendo la seguente equazione di ricorrenzaper un'opportuna costante c' > 0:

+ + (3.1)

Otteniamo una limitazione superiore per il termine T(^) sostituendo la disugua-glianza con l'uguaglianza nell'equazione (3.1), analogamente a quanto discusso nel Pa-

ragrafo 2.5.4. Moltiplicando i termini dell'equazione così trasformata per 2 e risolvendorispetto a T(^) otteniamo

1{£) =T(l- 1) + 2 c ' (3.2)

Espandendo l'equazione (3.2), abbiamo T{£) = 1{1 - 1 ) + 2c' = T(£ - 2) + (2 + 2)c' =••• = T(0) + (2£)c' = 0( £) . Quindi T(H) = O(H) = O( logn ) è il costo medio dellaricerca.

In conclusione, le liste randomizzate sono un esempio concreto di come l'uso ac-corto della casualità possa portare ad algoritmi semplici che hanno in media (o con alta

probabilità) ottime prestazioni in tempo e in spazio.

3.4 Opus libri: gestione di liste ammortizzatee di liste ad auto-organizzazione

L'efficacia dell'auto-organizzazione nella gestione delle liste, da sempre accertata a livelloeuristico e sperimentale, può essere mostrata in modo rigoroso facendo uso dell'analisiammortizzata, permettendo di ottenere un'implementazione efficiente delle operazioni

di ricerca, inserimento e cancellazione. Prima di descrivere e analizzare in dettaglio unatale organizzazione delle liste, discutiamo un semplice caso di liste ammortizzate.

3.4.1 Unione e appartenenza a liste disgiunte

Le liste possono essere impiegate per operazioni di tipo insiemistico: avendo già visto co-me inserire e cancellare un elemento, siamo interessati a gestire una sequenza arbitraria Sdi operazioni di unione e appartenenza su un insieme di liste contenenti un totale di melementi. In ogni istante le liste sono disgiunte, ossia l'intersezione di due qualunque liste

Page 113: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 113/373

 

è vuota. Inizialmente, abbiamo m liste, ciascuna for mata da un solo elemento. Un'o-perazione di unione in S prende due delle liste attualmente disponibili e le concatena(non importa l'ordine di concatenazione). Un'operazione di appartenenza in S stabiliscese due elementi appartengono alla stessa lista.

Tale problema viene chiamato di union-find  e trova applicazione, per esempio, inalcuni algoritmi su grafi che discuteremo in seguito. Mantenendo i riferimenti al primoe all'ultimo elemento di ogni lista, possiamo realizzare l'operazione di unione in tempocostante. Tuttavia, ciascuna operazione di appartenenza può richiedere tempo O(m) alcaso pessimo (pari alla lunghezza di una delle liste dopo una serie di unioni), totalizzandoO(nm) tempo per una sequenza di n operazioni.

Presentiamo un modo alternativo di implementare tali liste per eseguire un'arbitrariasequenza S di n operazioni di unione e appartenenza in O(nlogn) tempo totale, mi-gliorando notevolmente il limite di O(nm) in quanto n < m. Rappresentiamo ciascuna

lista con un riferimento all'inizio e alla fine della lista stessa nonché con la sua lunghez-za. Inoltre, corrediamo ogni elemento z di un riferimento z . l i s t a alla propria listadi appartenenza: la regola intuitiva per mantenere tali riferimenti, quando effettuiamoun 'un ione tra due liste, consiste nel cambiare il riferimento z . l i s t a negli elementi zdella lista più corta.

Il Codice 3.4 realizza tale semplice strategia per risolvere il problema di union-find,specificando l'operazione Crea per generare una lista di un solo elemento x, oltre allefunzioni A p p a r t i e n i e U n i s c i per eseguire le operazioni di appartenenza e unione perdue elementi x e y. In particolare, l'appartenenza è realizzata in tempo costante attraverso

la verifica che il riferimento alla propria lista sia il medesimo. L'operazione di unione trale due liste disgiunte degli elementi x e y determina anzitutto la lista più corta e quellapiù lunga (righe 2—8): cambia quin di i riferimenti z. l i s t a agli elementi z della listapiù corta (righe 9-13), concatena la lista lunga con quella corta (righe 14—15) e aggiornala dimensione della lista risultante (riga 16).

L'efficacia della modalità di unione può essere mostrata in modo rigoroso facendouso di un'analisi più approfondita, che prende il nome di analisi ammortizzata e cheillust remo in generale nel Paragrafo 3.4.3. Invece di valutare il costo al caso pessimo diuna singola operazione, quest'analisi fornisce il costo al caso pessimo di una sequenza di

operazioni. La giustificazione di tale approccio è fornita dal fatto che, in tal modo, nonignoriamo gli effetti correlati delle operazioni sulla medesima struttura di dati. In genera-le, data una sequenza S di operazioni, diremo che il costo ammortizzato di un'operazionein S è un limite superiore al costo effettivo (spesso difficile da valutare) totalizzato dallasequenza S diviso il numero di operazioni contenute in S. Naturalmente, più aderenteal costo effettivo è tale limite, migliore è l'analisi ammortizzata fornita.

Nel caso delle operazioni U n i s c i e A p p a r t i e n i , siamo interessati a valutare le pri-

me in quanto le seconde richiedono tempo costante. Partendo da m elementi, ciascuno

Page 114: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 114/373

 

Crea( x ) :

lista.inizio = lista.fine = x;

lista.lunghezza = 1;

x.lista = lista;

x.succ = nuli;

(pre: x non vuoto)

Appartieni( x, y ):

RETURN (x.lista == y.lista);

{pre: x, y non vuoti)

UnisciC x, y ): (pre: x,y non vuoti e x.. lista ^ y. lista)

IF (X.lista.lunghezza <= y.lista.lunghezza) {corta = x.lista;

lunga = y.lista;

> ELSE {

corta = y.lista;

lunga = x.lista;

z = corta.inizio ;

 WHILE (z != nu li ) {

z.lista = lunga;

z = z.succ;

>lunga.fine.succ = corta.inizio ;

lunga.fine = corta.fine;

lunga.lunghezza = corta.lunghezza + lunga.lunghezza;

Codice 3.4 Operazioni di creazione, appartenenza e unione nelle liste disgiunte.

dei quali diventa inizialmente una lista di un singolo elemento attraverso l'operazio-ne Crea, possiamo concentrarci su un'arbitraria sequenza S di n operazioni Unisci.Osserviamo che, al caso pessimo, la complessità in tempo dell'unione è proporzionaledirettamente al nume ro di riferimenti z . l i s t a che vengono modificati alla riga 11 delCodice 3.4: per calcolare il costo totale delle operazioni in S, è quindi sufficiente valutare

un limite superiore al num ero totale di riferimenti z . l i s t a cambiati.Possiamo conteggiare il numer o di volte che la sequenza S può cambiare z . l i s t a

per un qualunque elemento z nel seguente modo. Inizialmente, l'elemento z appartie-ne alla lista di un solo elemento (se stesso). In un 'operazione di unione, se z . l i s t acambia, vuol dire che z va a confluire in una lista che ha una dimensione almeno dop-

 pia rispetto a quella di partenza. In altre parole, la prima volta che z . l i s t a cambia,la dimensione della nuova lista contenente z è almeno 2, la seconda volta è almeno 4e così via: in generale, l'i-esima volta che z . l i s t a cambia, la dimensione della nuova

>

Page 115: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 115/373

 

lista contenente z è almeno 2 l . D'al tra parte, al termine delle n operazioni di unione,la lunghezza di una qualunque lista è minore oppure uguale a n + 1: ne deriva che lalista contenente z ha lunghezza compresa tra 2 l e n + 1 (ovvero, 2 l ^ n + 1) e che valesempre i = O(l ogn) . Quind i, ogni elemento z vede cambiare il riferimento z . l i s t a alpiù O(logn ) volte. Sommando tale quanti tà per gli n + 1 elementi coinvolti nelle n ope-razioni di unione, otteniamo un limite superiore di O(nlogn) al numero di volte che lariga 11 viene globalmente eseguita: pertanto, la complessità in tempo delle n operazioniUnisci è O(nlogn) e, quindi, il costo ammortizzato di tale operazione è O(logn). Alcosto di queste operazioni, va aggiunto il costo 0(1) per ciascuna delle operazioni Creae A p p a r t i e n i .

Lo schema adottato per cambiare i riferimenti z . l i s t a è piuttosto generale: ipo-tizzando di avere insiemi disgiunti i cui elementi hanno ciascuno un'etichetta (sia essaz . l i s t a o qualunque altra informazione) e applicando la regola che, quando due in-siemi vengono uniti, si cambiano solo le etichette agli elementi dell'insieme di cardinaliàminore, siamo sicuri che un'etichetta non possa venire cambiata più di O(logn) vol-te. L'intuizione di cambiare le etichette agli elementi del più piccolo dei due insiemida unire viene rigorosamente esplicitata dall'analisi ammortizzata: notiamo che, invece,cambiando le etichette agli elementi del più grande dei due insiemi da unire, un'etichettapotrebbe venire cambiata n(n) volte, invalidando l'argomentazione finora svolta.

ALVIE: unione e appartenenza a liste disgiunte

Osserva, sperimenta e verifica

U n ion F in d

<P>oCD

a?œo

3.4.2 Liste ad auto-organizzazione

L'auto-organizzazione delle liste è utile quando, per svariati motivi, la lista non è ne-

cessariamente ordinata in base alle chiavi di ricerca (contrariamente al caso delle listerandomizzate del Paragrafo 3.3). Per semplificare la discussione, consideriamo il so-lo caso della ricerca di una chiave k in una lista e adottiamo uno schema di scansionesequenziale, illustrato nel Codice 3.1: percorriamo la lista a partire dall'inizio verifican-do iterativamente se l'elemento attuale è uguale alla chiave cercata. Estendiamo taleschema per eseguire eventuali operazioni di auto-organizzazione al termine della scan-sione sequenziale (le operazioni di inserimento e cancellazione possono essere ottenutesemplicemente, secondo quanto discusso nel Paragrafo 3.1).

Page 116: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 116/373

 

MoveToFrontC a, k ):p = a;IF (p == n u l l I I p . d a t o == k) RETURN p;

 WHILE (( p. su cc != nu li ) && (p .s uc c. da to != k))p = p.succ;

IF (p.succ == null) RETURN n u l i ;tmp = a;a = p.succ;p . succ = p . succ . succ ;a.succ = tmp;re turn a ;

Codic e 3. 5 Ricerca di una chiave k in una lista ad auto-organizzazione .

Tale organizzazione sequenziale può trarre beneficio dal principio di località tem-porale, per il quale, se accediamo a un elemento in un dato istante, è molto probabileche accederemo a questo stesso elemento in istanti immediatamente (o quasi) successivi.Seguendo tale principio, sembra naturale che possiamo riorganizzare proficuamente glielementi della lista dopo aver eseguito la loro scansione. Per questo mot ivo, una listacosì gestita viene riferita come struttura di dati ad auto-organizzazione (self-organizing

o self-adjusting). Tra le varie strategie di auto-organizzazione, la più diffusa ed effica-ce viene detta move-to-front  (MTF ) , che consideriamo in questo paragrafo: essa consiste

nello spostare l'elemento acceduto dalla sua posizione attuale alla cima della lista, senzacambiare l'ordine relativo dei rimanenti elementi, come mostrato nel Codice 3.5. Osser-viamo che MTF effettua ogni ricerca senza conoscere le ricerche che dovrà effettuare inseguito: un algoritmo operante in tali condizioni, che deve quindi servire un insieme dirichieste man mano che esse pervengono, viene detto in linea {online).

Un esempio quotidiano di lista ad auto-organizzazione che utilizza la strategia MTFè costituito dall'elenco delle chiamate effettuate da un telefono cellulare: in effetti, èprobabile che un numero di telefono appena chiamato, venga usato nuovamente nelprossimo futuro. Un altro esempio, più informatico, è proprio dei sistemi operativi, dove

la strategia MTF viene comunemente denominata least recently used  (LRU). In questocaso, gli elementi della lista corrispondono alle pagine di memoria, di cui solo le prime rpossono essere tenute in una memoria ad accesso veloce. Quando una pagina è richiesta,quest'ultima viene aggiunta alle prime r, mentre quella a cui si è fatto accesso menorecentemente viene rimossa. Quest'operazione equivale a porre la nuova pagina in cimaalla lista, per cui quella originariamente in posizione r (acceduta meno recentemente) vain posizione successiva, r + 1, uscendo di fatto dall'insieme delle pagine mantenute nellamemoria veloce.

Page 117: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 117/373

 

Per valutare le prestazioni della strategia MTF, il termine di paragone utilizzato saràun algoritmo fuori linea (offl ine) , denominato OPT, che ipotizziamo essere a conoscenzadi tutte le richieste che perverranno. Le prestazioni dei due algoritmi verranno confron-tate rispetto al loro costo, definito come la somma dei costi delle singole operazioni, inaccordo a quanto discusso sopra: in particolare, contiamo il numero di elementi scan-diti a partire dall'inizio della lista, per cui accedere all'elemento in posizione i ha costo i inquanto dobbiamo scandire gli i elementi che lo precedono. Lo spostamento in cima allalista, operato da MTF, non viene conteggiato in quanto richiede un costo costante.

Tale paradigma è ben esemplificato dalla gestione delle chiamate in uscita di untelefono cellulare: l'ultimo numero chiamato è già disponibile in cima alla lista per laprossima chiamata e il costo indica il numero di clic sulla tastierina per accedere a ulteriorinumeri chiamati precedentemente (occorrono un numero di clic pari a i per scandire glielementi che precedono l'elemento in posizione i nell'ordine inverso di chiamata).

E di fondamentale importanza stabilire le regole di azione di OPT, perché questo puòdare luogo a risultati completamente differenti. Nel nostro caso, OPT parte dalla stessalista iniziale di MTF. Esaminate tutte le richieste in anticipo, OPT permuta gli elementidella lista solo una volta all'inizio, prima di servire le richieste. A questo punto, quandoarriva una richiesta per l'elemento k in posizione i, restituisce l'elemento scandendo iprimi i elementi della lista, senza però muovere k dalla sua posizione.

Notiamo che OPT permuta gli elementi in un ordine (per noi imprevedibile) che

rende min imo il suo costo futuro. Inol tre, ai fini dell'analisi, presumiamo che le liste

non cambino di lunghezza durante l'elaborazione delle richieste.

A titolo esemplificativo, è utile riportare i costi in termini concreti del numero diclic effettuati sui cellulari. Immaginiamo di essere in possesso, oltre al cellulare di marcaMTF, di un futuristico cellulare OPT che conosce in anticipo le n chiamate che sarannoeffettuate nell'arco di un anno su di esso (l'organizzazione della lista delle chiamate inuscita è mediante le omonime politiche di gestione). Potendo usare entrambi i cellularicon gli stessi m numeri in essi memorizzati, effettuiamo alcune chiamate su tali numeriper un anno: quando effettuiamo una chiamata su di un cellulare, la ripetiamo anchesull'altro (essendo futuristico, OPT si aspetta già la chiamata che intendiamo effettuare).Per la chiamata j, dove j = 0 , l , . . . , n — 1 , conti amo il num ero di clic che siamo costretti

a fare per accedere al numero di interesse in MTF e, analogamente, annotiamo il numerodi clic per OPT (ricordiamo che MTF pone il numero chiamato in cima alla sua listamentre OPT non cambia più l'ordine inizialmente adottato in base alle chiamate future).Allo scadere dell'anno, siamo interessati a stabilire il costo, ovvero il numero totale di cliceffettuati su ciascuno dei due cellulari.

Mostriamo che, sotto opportune condizioni, il costo di MTF non supera il doppio del

costo di OPT. In un certo senso, MTF offre una forma limitata di chiaroveggenza delle

richieste rispetto a OPT, motiv ando il suo impiego in vari contest i con successo. In

Page 118: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 118/373

 

Lista MTF == e4 22 26 eo Ci 23 27 25Lista PRM == e 0 ei e4 25 26 27

Figura 3.9 Un'istantanea delle liste manipolate da MTF e PRM.

realtà quella adottata da OPT è una delle possibili permutazioni degli elementi della lista:

mostriamo quindi una proprietà più generale. Presa una qualunque permutazione della

lista iniziale, definiamo PRM come l'algoritmo che opera sulla lista permutata in analogia

a quanto descritto per OPT (ovvero un elemento non viene cambiato di posizione dopo

ogni accesso). I costi di PRM sono definiti analogamente a quelli di OPT per cui, quando

la permutazione è quella fissata da OPT, i comportamenti di PRM e OPT coincidono.

Mostrando in generale che il costo di MTF non supera il doppio del costo di PRM, otteniamola dimostrazione anche per il caso specifico di OPT.Formalmente, consideriamo una sequenza arbitraria di n operazioni di ricerca su una

lista di m elementi, dove le operazioni sono enumerate da 0 a n — 1 in base al loro ordine

di esecuzione. Per 0 ^ j ^ n - 1, l'operazione j accede a un elemento k nelle lista come

nel Codice 3.5: sia Cj la posizione di k nella lista di MTF e cj la posizione di k nella lista

di PRM. Poiché vengono scanditi Cj elementi prima di k nella lista di MTF, e cj elementi

prima di k nella lista di PRM, definiamo il costo delle n operazioni, rispettivamente,

n—1 n-1

costo(MTF) = CÌ e costo(PRM) = c  j (3-3)  j=0 j=0

Vogliamo mostrare che costo(MTF) ^ 2 x costo(PRM) + 0(m 2 ) per ogni permutazioneiniziale della lista di m elementi, ovvero che

n- l n-1

Y_ Cj Si 2 Y_ c j + ° (™ 2 ) (3-4)  j=0 j=0

Da tale diseguaglianza segue che MTF scandisce asintoticamente non più del doppio degli ,elementi scanditi da OPT qu an do n m 2 (scegliendo nell'analisi la specifica permuta-zione, per noi imprevedibile, realizzata da OPT). Nel seguito proviamo una condizionepiù forte di quella espressa nella diseguaglianza (3.4) da cui possiamo facilmente derivarequest'ultima: a tal fine, introduciamo la nozione di inversione. Supponiamo di aver ap-pena eseguito l'operazione j che accede all'elemento k, e consideriamo le risultanti listedi MTF e PRM: un esempio di configurazione delle due liste in un certo istante è quelloriportato nella Figura 3.9.

Page 119: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 119/373

 

Presi due elementi distinti x e y in una delle due liste, questi devono occorrere an-che nell'altra: diciamo che l'insieme {x, y} è un'inversione quando l'ordine relativo dioccorrenza è diverso nelle due liste, ovvero quando x occorre prima di y (non necessa-riamente in posizioni adiacenti) in una lista mentre y occorre prima di x nell'altra lista.Nel nostro esempio, {eo,e2} è un'inversione, mentre {e], e-?} non lo è. Definiamo con

il numero di inversioni tra le due liste dopo che è stata eseguita l'operazione j: vale

0 ^ cDj ^ , per 0 ^ j ^ n — 1, in quanto = 0 se le due liste sono ugualimentre, se sono una in ordine inverso rispetto all'altra, ognuno degli (™) insiemi di dueelementi è un'inversione. Per dimostrare la (3.4), non possiamo utilizzare direttamente laproprietà che Cj ^ 2cJ + 0(1) , in quanto questa proprietà in generale non è vera. Invece,ammortizziamo il costo usando il numero di inversioni <Dj, in modo da dimostrare laseguente relazione (introducendo un valore fittizio O^i che specifichiamo in seguito):

Cj + O j - O j . , < 2c ( (3.5)

Possiamo derivare la (3.4) dalla (3.5) in quanto quest'ultima implica che

n — 1 n—1

  j=0 j=0

1 termini O nella sommatoria alla sinistra della precedente diseguaglianza formano una

cosiddetta somma telescopica, (<D0 - ^ - ì ) + - + (^ 2 - H 1- ( ^ n - i -

<t>n_2) = ^ n - i ~ il> i, nella quale le coppie di termini di segno opposto si elidono

algebricamente: da questa osservazione segue immediatamente che

n-l n-1® n _ i - < l > _ i + ^ c j < 2 ^ c ( (3.6)

  j = 0 i =0

Ponendo <D_i = m | r ^ + 1 ) , vale <5_i - Ì>n_i = 0(m 2 ): portando a destra del segno didiseguaglianza i primi due addendi nella (3.6), otteniamo così la disuguaglianza (3.4).

Possiamo quindi concentrarci sulla dimostrazione dell'equazione (3.5), dove il caso  j = 0 vale per sostituzione del valore fissato per 0 _ i , in qua nto <Do ^ 11 e Co < m.Ipotizziamo qu ind i che l'operazione j > 0 sia stata eseguita: a tale scopo, sia k l'elementoacceduto in seguito a tale operazione, e supponiamo che k occupi la posizione i nella listadi MTF -(per cui Cj = v): notiamo che la (3.5) è banalmente soddisfatta quando i = 0perché la lista di MTF non cambia e, quindi, Oj = <Dj_i. Prendiamo l'elemento k' cheappare in una generica posizione i ' < i. Ci sono solo due possibilità se esaminiamol'insieme {k', k}: è un'inversione oppure non lo è. Quando MTF pone k in cima allalista, tale insieme diventa un'inversione se e solo se non lo era prima: nel nostro esempio,se k = (per cui i = 5), possiamo riscontrare che, cons iderando gli element i k ' in

Page 120: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 120/373

 

posizione da 0 a 4, due di essi, e4 e e^, fo rmano (assieme a un'inversione mentr e irimanenti tre elementi non dan no luogo a inversioni. Qu an do viene posto in cima allalista di MTF, abbiamo che gli insiemi {e3, e4} e e<$} non sono più inversioni, mentrelo diventano gli insiemi (e2,63}, (eo, e-}) e {e\,

In generale, gli i elementi che precedono k nella lista di MTF sono composti daf elementi che (assieme a k) danno luogo a inversioni e da g elementi che non danno

luogo a inversioni, dove f + g = i. Dopo che MTF pone k in cima alla sua lista, ilnumero di inversioni che cambiano sono esclusivamente quelle che coinvolgono k. Inparticolare, le f inversioni non sono più tali mentre appaiono g nuove inversioni, comeillustrato nel nostro esempio. Di conseguenza la differenza nel nu mero di inversionidopo l'operazione j è <Dj — 1 = —f + g. Ne deriva che cj -(- <J>j — ®j-i = i — f + g =(f + g ) - f + g = 2g.

Consideriamo ora la posizione Cj dell'elemento k nella lista di PRM: sappiamo certa-mente che e- > g perché ci sono almeno g elementi che precedono k, in quant o appa ionoprima di k anche nella lista di MTF e non formano con k inversioni prima dell'operazio-ne j. A questo punto, otteniamo l'equazione (3.5), in quanto Cj+Oj—Oj 1 = 2g ^ 2c- ,concludendo di fatto l'analisi ammortizzata.

ALVIE: liste ad auto-organizzazione

Osserva, sperimenta e verifica

MoveToFront

Osserviamo che tale analisi della strategia MTF sfrutta la condizione che l'algorit-mo PRM non può manipolare la lista una volta che abbia iniziato a gestire le richieste.Purtroppo questa condizione è necessaria e la precedente analisi ammortizzata non è piùvalida se permettiamo anche all'algoritmo fuori linea di manipolare la sua lista: acce-dendo all'elemento in posizione i, l'algoritmo può ad esempio riorganizzare la lista intempo O(i) (pensiamo a un impiegato con la sua pila disordinata di pratiche: pescata lapratica in posizione i, può metterla in cima alla pila ribaltando l'ordine delle prime i nel

contempo). In particolare, è possibile dimostrare che un algoritmo fuori linea che adottatale strategia, denomina to REV, ha un costo pari a O ( n l og n ) men tre il costo di MTFrisulta essere S(n 2 ), invalidando l'equazione (3.4) per n sufficientemente grande.

Tuttavia, MTF rimane una strategia vincente per organizzare le proprie informazioni.L'economista giapponese Noguchi Yukio ha scritto diversi libri di successo sull'organizza-zione aziendale e, tra i metodi per l'archiviazione cartacea, ne suggerisce uno particolar-mente efficace. Il metodo consiste nel mettere l'oggetto dell'archiviazione (un articolo,il passapor to, le schede telefoniche e così via) in una bus ta di carta etichet tata. Le buste

Page 121: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 121/373

 

sono mantenute in un ripiano lungo lo scaffale e le nuove buste vengono aggiunte incima. Quando una busta viene presa in una qualche posizione del ripiano, identificatascandendolo dalla cima, viene successivamente riposta in cima dopo l'uso. Nel momentoin cui il ripiano è pieno, un certo quantitivo di buste nel fondo viene trasferito in un'op-portuna sede, per esempio una scatola di cartone etichettata in modo da identificarne ilcontenuto . Noguchi sostiene che è più facile ricordare l'ordine temporale dell'uso deglioggetti archiviati piuttosto che la loro classificazione in base al contenuto, per cui il me-todo proposto permette di recuperare velocemente tali oggetti dallo scaffale. Possiamofacilmente riconoscere la strategia MTF nell'ordine ottenuto dal metodo di Noguchi, inbase alla frequenza d'uso.

In conclusione, le liste ad auto-organizzazione presentano una serie di vantaggi, inquanto hanno buone prestazioni sotto certe condizioni, sono adattive rispetto alla distri-buzione delle richieste, possiedono semplici algoritmi di manipolazione e, infine, nonnecessitano di informaz ioni ausiliarie per la gestione (a par te i puntato ri di lista). Co-me ogni altra struttura, tuttavia, presentano anche alcuni svantaggi, poiché il costo dellasingola operazione al caso pessimo può essere lineare e, inoltre, ogni ricerca comportacomunque una ristrutturazione della lista.

3.4.3 Tecniche di analisi ammortizzata

Le operazioni di unione e appartenenza su liste disgiunte e quelle di ricerca in liste adauto-organizzazione non sono i primi due esempi di algoritmi in cui abbiamo applicatol'analisi ammortizzata. Abbiamo già incontrato implicitamente un terzo esempio di tale

analisi per valutare il costo delle operazioni di ridimensionamento di un array di lun-ghezza variabile (Paragrafo 2.1 .3) . Questi tre esempi illustrano tre dif fuse modalità dianalisi ammortizzata di cui diamo una descrizione utilizzando come motivo conduttoreil problema dell'incremento di un contatore.

In tale problema, abbiamo un contatore binario di k cifre binarie, memorizzate inun array c o n t a t o r e di dimensione k i cui elementi valgono 0 oppure 1. In particolare,il valore del contatore è dato da ( c o n t a t o r e [ i ] x 2V) e supponiamo che esso

contenga tutti 0 inizialmente.

Come mostrato nel Codice 3.6, l'operazione di incremento richiede un costo in tem-

po pari al numero di elementi cambiati in c o n t a t o r e (righe 4 e 7), e quindi O(k) tempoal caso pessimo: discutiamo tre modi di analisi per dimostrare che il costo ammortizzatodi una sequenza di n = 2 k incrementi è soltanto 0(1) per incremento.

Il primo metodo è quello di aggregazione: conteggiamo il numero totale T(n) di pas-si elementari eseguiti e lo dividiamo per il numero n di operazioni effettuate. Nel nostrocaso, conteggiamo il num ero di elementi cambiati in c o n t a t o r e (righe 4 e 7), suppo-nen do che quest' ult imo assuma il valore iniziale pari a zero. Effettu ando n incrementi,osserviamo che l'elemento contatore[0] cambia (da 0 a 1 o viceversa) a ogni incre-

Page 122: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 122/373

 

Incrementa( contatore ):

i = 0;

 WHILE ((i < k) && (contatore[i]

contatore[i] = 0;

i = i+1;

>

IF (i < k) contatore[i] = 1;

Codice 3.6 Incremento di un contatore binario.

mento, quindi n volte; il valore di contatore[l] cambia ogni due incrementi, quindi

n/2 volte; in generale, il valore di contatore[i] cambia ogni 2 l incrementi e quindi

n / 2 1 volte. In totale, il numero di passi è T(n) = £ i = 0 n / 2 1 = ( £ i = 0 1/21)™ < 2n.

Quindi il costo ammortizzato per incremento è 0(1) poiché T(n)/n < 2. Osserviamo

che abbiamo impiegato il metodo di aggregazione per analizzare il costo dell'operazione

di unione di liste digiunte.

Il secondo metodo è basato sul concetto di credito (con relativa metafora bancaria):

utilizziamo un fondo comune, in cui depositiamo crediti o li preleviamo, con il vinco-

lo che il fondo non deve andare mai in rosso (prelevando più crediti di quanti siano

effettivamente disponibili). Le operazioni possono sia depositare crediti nel fondo che

prelevarne senza mai andare in rosso per coprire il proprio costo computazionale: il costo

ammortizzato per ciascuna operazione è il numero di crediti depositati da essa. Osser-viamo che tali operazioni di deposito e prelievo di crediti sono introdotte solo ai fini

dell'analisi, senza effettivamente essere realizzate nel codice dell'algoritmo cosi analizza-

to. Nel nostro esempio del contatore, partiamo da un contatore nullo e utilizziamo un

fondo comune pari a zero. Con riferimento al Codice 3.6, per ogni incremento eseguito

gestiamo i crediti come segue:

1. preleviamo un credito per ogni valore di contatore[i] cambiato da 1 a 0 nella

riga 4;

2. depositiamo un credito qua ndo c o n t a t o r e [ i ] cambia da 0 a 1 nella riga 7.

Da notare che la situazione al punto 1 può occorrere un numero variabile di volte du-rante un singolo incremento (dipende da quanti valori pari a 1 sono esaminati dal ciclo);invece, la situazione al punto 2 occorre al più una volta, lasciando un credito per quandoquel valore da 1 tornerà a essere 0: in altre parole, ogni volta che necessitiamo di un cre-dito nel pu nt o 1, possiamo prelevare dal fond o in qua nto tale credito è stato sicuramentedepositato da un precedente incremento nel punto 2. Quindi il costo ammortizzato per

(pre: V.è la dimensione di contatore)

== 1) ) {

Page 123: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 123/373

 

incremento è 0 (1) . Possiamo applicare il metodo dei crediti per l'analisi ammortizzatadel ridimensionamento di un array a lunghezza variabile: ogni qualvolta che estendia-mo l'array di un elemen to in fondo, depos itiamo c crediti per una certa costante c > 0(di cui uno l'utilizziamo subito); ogni volta che raddoppiamo la dimensione dell'array,ricopiando gli elementi, utilizziamo i crediti accumulati fino a quel momento.

Infine, il terzo metodo è basato sul concetto di potenziale (con relativa metafora fisi-ca). Numerando le n operazioni da 0 a n — 1, ind ichiamo con 0 _ i il potenziale iniziale econ <Dj ^ 0 quello raggiunto dopo l'operazione j, dove 0 ^ j ^ n — 1. La difficoltà con-siste nello scegliere l'opportuna funzione come potenziale <D, in modo che la risultanteanalisi sia la migliore possibile. Ind icando con Cj il costo richiesto dall 'operazione j, ilcosto ammortizzato di quest'ultima è definito in termini della differenza di potenziale,nel modo seguente:

Cj = C j - c D j _ , (3.7)

Quindi , il costo totale che ne deriva è dato da ^ " J o = (c

i + ~~ =^ j 1 ^ 1 Cj + (® n _ i — ): utilizzando il fatto che otteniamo una somma telescopica per

le differenze di potenziale, deriviamo che il costo totale per la sequenza di n operazioni

può essere espresso in termini del costo ammortizzato nel modo seguente:

n— 1 n— 1

C j = ^ c  j + ( 0 „ 1 - c D n _ 1 ) (3.8)

  j=0 j=0

Nell' esempio del contato re binario, po ni amo uguale al nu mero di valori pari a 1 in

c o n t a t o r e dopo il (j + l)-esimo incremento, dove 0 ^ j ^ n — 1: quind i, O i = 0in quanto il con tatore è inizialmente pari a tutti 0. Per semplicità, ipotizziamo che ilcontatore contenga sempre uno 0 e, fissato il (j + 1 )-esimo incremento, indichiamo con£ il numero di volte che viene eseguita la riga 4 nel ciclo while del Codice 3.6: il costoè quindi Cj = £ + 1 in quanto £ valori pari a 1 diventano 0 e un valore pari a 0 diventa 1.Inoltre, la differenza di potenziale ®j — ® j - i misura qua nti 1 sono cambiati: ne abbiamo£ in meno e 1 in più, per cui Ì>j—— ì =—£-{-\. Utilizzando la formula (3.7), otteniamoun costo ammortizzato pari a Cj = (£+ 1) + (—£+ 1) = 2. Poiché ® n _ i ^ 0 e = 0,in base all'equazione (3.8) abbiamo che ^  JQ 1 Cj ^ ^ 2n . Osserviamo che

abbiamo utilizzato il metodo del potenziale per l'analisi della strategia MTF scegliendocome potenziale ®j il numero di inversioni rispetto alla lista gestita da PRM.

ALVIE: problema del contatore binario

Osserva, sperimenta e verifica

BinaryCounter

Page 124: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 124/373

 

RIEPILOGO

  In questo capitolo abbiamo descritto come realizzare le operazioni di ricerca, inserimento e

cancellazione all'interno di una lista, considerando anche le varianti relative a liste doppiee circolari. Abbiamo poi mostrato un'applicazione di array e liste per la risoluzione del

  problema dei matrimoni stabili. Infine, abbiamo ripreso il concetto di algoritmo randommostrando una realizzazione efficiente di una lista a salti e abbiamo introdotto il concetto

di analisi ammortizzata, considerando la gestione dell'unione di liste disgiunte e quella di

liste ad auto-organizzazione.

ESERCIZI

1. Mostrate come modificare il Codice 3.1, utilizzando un ulteriore riferimento q inmodo tale che valga la seguente invariante, necessaria a implementare l'operazio-ne di cancellazione in una lista semplice: se entrambi p e q puntano allo stesso

elemento, questo è il primo della lista; altrimenti, p e q. succ puntano allo stessoelemento nella lista, e tale elemento è diverso dal primo elemento della lista.

2. Mostrate le istruzioni necessarie a inserire un nuovo elemento in cima a una listadoppia e quelle necessarie per le operazioni di inserimento e cancellazione in unalista circolare.

3. Dimost rate che utilizzando una lista semplice per implementare l'algoritmo roundrobin, esistono sequenze di operazioni che richiedono tempo O(n) per operazione,

invece che tempo 0(1).4. Descrivete un'implementazione dell'algoritmo insertion sort che utilizzi liste an-

ziché array, identificando il tipo di lista adatto a ottenere, per ogni sequenza di ndati in ingresso, un costo computazionale uguale a quello dell'implementazionebasata su array (ricordiamo che per alcune sequenze quest'ultima ha complessitàtemporale 0(n)).

5. E possibile implementare l'algoritmo di risoluzione del problema dei matrimonistabili facendo uso esclusivamente di array e mantenendo la complessità temporale

0(n 2 )? Giustificate la risposta.

6. Mostrate che, nonostante sia costo(MTF) ^ 2 x costo(OPT), alcune configura-zioni hanno costo(MTF) < costo(OPT) (prendete una lista di m = 2 elementi eaccedete a ciascuno n / 2 volte).

7. Cons ideriamo un algoritmo fuor i linea REV, il quale applica la seguente strategiaad auto-organizzazione per la gestione di una lista. Qu an do REV accede all'ele-mento in posizione i, va avanti fino alla prima posizione i' ^ i che è una potenza

Page 125: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 125/373

 

del 2, prende quindi i primi i' elementi e li dispone in ordine di accesso futuro(ovvero il successivo elemento a cui accedere va in prima posizione, l'ulteriore suc-cessivo va in seconda posizione, e così via). Ipotizziamo che n = m = 2 k + 1 perqualche k > 0, che inizialmente la lista contenga gli elementi eo, . . . , e m - i eche la sequenza di richieste sia eo, e\,..., e m _i, in questo ordine (vengono cioè ri-chiesti gli elementi nell'ordine in cui appaiono nella lista iniziale). Dimostrate cheil costo di MTF risulta essere 0 ( n 2 ) mentre quello di REV è O(nlogn). Estendetela dimostrazione al caso n > m.

8. Calcolate un valore della costante c adoperata nell'analisi ammortizzata con i cre-diti per il ridimensionamento di un array di lunghezza variabile, dettagliandocome gestire i crediti.

Page 126: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 126/373

 

Capitolo 4

Alberi

SOMMARIO In questo capitolo descriviamo come organizzare i dati in strutture gerarchiche introducendole nozioni di albero binario, albero cardinale e albero ordinale. Discutiamo inoltre unametodologia generale di progettazione degli algoritmi ricorsivi su alberi e le varie modalità divisita (anticipata, simmetrica, posticipata e per ampiezza). L'opus libri del capitolo consistein una soluzione efficiente del problema del minimo antenato comune. Infine, descriviamocome rappresentare in modo implicito e succinto gli alberi al fine di ottenere un risparmiodella memoria occupata.

DIFFICOLTÀ

1,5 CFU.

4.1 Alberi binari

Gli alberi rappresentano una generalizzazione delle liste nel senso che, mentre ogni ele-mento delle liste ha al più un successore, ogni elemento degli alberi può avere più diun successore. Come vedremo, gli alberi sono solitamente utilizzati per rappresentarepartizioni ricorsive di insiemi e strutture gerarchiche: un tipico utilizzo di alberi perrappresentare gerarchie è fornito dagli alberi genealogici, in cui ciascun nodo dell'alberorappresenta una persona della famiglia i cui figli sono ad esso collegati da un arco cia-scuno. Ad esempio, nella Figura 4.1, è mostrata una parte dell'albero genealogico dellafamiglia Baggins di Hobbiville, quella relativa ai discendenti di Largo (corrispondente al-la radice dell'albero), il cui unico figlio è Fosco, i cui nipoti sono Dora, Drogo, e Dudo,e i cui pronipoti sono Frodo e Daisy.

Come possiamo osservare, ogni nodo dell'albero ha associata la lista (eventualmentevuota) dei figli: utilizzando una terminologia che è un misto di genealogia e botanica,

Page 127: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 127/373

 

Figura 4.1 I discendenti di Largo Baggins di Hobbiville.

chiamiamo foglie i nodi senza figli, e nodi interni i rimanenti nodi. Allo stesso tempo,l'albero associa a tutti i nodi, eccetto la radice, un unico genitore, detto padre: i nodifigli dello stesso padre sono detti (rateili. Osserviamo che, ad ogni nodo interno, è

anche associato il sottoalbero di cui tale nodo è radice. Se un nodo u è la radice diun sottoalbero contenente un nodo v, diciamo che u è un antenato di v e che v èun discendente di u. Ad esempio, nella Figura 4.1 consideriamo il nodo Drogo il cuisottoalbero contiene, oltre a se stesso, il suo unico figlio Frodo. Il padre di Drogo èFosco e i suoi fratelli sono Dora e Dudo. Infine, Drogo è discendente di Largo, che èsuo antenato.

Gli alberi genealogici sono spesso utilizzati anche per rappresentare l'insieme degliantenat i di una persona, anziché quello dei suoi discendenti: in tali alberi i figli di unnodo rappresentano, in modo apparentemente contraddittorio, i suoi genitori. Nel-

la Figura 4.2 sono, ad esempio, rappresentati gli antenati (noti agli autori) di FrodoBaggins.

Questo tipo di albero, rispetto a quello visto in precedenza, presenta due principalicaratteristiche: ogni nodo ha al più due figli e ogni figlio ha un ruolo ben determinatoche dipende dall'essere il figlio sinistro oppure il figlio destro (in particolare, il figliosinistro indica il padre mentre il figlio destro rappresenta la madre).

Chiamiamo albero binario un albero siffatto, che può essere definito ricorsivamentenel modo seguente: un albero vuoto è un albero binario che non contiene alcuna chiave

Page 128: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 128/373

 

Figura 4.2 L'albero degli antenati di Frodo Baggins.

e che viene indicato con nu l i , analogamente a quanto fatto con la lista vuota. Un alberobinario (non vuoto) contenente n elementi è costituito dalla radice r, che memorizza unodi questi elementi mettendolo "a capo" degli altri; i rimanenti n — 1 elementi sono divisiin due gruppi disgiunti, ricorsivamente organizzati in due sottoalberi binari distinti, eti-chettati come sinistro e destro e radicati nei due figli t s e r o della radice. Notiamo cheuno o entrambi i nodi Ts e TD possono essere n u l i , a rappresentare sottoalberi vuoti,e che i figli di una foglia sono entrambi uguali a n u l i , così come il padre della radicedell'albero.

Un albero binario viene generalmente rappresentato nella memoria del calcolatorefacendo uso di tre campi. In particolare, dato un nodo u, indichiamo con u . d a t o ilcontenuto del nodo, con u . sx il riferimento al figlio sinistro e con u . dx il riferi-mento al figlio destro UD (talvolta ipotizzeremo che sia anche presente un riferimentou. padre al padre del nodo).

Ad esempio, nella Figura 4.3 (in cui tre sottoalberi sono solo tratteggiati e non espli-citamente disegnati) mostriamo come viene rappresentata la parte superiore dell'albero

Page 129: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 129/373

 

da to s x dx px

FrodoBaggins / /

V

V

RubyBolgeri /

 \

PrimulaBrandibuck /  J 

Ti Fi gu ra 4. 3 La rappresentazione della parte superiore dell'albero genealogico nella Figura 4.2.

genealogico illustrato nella Figura 4.2: osserviamo, tuttavia, che nel seguito preferiremosempre fare riferimento alla rappresentazione grafica semplificata di quest'u ltima figura.

4.1.1 Algoritmi ricorsivi su alberi binari

Gli alberi binari, essendo definiti in modo ricorsivo, permettono di progettare algoritmiricorsivi seguendo una metodologia generale di risoluzione: nel discuterne alcuni esempi,introdurremo anche della terminologia aggiuntiva che, sebbene fornita per semplicitàcon riferimento agli alberi binari, è in generale applicabile anche ad alberi di tipo diverso,quali quelli considerati nel Paragrafo 4.3.

Un parametro che caratterizza un albero è la sua dimensione n, data dal numero dinodi in esso contenuti: chiaramente, un albero di dimensione n ha esattamente n — 1archi (che collegano un qualunque nodo diverso dalla radice al padre), come possiamo

notare nell'esempio di Figura 4.2.Osserviamo che la dimensione di un albero binario può essere definita ricorsivamente

nel modo seguente: un albero vuoto ha dimensione 0, mentre la dimensione di un alberonon vuoto è pari alla somma delle dimensioni dei suoi sottoalberi, incrementata di 1, perincludere la radice.

Il Codice 4.1 utilizza tale osservazione per realizzare un algoritmo che determina ladimensione di un albero binario. Se l'albero è vuoto, la sua dimensione è pari a 0 (riga 3).Se non lo è, le due chiamate ricorsive calcolano la dimensione dei sottoalberi radicati nei

Page 130: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 130/373

 

Dimensione( u ):

IF (u == nuli) {

RETURN 0;

> ELSE {

dimensioneSX = Dimensione( u.sx );

dimensioneDX = Dimensione( u.dx );RETURN dimensioneSX + dimensioneDX + 1;

Co di ce 4.1 Algor itmo ricorsivo per il calcolo della dimensione di un albero binar io.

figli (righe 5-6): di tali dimensioni viene restituita come risultato la somma incrementatadi 1 (riga 7).

Un altro parametro caratteristico di un albero è la sua altezza: per definire tale pa-rametro, notiamo che l'ordine gerarchico esistente tra i nodi di un albero permette diclassificarli in base alla loro profondità. La radice r ha profondità 0, i suoi figli r$ e TDhanno profondità 1 (se diversi da nuli), i nipoti hanno profondità 2 e così via. In ge-nerale, se la profondità di un nodo è pari a p, allora i suoi figli non vuoti (ovvero diversida nuli) hanno profondità p + 1. Il seguente frammento (iterativo) di codice calcola laprofond ità di un nodo, fermandosi alla radice quand o il riferimento al padre è n u l i .

p = 0;

  WHILE (U.padre != nuli) {

p = p + 1;

u = u.padre ;

>

L'altezza h. di un albero è data dalla massima profondità raggiunta dalle sue foglie.Quindi, l'altezza misura la massima distanza di una foglia dalla radice dell'albero, intermini del numero di archi attraversati. E inefficiente calcolare esplicitamente tu tte leprofondità iterando il suddetto frammento di codice per ogni foglia dell'albero, pren-dendone poi la massima per trovare l'altezza. Infa tti, un o stesso nodo u può essereattraversato molte volte per calcolare tali profondità, ovvero tante quante sono le fogliediscendenti di u. Poiché la defin izione di altezza si applica anche ai sottoalberi, è piùefficiente e semplice trovare l'altezza di un albero binario osservando che l'albero com-posto da un solo nodo ha altezza pari a 0, mentre un albero con almeno due nodi haaltezza pari all'altezza del suo sottoalbero più alto, incrementata di 1 in quanto la radiceintroduce un ulteriore livello (da cui deriviamo che l'albero vuoto ha altezza pari a —1).

Il Codice 4.2 utilizza tale osservazione per realizzare un algoritmo che determina l'al-tezza di un albero. Co me poss iamo osservare, se usiamo l'accorgimento di considerare

Page 131: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 131/373

 

Altezza( u ):

IF (u == nuli) {

RETURN -1;

} ELSE {

altezzaSX = Altezza( u.sx );

altezzaDX = Altezza( u.dx );RETURN max( altezzaSX, altezzaDX ) + 1;

 }• (post: restituisce — 1 se e solo se u è nuli)

Codice 4. 2 Algoritmo ricorsivo per il calcolo dell'al tezza di un albero binario.

come caso base l'albero vuoto, otteniamo che il codice segue lo stesso schema del Co-dice 4.1 e, inoltre, che l'altezza calcolata per le foglie risulta correttamente pari a 0 (inquanto sottoalberi composti da un solo nodo): di conseguenza, per induzione è correttaanche l'altezza calcolata per tutti i sottoa lberi. Nello specifico, il Cod ice 4.2 opera nelmodo seguente: se l'albero è vuoto, la sua altezza è pari a —1 (riga 3). Se non lo è, le duechiamate ricorsive calcolano l'altezza dei sottoalberi radicati nei figli (righe 5-6): di talialtezze viene restituita come risultato la massima incrementata di 1 (riga 7).

Sia il Codice 4.1 che il Codice 4.2 hanno quindi un caso base (albero vuoto) e unpasso induttivo (albero non vuoto) in cui avvengono le chiamate ricorsive. A parte ledifferenze sintattiche dovute al fatto che i due codici calcolano quantità differenti, lastruttura computazionale è identica: ciascuna invocazione restituisce un valore (l'altezzao la dimensione), che possiamo facilmente dedurre nel caso base di un albero vuoto.Nel passo induttivo, deleghiamo il calcolo delle rispettive quantità alla ricorsione sui duefigli (sottoalberi): una successiva fase di combinazione di tali quant ità , restituite dallechiamate ricorsive sui figli, contribuisce a ottenere il risultato per il nodo corrente. Talerisultato va a sua volta restituito medi ante l'istruzione r e t u r n , per far sì che l'induzionesi propaghi at traverso la ricorsione: infa tti, chi invoca le chiamate ricorsive deve a suavolta trasmettere il risultato così ottenuto.

A L V I E : altezza di un albero binario

Osserva, sperimenta e verifica

BinaryTreeHeight

o

Non è difficile applicare lo schema ricorsivo appena delineato al calcolo del nu-

mero di foglie discendenti : in realtà, molti problemi su alberi possono essere risol-

Page 132: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 132/373

 

Decomponibile(u):

IF (u == nuli) {

RETURN Decomponibile(nuli);

> ELSE {

risultatoSX = Decomponibile(u.sx);

risultatoDx = Decomponibile(u.dx);

RETURN Ricombina(risultatoSX, risultatoDx);

Codice 4.3 Algoritmo ricorsivo per risolvere un problema decomponibile su alberi binari.

ti analogamente, con varianti più o meno sofisticate. Tali problemi sono dett i de-

componibili, in quanto caratterizzabili secondo il paradigma del divide et impera: siaD ec o mp o ni b i l e ( u ) il valore da calcolare relativamente al sottoalbero radicato nel n odou (ad esempio la sua dimens ione o il numero di foglie in esso con tenute ). Per sfru ttareil paradigma del divide et impera, dobbiamo individuare i seguenti punti focali nelladefinizione di De co mp on ib il e( u) .

Caso base: stabilire il valore di D e c o m p o n i b i l e ( u ) qua ndo u = n u l i (anche se alcune

volte è più semplice definire tale valore quando u è una foglia).

Decompos izione: riformulare il problema per il sottoalbero radicato in un no do u in

termini di quelli radicati nei suoi figli Us e u q .

Ricombinazione: trovare la regola Ricombina che permette di ricombinare i valo-

ri Decomponibile(us) e Decomponibilefuo) in modo da ottenere i l valore

D e c o m p o n i b i l e ( u ) .

Nel caso del Codice 4.2, Decomponibile(u) è l'altezza del sottoalbero radicatoin u: il caso base è rappresentato dalla riga 3 mentre il passo induttivo ha come regoladi combinazione quella riportata nella riga 7. Nel Codice 4.1, Decomponibile(u) èla dimensione del sottoalbero radicato in u: il caso base è rappresentato dalla riga 3mentre il passo induttivo ha come regola di combinazione quella riportata nella riga 7 di

tale codice. Fatte le dovute premesse, possiamo fornire un codice generale per risolvereproblemi decomponibil i su alberi (Codice 4.3), che ricalca lo schema ricorsivo finorausato. Notiamo che ogni nodo viene attraversato un numero costante di volte, per cui seil caso base e la regola Ricombina richiedono tempo costante, l'esecuzione richiede untempo totale O(n).

Lo schema del Codice 4.3 permette di effettuare una visita di un albero binario apartire dalla sua radice. La visita equivale a esaminare tutti i nodi in modo sistematico,una e una sola volta, analogamente alla scansione di sequenze lineari, dove procediamodall'inizio alla fine o viceversa. Per semplicità, du ran te la visita facciamo corr ispondere

Page 133: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 133/373

 

Anticipata( u ):

IF (u != nuli) {

PRINT u.dato;

Anticipata( u.sx );

Anticipata( u.dx );

>

Co di ce 4 .4 Visita antic ipata di un albero binario. Le altre due visite, simmetrica e posticipata, sonoottenute spostando l'istruzione di stampa dalla riga 3 a una delle due righe successive.

l'esame di un nodo all'operazione di stampa del suo contenuto. Tale visita permette

di operare varie scelte che dipendono dall'ordine con cui viene esaminato l'elemento

memorizzato nel nodo corrente e vengono invocate le chiamate ricorsive nei suoi figli.

Visita anticipata (preorder): stampa l'elemento contenuto nel nodo; visita ricorsivamenteil sottoalbero sinistro; visita ricorsivamente il sottoalbero destro.

Visita simmetrica (inorder ): visita ricorsivamente il sottoalbero sinistro; stampa l'ele-

mento contenuto nel nodo-, visita ricorsivamente il sottoalbero destro.

Visita posticipata (postorder ): visita ricorsivamente il sot toa lbero sinistro; visita ricorsi-

vamente il sottoalbero destro; stampa l'elemento contenuto nel nodo.

Il costo di ciascuna delle tre visite è O( n) per un albero di dimensione n (cambiasoltanto l'ordine con cui l'elemento nel nodo corrente viene stampato). Il codice per tali

visite è una semplice variazione del Codice 4.3 (che rappresenta una visita posticipatain cui non viene resti tui to alcun valore). Per esempio, il Codice 4.4 realizza la visitaanticipata: osserviamo che non restituisce alcun valore in questa fo rma e che può es-sere trasformato nel codice di una visita simmetrica o posticipata molto semplicemente,spostando l'istruzione di stampa (riga 3). Per apprezzare la differenza delle tre visite, con-sideriamo l'esempio mostrato nella Figura 4.4, in cui oltre alle tre visite suddette vieneillustrato anche il risultato di una quarta visita che illustreremo più avanti.

A L V I E : visita posticipata di un albero binario

Tornando allo schema del Codice 4.3, possiamo notare che esso rappresenta un mo-

do di effettuare una visita posticipata in cui viene raccolta l'informazione necessaria alla

Osserva, sperimenta e verifica

BinaryTreePostorder

Page 134: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 134/373

 

anticipata:

F B D B R B P B G B M B A B M T G T

posticipata:

simmetrica:

R B D B M B A B G B G T M T PB FB

ampiezza:

D B RB FB M B G B A B P B M T G T

F B D B PB RB G B M T M B A B G T

Figu ra 4.4 Risultato delle visite di un albero binario.

computazione di De c o mp o n i b i l e ( u ) , a partire dal basso verso l'alto. Per risolvere alcu-ni problemi decomponibili, è necessario raccogliere più informazione di quanta ne servaapparentemente: studiamo, ad esempio, il caso degli alberi completamente bilanciati.

Un albero binario è completo se ogni nodo interno ha esattamente due figli nonvuoti. L'albero è completamente bilanciato se, oltre a essere completo, tutte le fogliehanno la stessa profondità . Un albero complet ament e bilancia to di altezza h. ha qu indi2 h — 1 nodi interni e 2

h foglie: ne deriva che la relazione tra altezza h. e numero di nodin = 2 h + 1 — 1 è h = log(n + 1) — 1. Possiamo introdurre la definizione di albero binariobilanciato: in tale albero vale la relazione h. = O(l ogn) , che risulta essere in teressanteper la complessità delle operazioni fornite da diverse str ut ture di dati. No tiamo che unalbero completamente bilanciato è bilanciato, mentre il viceversa non sempre vale.

Volendo usare lo schema del Codice 4.3 per stabilire se un albero binario è completa-

mente bilanciato, possiamo valutare cosa succede ipotizzando che Decomponibile(u)

sia un valore booleano, che risulta T R U E se e solo se T(u) è completamen te bilanciato,

dove T(u) indica l'albero radicato in u . Indicati come al solito con e con u p i duefigli di u, il fatto che T (us ) e T(UQ ) siano completamente bilanciati, non comporta pur-

troppo che anche T(u) lo sia, in quanto i due sottoalberi potrebbero avere altezze diverse:

in altre parole, T(u) è completamente bilanciato se e solo se T (us ) e T(U.D ), oltre a essere

completamente bilanciati, hanno anche la stessa altezza.

Nel Codice 4.5 richiediamo che Decomponibile(u) sia una coppia di valori, in

cui il primo è TRUE se e solo se T(u) è completamente bilanciato, mentre il secondo è

l'altezza di T(u) (calcolata come nel Codice 4.2). La regola Ricombina diventa quindi

quella riportata di seguito.

Page 135: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 135/373

 

CompletamenteBilanciatoC u ):

IF (u == nuli) {

RETURN <TRUE, ~1>;

> ELSE {

<bilSX,altSX> = CompletamenteBilanciatoC u.sx );

<bilDX,altDX> = CompletamenteBilanciatoC u.dx );completamenteBil = bilSX && bilDX && CaltSX == altDX);

altezza = maxCaltSX, altDX) + 1;

RETURN <completamenteBil,altezza»;

 } [post: restituisce T R U E come prima componente «-• T(u) è completamente bilanciato)

Codice 4.5 Algoritmo ricorsivo per stabilire se un albero binario è completamente bilanciato.

• La prima componente di De co mp on ib il e( u) è TRUE se e solo se lo sono le primecomponent i di Decomponibi lefus ) e di De c omp oni b i l e ( u D ) e se le secondecomponenti sono uguali (riga 7).

• La seconda comp onen te di D e c o m p o n i b i l e ( u ) è uguale al massimo tra le secon-de component i di Decomponibi lefus ) e di De co mp on ib i l ef uo ) incrementa-to di 1 (riga 8)

ALVIE: albero binario completamente bilanciato

Osserva, sperimenta e verifica

FullyBalancedTree

Per completare il quadro dello schema generale riportato nel Codice 4.3, discutiamoun algoritmo ricorsivo su alberi binari, in cui le chiamate non solo raccolgono informa-zione dai sottoalberi, ma propagano simultaneamente informazione proveniente dagliantenati , passando opportu ni paramet ri alle chiamate. Un problema di questo tipo ri-

guarda l'identificazione dei nodi cardine. Da to un nod o u, sia p u la sua profondità eh.u l'altezza di T(u). Diciamo che u è un nodo cardine se e solo se p u = b u . Vogliamoprogettare un algoritmo ricorsivo che stampi il contenuto di tutti i nodi cardine presentiin un albero binario.

In questo caso, possiamo presumere che D ec o m p o n i b il e (u ) = h u , analogamenteal Codice 4.1: tuttavia, al momento di invocare la chiamata ricorsiva su u dobbiamogarantire di passare p u come paramet ro. Il Codice 4.6 ha quind i due parametri in in-gresso per questo scopo: il primo indica il nodo corrente e il secondo la sua profondità.

Cj r-:©

Page 136: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 136/373

 

Cardine ( u, p ) : (pre: p èia profondità di u)IF (u == nuli) {

RETURN -1 ;

> ELSE {

altezzaSX = Cardine( u.sx, p+1 );

altezzaDX = Cardine( u.dx, p+1 );altezza = max( altezzaSX, altezzaDX ) + 1;

IF (p == altezza) PRINT u.dato;

RETURN altezza;

 }• (post: stampa i nodi cardine di T(u))

Cod ice 4.6 Algoritmo ricorsivo per individuare i nodi cardine in un albero binario. La chiamatainiziale ha come parametri la radice e la sua profondità pari a 0.

Inizialmente, questi parametri sono la radice r dell'albero e la sua profondità p r = 0.Le successive chiamate ricorsive provvedono a passare i parametri richiesti (righe 5 e 6):ovvero, se il nodo corrente ha profondità p, i figli avranno profondità p + 1. La verificache la profondità sia uguale all'altezza nella riga 8 stabilisce infine se il nodo correnteè un nodo cardine: in tal caso, la sua inf ormazione viene stampata. Da notare che lacomplessità temporale dell'algoritmo rimane O(n) in quanto si tratta di una semplicevariazione della visita posticipata implicitamente adottata nel Codice 4.3.

A L V I E : nodi cardine di un albero binario

Osserva, sperimenta e verifica

HingeNode

4.1.2 Inserimento e cancellazione

Analogamente alle liste, gli alberi binar i si prestano a essere mantenut i dinamicamentein modo efficiente. Osserviamo che una lista può essere rappresentata come un alberodegenere in cui i nodi u hanno uno dei due campi, u . s x oppu re u. d x , sempre ugualea n u l i . La testa della lista coincide quind i con la radice dell'albero degenere. Ne derivache l'altezza di un albero binario può essere h. = n — 1 in tali casi degeneri (confrontiamoquesto con il valore di h. = O(logn) nel caso di alberi bilanciati).

Nel seguito, ipotizziamo che ogni nodo contenga il riferimento al padre (in tal caso,l'albero degenere è una lista doppia). L'operazione dinamica più semplice è quella di

Page 137: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 137/373

 

p = u . p a d r e

z . s x = u ;

z . d x = n u l i ;

u . p a d r e = z ;

z . p a d r e = p ;

IF ( p = = n u l i ) {r = z ;

> E L S E I F ( u = = p . s x ) {

p . s x = z ;

> EL SE {

p . d x = z ;

>

Codice 4. 7 Inserimento di un nuovo padre z (con il campo z.dato già inizializzato) del nodo u,che ne diventa fig lio sinistro. Gli assegnamenti nelle righe 2-3 vanno invertiti per

rendere u figlio destro di z.

inserire o cancellare un figlio. Sostituiamo un figlio di u, per esempio quello sinistro,con il nuovo figlio v a cui va aggiornato il riferimento al padre. Il seguente frammento dicodice descrive un inserimento se u. sx è vuoto prima delle operazioni descritte, mentredescrive una cancellazione se u. sx non è vuoto (supponiamo che i campi v. dato, v. sxe v.dx siano stati già correttamente inizializzati dall'applicazione di riferimento).

u.sx = v;IF (v != nuli) v . p a d r e = u;

Un'operazione analoga è quella di inserire un nuovo padre z per un nodo u, ipo-tizzando di avere anche il riferimento r alla radice dell'albero binario. Ai fini della di-scussione, supponiamo che il nuovo padre abbia u come figlio sinistro (non è difficilemodificare le righe 2-3 del Codice 4.7 per rendere u un figlio destro), e che il campoz. dato sia già stato correttamente inizializzato dall'applicazione di riferimento.

Nel Codice 4.7, stabiliamo prima il vecchio padre di u, indicato con p nella riga 1(p è n u l i quando u è la radice r dell'albero binario) . Nelle righe 2—4, u diventa figliosinistro di z, il nuovo padre. Le righe rimanenti rendono p (o il riferimento r se p èn u l i ) padre di z. In particolare, il confronto nella riga 6 permette di stabilire se r è ilpadre di z, mentre quello nella riga 8, permette a z di prendere il posto di u come figliodi p. In ogni caso, il costo totale dell'inserimento è 0( 1) tempo.

Infine, mostr iamo la cancellazione del padre di u quando u è figlio unico. In talcaso, le operazioni da eseguire sono riportate nel Codice 4.8, dove presumiamo che ilpadre di u non sia n u l i (altrimenti non procediamo con la cancellazione). Dopo aver

Page 138: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 138/373

 

p = u.padre;pp = p.padre;u.padre = pp;IF (pp == n u l i ) {

r = u;

} ELSE IF (p == pp . sx) {pp.sx = u;} ELSE {

pp.dx = u;>

Codice 4.8 Cancellazione del padre p del nodo u (dove p è diverso da n u l i e il frate llo di u èuguale a nuli).

individuato il nonno di u nelle prime due righe ed aver aggiornato in u il riferimento alnuovo padre, nella terza riga, le righe successive stabiliscono se il padre di u sia la radice0 meno. Se lo è, allora u diviene la nuova radice, altrimenti u prende il posto del padrecome figlio del nonno: osserviamo come le righe 3-10 siano analoghe alle righe 5-12del Codice 4.7. Anche in questo caso, il costo è O( 1 ) tempo.

Per concludere, osserviamo che altre operazioni dinamiche sono possibili, ma questevanno discusse contestualmente all'applicazione di riferimento.

4.2 Opus libri: minimo antenato comuneIn molte situazioni reali, i dati che devono essere elaborati sono statici (non subisco-no modifiche nel corso del tempo) e possono quindi essere organizzati in un'opportunastruttura di dati in modo tale che le successive richieste siano efficientemente eseguite.In altre parole, siamo disposti a pagare un prezzo, in termini di tempo, per pre-elaborare

1 dati a disposizione (preprocessing), per poi rispondere molto velocemente a future ri-chieste relative ai dati stessi (query). In questo paragrafo vediamo un esempio di taleapproccio per la risoluzione di un noto problema su alberi.

Dato un albero degli antenati, una domanda naturale consiste nel determinare chisia il primo erede comune di due qualunque persone presenti nell'albero: facendo rife-rimento alla Figura 4.2, ad esempio, il primo erede comune di Marmadoc Brandibucke di Adamanta Paffuti è Primula Brandibuck, mentre il primo erede comune di DrogoBaggins e Mirabella Tue è Frodo Baggins. Rispondere a tale domanda è equivalente asaper calcolare, dati due nodi u. e v di un albero, l'antenato comune di u e v che si trovapiù lontano dalla radice dell'albero: tale antenato è comunemente denominato minimoantena to comune. Il Codice 4.9 riporta le istruzioni necessarie a identificare tale an-

Page 139: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 139/373

 

MinimoAntenatoComune ( u, v ) : {pre: Il campo prof contiene la profondità) WHILE (u.prof != v.prof) {

IF (u.prof > v.prof) {

u = u.padre;

> ELSE {

v = v.padre;>

>IF (u == v) RETURN U;

 WHILE (u.padre != v.padre) {u = u.padre;v = v.padre;

>

RETURN u . p a d r e ;

Codice 4.9 Ricerca del minimo antenato comune tra due nodi u e v.

fenato. A tal fine, ipotizziamo che ogni nodo sia dotato di un ulteriore campo prof 

contenente la sua profondità nell'albero (abbiamo visto nel paragrafo precedente come

calcolare, per ogni nodo dell'albero, il valore di tale campo). Dopo aver risalito parte del

cammino dal nodo di profondità maggiore verso la radice, i due nodi correnti si trovano

alla stessa profondità (righe 2—8). A questo punto, abbiamo due possibilità:

1. abbiamo già raggiunto il minimo antenato comune, in quanto un nodo era di-scendente dell'altro (riga 9);

2. dobb iamo risalire in parallelo i due cammini verso la radice fino a che il minimoantenato comune risulti essere il padre di entrambi i nodi correnti (righe 10-13).

Il costo temporale di tale algoritmo è proporzionale alla massima profondità tra u

e v, per cui il costo al caso pessimo è O(h) tempo per un albero di altezza h.

Uno scenario più interessante si presenta quando vogliamo rispondere a molte ri-chieste di ricerca del mi ni mo ante nato comune. Ad esempio, un tale scenario si pre-senta nelle scienze economiche e manageriali per ottimizzare il flusso di informazioni

nell'organizzazione delle reti gerarchiche (di comunicazione, sociali ed economiche).Tali reti sono rappresentate come alberi nella loro struttura principale e i nodi han-

no importanza diversa in base alla quantità di informazione che devono scambiare conil resto dei nodi (tale importanza non è necessariamente collegata all'ordine gerarchicoindo tto dall'albero). Alle coppie di nodi critici, attivamente impiegate in flussi di va-ste dimensioni, vengono aggiunti degli ulteriori collegamenti per creare canali diretti dicomunicazione preferenziale, identificandone i minimi antenati comuni che sono i lorocolli di bottiglia.

Page 140: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 140/373

 

Eulero ( u, p ) : {pre: p è la profondità di u)IF (u != nuli) {

PRINT p;

IF (u.sx != nuli) {

Eulero( u.sx, p+1 );

PRINT p;>IF (u.dx != nuli) {

Eulero( u.dx, p+1 );

PRINT p;

>>

Codi ce 4 .1 0 Stampa ricorsiva delle profondi tà dei nodi di un albero secondo il suo ciclo Euleriano.La chiamata iniziale ha come argomenti la radice r e la sua profondità p r = 0.

Nel caso di molte richieste, è pertanto preferibile dedicare tempo polinomiale di cal-colo per effettuare un preprocessing dell'albero, così da poter rispondere successivamentealla sequenza di query sul minimo antenato comune in modo molto efficiente. Questoproblema è generalmente indicato con l'acronimo LCA (dall'inglese Least Common An-

cestor) e risulta essere uno strumento fondamentale per risolvere altri problemi, comevedremo in alcuni dei capitoli successivi. Il problema LCA è introdot to nel mo do se-guente: pre-elaborare un albero binario in tempo polinomiale così che sia possibile, datidue nodi qualunque u e v dell'albero, determinare in tempo costante il minimo antenatocomune di u e v.

Una soluzione immediata a tale problema per un albero di dimensione n e altezza h.deriva dalla costruzione di un array bidimensionale t di n righe e n colonne, tale chet[u][v] memorizza il risultato restituito da MinimoAntenatoComunefu, v). Tale fase dipreprocessing richiede 0(n 2h.) tempo. Il vantaggio è che ora ogni successiva query ri-chiede 0(1) tempo. Tuttavia, lo spazio occupato è 0(n 2 ) celle di memoria a causa delladimensione dell'array t prodotto dal preprocessing. Per ridurre tale spazio a O(nlogn)

celle, mantenendo un tempo costante di query, operiamo una trasformazione del proble-ma LCA nel problema della ricerca del minimo valore all'interno di un segmento di unarray di numeri interi.

4.2.1 Trasformazione da antenati comuni a minimi in intervalli

La tecnica di trasformare un problema computazionale TT i in un altro problema compu-tazionale n 2 è probabilmente una delle più comunemente utilizzate per progettare algo-ritmi di risoluzione. Intuitivamente, tale tecnica consiste nel mostrare come le soluzioni

Page 141: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 141/373

 

MarmadocBrandibuck

 \ 1Adaldrida

• /

GerontiusBolgeri Tue

Fi gu ra 4. 5 Cammino Eleuriano di una porzione dell'albero degli antenati della Figura 4.2.

per Fi2 possano essere efficientemente impiegate per ottenere le soluzioni di FI) (vedremonell'ultimo capitolo del libro una trattazione più formale del concetto di riduzione).

Nel caso in questione, mostriamo come trasformare LCA nel problema RMQ (dal-l'inglese Range-Min Query) così definito: pre-elaborare in tempo polinomiale un arraya di n numeri interi, così che sia possibile, dati due indici i e j, determinare in tempocostante l'indice del minimo elemento contenuto nel segmento a[i, j] (lo spazio totaledeve essere di O(nlogn) celle di memoria).

Dato un albero (ovvero un'istanza del problema LCA), definiamo il ciclo Eulerianodell'albero attraverso una visita ricorsiva dei suoi nodi come illustrato nel Codice 4.10.In tale codice, le profondità dei nodi sono stampate secondo l'ordine indicato dalle freccenella Figura 4.5 (in cui l'albero utilizzato è una porzione dell'albero degli antenati mo-

strato nella Figura 4.2): a partire dalla radice, i nodi sono visitati la prima volta quando cimuoviamo dai padri ai figli (frecce verso il basso) e sono poi visitati nuovamente quandoci muoviamo dai figli ai padri (frecce verso l'alto).

Modificando in modo opportuno il Codice 4.10, ogni qualvolta un nodo viene vi-sitato, possiamo aggiungere la sua profondità a un array di numeri interi, mantenendoal contempo un collegamento bidirezionale tra il nodo e il nuovo elemento dell'array(osserviamo che a ogni nodo dell'albero corrispondono al più tre elementi dell'array).Ad esempio, l'array risultante dalla visita dell'albero mostrato nella Figura 4.5 è illustra-

Page 142: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 142/373

 

.S •= -e •= .5

DO « oa0 a, a> o1 2 2 ^£ Q B! Q UH

0 1 2 1 0 1 2 3 2 3 2 1 2 3 2 1 0

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

Figura 4.6 L'array corrispondente al cammino Euleriano della Figura 4.5.

to nella Figura 4.6. Osserviamo che la costruzione dell'array richiede tempo lineare nelnumero di nodi dell'albero.

Una volta costruito l'array, possiamo notare che il minimo antenato comune di duenodi u e v dell'albero è semplicemente il nodo con la profondità minima che si trovaall'interno del segmento delimitato dalle prime due occorrenze dei due nodi nell'array.Ad esempio, supponiamo di voler trovare il minimo antenato comune di MarmadocBrandibuck e di Mirabella Tue: la prima occorrenza nell'array di Marmadoc Brandibucksi ha in corrispondenza dell'indice 7, mentre la prima occorrenza di Mirabella Tue apparein corrispondenza dell'indice 12. Tra queste due occorrenze il nodo con profonditàminima è quello corrispondente all'elemento di indice 11: in effetti, il minimo antenatocomune di Marmadoc Brandibuck e di Mirabella Tue è Primula Brandibuck. In modoanalogo, possiamo verificare che il minimo antenato comune di Ruby Bolgeri (indice 2)e Marmadoc Brandibuck (indice 7) è Frodo Baggins (indice 4). L'osservazione è validain generale, in quanto supponendo che u venga visitato per la prima volta prima di v, iltratto di ciclo Euleriano che va da u a v attraversa una serie di nodi di cui il loro antenatocomume minimo è quello con profondità minima.

Abbiamo così dimostrato che il problema di rispondere in tempo costante a unaqualunque query di tipo LCA, è riducibile al problema di rispondere in tempo costan-te a un'opportuna query di tipo RMQ. Possiamo quindi concentrarci su quest 'ul timoproblema, ponendoci come obiettivo di mantenere lo spazio a O(nlogn) celle.

4.2.2 Soluzione efficiente in spazio

Dato un array a di n numeri interi, la soluzione quasi lineare del problema RMQ consistenel considerare solamente la famiglia dei segmenti di a aventi una potenza del 2 come

Page 143: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 143/373

 

RMQ( i , j ) :p* = p[ j - i+ l ] ;q* = q Cp *] ;

mi = b[p*] [i];m2 = b[p*] [j-q*+l] ;

IF (a[ml] < A [m2] ) {RETURN MI;

> ELSE {

RETURN M2;

>

Co di ce 4.11 Query per il problema RMQ.

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

2° 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

21 0 1 3 4 4 5 6 8 8 10 11 11 12 14 15 16

22 0 4 4 4 4 5 6 8 11 11 11 11 15 16

23 0 4 4 4 4 5 11 11 11 1624 0 4

Fi gu ra 4. 7 La tabella b per la soluzione del problema RMQ in spazio O( nlog n) .

lunghezza, e nel memorizzare il minimo elemento di ciascun segmento. Osserviamo che,per ogni indice i compreso tra 0 e n — 1, vi sono al più logn segmenti di tale famiglia,per cui lo spazio richiesto risulta essere O(nlogn) celle di memoria. A tal fine il prepro-cessing richiede la costruzione di un array bidimensionale b di logn righe e n colonne,in cui b[l][i] contiene la posizione del minimo elemento nel segmento a[i,i + 2 l — 1],dove '°g n 0 a tabella mostrata nella Figura 4.7, costruita

a partire dall'array mostrato nella Figura 4.6, mostra gli elementi minimi in corrispon-denza a segmenti che rientrano interamente nei limiti dell'array). La costruzione di brichiede tempo O(nlogn) utilizzando, per ogni l con lo gn , la seguente regoladi programmazione dinamica chiamata del raddoppio.

Caso base: poiché il minimo elemento all'interno di un segmento di lunghezza 2° = 1coincide con l'unico elemento del segmento stesso, b[0][v] = i per 0 ^ i ^ n — 1.

Passo induttivo: osserviamo che un segmento a[i, i + 2 l + 1 — 1] con l ^ 0, è interamentecontenuto nell'array a solo se i + 2 l + 1 - 1 ^ n - 1, ovvero se i ^ n - 2 l + 1 . In talcaso, il minimo elemento in a[i, i + 2 l + 1 - 1] può essere calcolato confrontando

Page 144: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 144/373

 

il minimo elemento all'interno della metà destra del segmento con il minimoelemento all'interno della metà sinistra. Quindi, b[l+l][i] = min{b[l][i], b[l][i+2 l]per 0 ^ i ^ n — 2 l + 1 .

Il preprocessing richiede la costruzione di due ulteriori array: l'array p di n numeriinteri, tale che p[x] = l se e solo se 2 l è la più grande potenza del 2 minore o ugualea x (in altre parole, p[x] = [logxj ), e l'array q di logn numeri interi, tale che q[l] = 2 l .Il tempo totale e lo spazio per il preprocessing sono quindi O (n logn ) (ma è possibileridurre lo spazio a O(n) celle di memoria).

Per gestire una query di RMQ relativa a un segmento a[i, }] di lunghezza x = j — i + 1,calcoliamo p* = p[x] e q* = q[p*] = 2 P* in tempo costante, e troviamo i due indicittl] = b[p*][i] e m.2 = b[p*][j — q* + 1] che indicano la posizione degli elementi miniminei due corrispondenti segmenti di lunghezza q* che ricoprono a[i, j] (un segmento èa[i, i +q * — 1] e l'altro è a[j — q* + 1,j]). Come già notato, il minimo in a[i, j] è il minimotra i due elementi a[mi] e a[m2l: il Codice 4.11 si basa su tale sequenza di operazioni.Ad esempio, supponiamo di voler determinare il valore minimo contenuto all'interno delsegmento a[7,12] dell'array mostrato nella Figura 4.6: in tal caso, x = 12 — 7+1 = 6, percui p* = 2 e q* = 4 . Pertanto, possiamo consultare la tabella mostrata nella Figura 4.7per determinare l'indice del valore minimo contenuto all'interno del segmento a[7,10]:l'elemento della tabella che ci interessa si trova in terza riga (in quanto il segmento halunghezza 4) e in ottava colonna (in quanto il segmento ha inizio dall'elemento di indice7) ed è uguale a 8. Analogamente, utilizzando la tabella possiamo dedurre che l'indicedel valore minimo contenuto all'interno del segmento a[9,12] è pari a l i . Poiché a[8] =

2 > 1 = Q[1 1], concludiamo che la risposta alla nostra interrogazione è l i .

ALVIE: minimo antenato comune

<2E>

Osserva, sperimenta e verifica ® ^ ¡ ^

LeastCommonAncestor _

4.3 Visita per ampiezza e rappresentazione di alberi

Abbiamo già osservato che, in molte applicazioni, le strutture di dati utilizzate risultanoessere statiche, non subendo modif iche nel corso del tempo. Nel caso degli alberi, unesempio concreto è dato dal formato di scambio denominato XML (Extensible Markup

 Lartguage), il cui vasto utilizzo in molte applicazioni permette di memorizzare dati strut-turati. I documenti in formato XML descrivono un albero i cui nodi sono etichettati

Page 145: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 145/373

 

<genealogico>

<persona nome»"Frodo Baggins"><padre>

<persona nome="Drogo Baggins"><padre>

</padre>

<madre>

<persona nome="Ruby Bolgeri">

<padre>

</padre>

<madre>

</madre></persona>

</madre></persona>

</padre>

<madre><persona nome="Primula Brandibuck">

<padre>

</padre>

<madre>

</madre></persona>

</madre>

</persona></genealogico>

Figura 4 .8 Una rappresentazione XML dell'albero genealogico mostrato nella Figura 4.3.

con attributi e valori: per esempio, uno dei modi per esprimere la porzione di alberomostrata nella Figura 4.3 in formato XML è quello riportato nella Figura 4.8.

Questo utile formato occupa più spazio di quanto richiesto a causa della sua verbo-sità: poiché viene usato per rappresentare grandi quantità di dati, è necessario codificarloin forma compressa mantenendo la sua struttura originale (in tal modo, il risparmiodi spazio si traduce in una compressione dei dati memorizzati). Le etichette (nel nostroesempio, i nomi delle persone) sono memorizzate in una zona di memoria contigua usan-do opportune tecniche di compressione testuale (ad esempio, sfruttando la ridondanzadi un qualunque linguaggio naturale).

Per quanto riguarda, invece, la struttura ad albero, ovvero i riferimenti ai figli, èutile comprimerla a patto di simulare tali riferimenti in tempo costante: la caratteristi-ca di questa metodologia è di permettere l'attraversamento dell'albero compresso senzadecodificarne l'intera struttura ogni volta.

Page 146: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 146/373

 

In questo paragrafo consideriamo due diverse metodologie (quella implicita e quellasuccinta) di compressione per gli alberi binari statici, in cui ipotizziamo che il campou.dato (l'etichetta del nodo) occupi uno spazio prefissato di memoria indipendente-mente dal nodo u di appartenenza e che il contenuto di u . d a t o sia già forni to com-presso. In tal modo, possiamo concentrarci sul risparmio di spazio per memorizzare iriferimenti u. padre, u. sx e u. dx in forma implicita o succinta. L'idea è di allocare unalbero binario in un array secondo l'ordine derivante da una visita in ampiezza dei suoinodi (un esempio di tale visita è riportato nella Figura 4.4). Ogni nodo u viene così asso-ciato concettualmente alla sua posizione nell'array (occupata dal campo chiave u. dato).In particolare, identificheremo il nodo u con il corrispondente elemento dell'array e,per attraversare l'albero dai padri ai figli e viceversa, utilizzeremo una rappresentazio-ne indiretta dei riferimenti u. padre, u. sx e u. dx, che saranno simulati calcolando leposizioni nell'array del padre, del figlio sinistro e del figlio destro di u, rispettivamente.

Anticipiamo sin da ora, che mentre nella rappresentazione implicita (utilizzabile soloin alcune classi di alberi binari) usiamo soltanto l'array dei nodi e un numero costantedi celle di memoria aggiuntive, nella rappresentazione succinta tale array è affiancatoda ulteriori strutture di dati che richiedono 2n + o(n) bit aggiuntivi: in compenso,quest'ultima rappresentazione è applicabile ad alberi binari qualunque.

Abbiamo addotto il notevole risparmio di spazio per motivare tali rappresentazio-ni, per cui cerchiamo di quantif icarne il vantaggio. Ipotizzando che ciascun riferimen-to (u.padre, u.sx, u.dx) sia allocato in una cella di memoria (di 32 o 64 bit), larappresentazione succinta permette di sostituire i circa 100 bit (o più) occupati da tali

campi con all'incirca due bit per nodo: ne deriva un risparmio in spazio che è quasi uncinquantesimo (o più) della rappresentazione esplicita dei riferimenti! In generale, seciascun riferimento in un albero binario di dimensione n richiede logn bit (il minimonecessario per indicare uno qualunque dei nodi), la rappresentazione succinta sostituiscei 3nlogn bit usati dalla rappresentazione esplicita della struttura dell'albero, con soli2n + o(n) bit (ricordiamo che la rappresentazione implicita è superiore in prestazioni mameno generale e che i campi u. dato vengono compressi con altre tecniche).

4.3.1 Rappresentazione implicita di alberi binari

La relazione tra i nodi u di un albero binario in una rappresentazione implicita è codifi-cata completamente tramite una semplice regola matematica senza alcun uso di memoriaaggiuntiva, a parte quella necessaria alle chiavi u. dato e a un numero costante di varia-bili locali. L'albero binario completo a sinistra è l'esempio principe di albero rappresen-tabile in modo implicito. Un albero binario di altezza h. è completo a sinistra se i nodidi profondità minore di h. formano un albero completamente bilanciato, e se i nodi di (

profondità h. sono tutti accumulati a sinistra (parte sinistra della Figura 4.9). Possiamoverificare facilmente che h. = [ logn j = O( lo gn), dove n è il numero di nodi.

Page 147: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 147/373

 

Figura 4. 9 Rappresentazione implicita di un

padre

i0 1 2 3 4 5 6 7 8 9

FB D B P B FB RB G BI  -

£   L B Ts X Y

1

sx

dx

completo a sinistra.

Dato un albero binario completo a sinistra possiamo memorizzare in un array i suoi

nodi, effettuando una visita per ampiezza operata a partire dalla radice, come mostratonel Codice 4.12: la caratteristica di tale visita è che essa memorizza i nodi in ordinecrescente di profondità p nell'array nodo (la cui lunghezza presumiamo che sia ugualealla dimensione dell'albero binario), a partire dalla radice: inoltre, i nodi di profondità psono memorizzati a partire dalla posizione ultimo + 1, procedendo da sinistra verso de-stra. Dopo aver memorizzato la radice dell'albero (righe 3—4), iteriamo fintanto che nonvi sono più nodi da visitare (riga 5). Preso il nodo nella posizione a t t u a l e (riga 6), ag-giungiamo nelle posizioni immediatamente successive a quella indicata da ultimo i suoifigli non vuoti (righe 7-14). Per dimostrare che tutti i nodi sono visitati, osserviamo che

ogni qualvolta un nodo viene visitato, i suoi figli sono memorizzati in nodo e, da quelmomento in poi, ultimo è maggiore oppure uguale alla loro posizione nell'array. Quin-di, se la condizione della riga 5 non è verificata, allora non vi sono più nodi da visitare.Not and o inoltre che, a ogni iterazione del ciclo w h i l e , il valore di a t t u a l e aumenta di1, abbiamo che il costo è O(n) tempo, dove n indica la dimensione dell'albero.

La Figura 4.9 riporta un esempio di tale memorizzazione dei nodi di un albero bina-rio completo a sinistra in un array: osserviamo che i soli campi u . d a t o sono effettiva-mente memorizzati. Pur avendo ignorato i campi u.padre, u.sx e u.dx, la relazionegerarchica viene comunque preservata: è infatti sufficiente esaminare, nell'array, la posi-

zione corrispondente a ciascun nodo. La radice occupa la posizione i = 0. In generale,se l'albero ha dimensione n e un suo generico nodo u occupa la posizione i, possiamoassociare le posizioni dell'array ai tre riferimenti u. sx, u. dx e u. padre con la regola:

• il figlio sinistro occupa la posizione 2i + 1 (se 2v + 1 ^ n , allora u . sx = n u l i ) ;

• il figlio destro occupa la posizione 2i + 2 (se 2i + 2 ^ n , allora u. dx = n u l i ) ;

• il padre occupa la posizione |_(i — 1 )/2J (se i = 0, allora u . p a d r e = n u l i ) .

Page 148: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 148/373

 

Implicita( u, nodo ):

(pre: \iè la radice dell'albero e la lunghezza di nodo è uguale al numero dei suoi nodi)ultimo = attuale = 0;

nodo[attuale] = u;

 WHILE (attuale <= ultimo) {

u = nodo[attuale];IF (u.sx != nuli) {

nodo[ultimo+1] = u.sx;

ultimo = ultimo +1;

>IF (u.dx ! = nuli) -C

nodo[ultimo+1] = u.dx;

ultimo = ultimo +1;

}attuale = attuale+1;

>

Cod ice 4. 12 Memorizzazione in un array dei nodi di un albero binario completo a sinistramediante una visita per ampiezza.

La navigazione nell'albero da un nodo ai suoi figli, e viceversa, richiede quindi tem-po costante come nella rappresentazione esplicita. Nell'ottica del risparmio di memoria,la rappresentazione implicita è preferibile perché usa soltanto 0(1) celle di memoria

aggiuntive, quindi O(logn) bit, oltre allo spazio necessariamente richiesto per la me-morizzazione dei campi u . d a t o . Osserviamo che tale rappresentazione non può essereusata per alberi binari qualunque (a meno di non sfruttare una qualche relazione tra lechiavi dei campi u. dato), come possiamo dedurre dall'albero mostrato nella parte sini-stra della Figura 4.10 . In base alla regola matemat ica appena esposta, l'unico figlio delnodo in posizione 1 dovrebbe occupare la posizione 4, mentre esso viene memorizzato inposizione 3 dalla visita in ampiezza realizzata dal Codice 4.12: in questo caso, la regolanon vale cosi come è formulata, ma è possibile raffinare l'idea mediante l'introduzionedella rappresentazione succinta.

A L V I E : rappresentazione implicita

®

® ® ® "<s>® ® ®Osserva, sperimenta e verifica

ImplicitRepresentation

Page 149: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 149/373

 

( P B )

0 1 2 3 4 5 6 7 8 9 10 11 12

1 1 1 0 1 0 0 1 1 0 0 0 0

F B D B P B - R B X Y Z Y

1FbDbPbRBXYZY ,1 1 1 0 1 0 0 1 1 0 0 0 0nodo[0,n-l].dato pieno[0,2n]

Figura 4.1 0 Rappresentazione succinta per ampiezza di un albero binario con n nodi.

4.3.2 Rappresentazione succinta per ampiezza

Nell'ottica del risparmio di memoria per un albero binario qualunque, utilizziamo unarappresentazione succinta, il cui scopo è quello di rappresentare la struttura dell'albero(senza sfruttare proprietà particolari delle chiavi) utilizzando 2n + o(n) bit in aggiuntaallo spazio occupato dai campi u . d a t o . In particolare, riprendendo l'esempio mostratonella Figura 4.10, modifichiamo il Codice 4.12 in modo che produca un ulteriore arraybinario pieno contenente i bit pari a 1 in corrispondenza dei nodi dell'albero e i bit paria 0 in corrispondenza dei riferimenti n u l i (tale modifica è illustrata nel Codice 4.13).

Come possiamo osservare nell'esempio nella Figura 4.10, esiste una corrispondenza

biunivoca tra i nodi dell'albero e i bit pari a 1 in p i en o : ricordando che nodo[i] memo-rizza l'(i + 1)-esimo nodo u visitato per ampiezza (0 ^ i < n — 1), abbiamo che essocorrisponde al l'fi + 1 )-esimo 1 nell'array pieno e viceversa. In base a tale corrisponden-za, l'array pieno permette di stabilire la seguente regola, analoga a quella introdotta perla rappresentazione implicita:

1. se un nodo occupa la posizione i nell'array nod o, allora i bit corrispondenti aidue figli occupano le posizioni 2i + 1 e 2i + 2 nell'array pieno;

2. se un nodo occupa la posizione i nell'array no do , allora p ieno[2 i + 1] = 1 se solose il riferimento al suo figlio sinistro non è n u l i e p ieno [2 i + 2] = 1 se solo se ilriferimento al suo figlio destro non è nuli.

Tale regola può essere verificata per ispezione diretta nella Figura 4.10. Per convin-cerci della sua correttezza in generale, osserviamo che l'invariante mantenuta dal ciclow h i l e nel Codice 4.13 è che il valore di u l t i m o P i e n o è pari a due volte il valore dia t t u a l e : chiaramente ciò è vero prima dell'inizio del ciclo (poiché entrambi i valori so-no 0); inoltre, l'invariante è mantenu ta a ogni iterazione, in quanto il valore di a t t u a l e

suoi

Page 150: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 150/373

 

SuccintaAmpiezza( u, nodo, pieno ):

(pre: u radice di albero di n nodi; lunghezze di nodo e pieno sono n e 2n + 1)

ultimoNodo = ultimoPieno = 0;

nodo[ultimoNodo] = u;

pieno[ultimoPieno] = 1;

attuale = 0; WHILE (attuale <= ultimoNodo) {

u = nodo[attuale];

IF (u.sx != nuli) {

nodo[ultimoNodo+1] = u.sx;

ultimoNodo = ultimoNodo + 1;

pieno[ultimoPieno +1] =1;

> ELSE {

pieno[ultimoPieno +1] =0;

>

IF (u.dx != nuli) {nodo[ultimoNodo+1] = u.dx;

ultimoNodo = ultimoNodo + 1;

pieno[ultimoPieno +2] = 1;

> ELSE {

pieno[ultimoPieno +2] =0;

>attuale = attuale + 1;

ultimoPieno = ultimoPieno + 2;

Codic e 4. 13 Rappresentazione succinta per ampiezza di un albero binario.

è incrementato di 1 e quello di u l t i m o P i e n o è incrementato di 2. Da ciò deriva chese un nodo occupa la posizione a t t u a l e in no do , allora i bit corrispondenti ai suoidue figli occupano nell'array p i e n o le posizioni u l t i m o P i e n o + 1 = 2 x a t t u a l e + 1(riga 12 o 14) e u l t i m o P i e n o + 2 = 2 x a t t u a l e + 2 (riga 19 o 21). Abbiamo cosìdimostrato il punto 1 della suddetta regola.

Per dimostrare il punto 2, osserviamo che, quando un nodo u. viene esaminato dallavisita in ampiezza (riga 8), se un figlio di u esiste, allora il bit a esso corrispondente èposto a 1 (riga 12 o 19), altrimenti tale bit è posto a 0 (riga 14 o 21).

La regola suddetta permette di navigare da un nodo u ai suoi figli e viceversa, mante-nendo i soli campi u . d a t o in nodo e associandoli ai corrispettivi bit pari a 1 in p i e n o .Per poter passare in tempo costante dalle posizioni di nodo a quelle dei corrispondenti 1in pieno e viceversa, utilizziamo due funzioni basilari per le strutture di dati succinte,una inversa dell'altra, definite su un array b di m bit (nel nostro caso, b è l'array pieno

Page 151: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 151/373

 

Select

1

LFB D B P B RB XY ZY , 1 1 1 0 1 0 0 1 1 0 0 0 0

nodo pieno

Rank

Figura 4.11 Uso di Rank e S e lec t per mettere in corrispondenza gli elementi di nodo con icorrispettivi bit in pieno.

e m = 2n + 1 come mostrato nella Figura 4.11):

• Ra nk (b ,i ) = numer o di 1 presenti nel segmento b[0,i], per 0 ^ i ^ m — 1;

• S e l e c t ( b , i) = posizione dell'(i + 1 )-esimo 1 in b, per 0 ^ i < Rank(b, m — 1 ).

Il Codice 4.14 mostra come navigare nell'albero binario utilizzando l'array nodo,

le funzioni Ran k e S e l e c t applicate all'array p i e n o e la regola discussa prima. Peresempio, per identificare la posizione nell'array nodo del figlio sinistro di nodo[i], primaidentifichiamo la sua posizione f = 2i+ 1 nell'array pieno; poi, usiamo Rank(pieno, f)per vedere quanti 1 sono presenti nel segmento pieno[0, f], deducendo che il figlio sini-stro si trova in nodo[Rank(pieno, f) — 1], Per trovare il padre di nodo[i], è sufficienteidentificare la posizione p = Selectfpieno, i) del bit 1 corrispondente a nodo[i] nel-

l'array pieno; invertendo la regola, otteniamo che nodo[[(p — 1)/2J] contiene il padredi nodo[i].Per calcolare lo spazio aggiuntivo rispetto a quello occupato dall'array nodo, dob-

biamo conteggiare i 2n + 1 bit necessari per memorizzare l'array p i e n o , e lo spa-zio necessario per realizzare le funzion i Rank e S e l e c t , che ora mostreremo essereO(nloglogn/logn) = o(n) bit. Quindi, la rappresentazione succinta utilizza un totaledi 2n + o(n) bit aggiuntivi per la rappresentazione di un qualunque albero binario.

4.3.3 Implementazione di rank e select

La funzione Rank per un array b di m bit può essere implementata usando o(m) bit ag-giuntivi: per illustrare il me todo adotta to, consideriamo ad esempio un array di m = 256bit, i cui primi 13 bit corrispondano a quelli dell'array pieno riportato nella Figura 4.10.Partiamo dalla rappresentazione esplicita di Rank, tabulando i suoi valori in un array:

b[i] 1 1 1 0 1 0 0 1 1 0 0 0 0 0 0 0i 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 •••

Rank 1 2 3 3 4 4 4 5 6 6 6 6 6 6 6 6 •••

Page 152: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 152/373

 

IndiceFiglioSinistro( i ):

f = 2 x i + 1;

IF (pieno[f] == 0) {

RETURN nuli;

> ELSE {

RETURN Rank( pieno, f ) - 1;}

IndiceFiglioDestro( i ):

f = 2 x i + 2;

IF (pieno[f] == 0) {

RETURN nuli;

> ELSE {

RETURN Rank( pieno, f ) - 1;

>

IndicePadre( i ):IF (i == 0) {

RETURN nuli;

> ELSE {

p = Select( pieno, i );

RETURN (p - 1) / 2;

>

Cod ice 4. 14 Simulazione dei riferimenti sx, dx e padre per nodo[i]. Tali riferimenti, se diversi danuli, restituiscono la posizione del corrispondente elemento nell'array nodo.

Tale rappresentazione permette un tempo costante di calcolo, ma richiede un'occu-pazione di memoria eccessiva, in quanto cont iene m interi di logm bit ciascuno. Perridurre lo spazio, partizioniamo b in segmenti elementari di k = jlogm bit ciascuno(k = 4 nell'esempio). Inoltre, mante nia mo un solo valore di Ran k per ogni segmentoelementare, cosi che dobbiam o memorizzare solo m/ k = 2 m / log m interi di log m bitciascuno in un array r', per un totale di 2m bit:

r'[j] 3 5 6 6

j 1 2 3 4 ...

b[i] 1 1 1 0 1 0 0 1 1 0 0 0 0 0 0 0 •••

i 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 •••

Rank 1 2 3 3 4 4 4 5 6 6 6 6 6 6 6 6

Se dobbiamo restituire uno dei valori in r', possiamo chiaramente farlo in tempo co-stante. Ma cosa succede se dobbiamo restituire un valore Rank(b,i) non "campionato"?Usiamo la proprietà che ogni valore non campionato può essere espresso come la som-

Page 153: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 153/373

 

ma del valore campionato più vicino a partire da sinistra (ovvero, r ' [i/k] supponendoche r'[0] = 0) e del numero di bit pari a 1 nella porzione di segmento elementare checontiene b[i]. Per esempio,

Rank( b,6) = r' [l ] + (numero di 1 nei primi tre bit di b[4,7]) = 3 + 1 = 4

Per calcolare il numero di 1 nei primi bit di ciascun segmento elementare, costruiamouna volta per tutte un array bidimensionale c, le cui colonne corrispondono a tutti ipossibili 2 k segmenti elementari di k bit e le cui righe corrispondono a tutte le possibilik posizioni all'interno di un segmento elementare. Questo array contiene tutte le rispostealle richieste che possono essere eseguite sui primi bit di un segmento elementare, comemostrato nella Figura 4.12: usando c, possiamo dunque calcolare Rank(b,6) = r'[l] +c[2][9] = 3 + 1 = 4, in quanto il segmento b[4,7] = 1, 0 ,0 ,1 corrisponde alla colonna 9e i suoi primi tre bit terminano nella posizione j = 2.

L'osservazione fondamentale, nota come trucco dei quattro russi, è che non ci possonoessere troppi segmenti elementari distinti: precisamente, il loro numero è 2 k = x/rrT- Nederiva che l'array c contiene soltanto k x 2 k = CH^TU logm) elementi di O(logk) bit,e quindi utilizza o(m) bit. A questo punto, per calcolare Rank(b,i) in tempo costanteusando solo r', c e i segmenti elementari di b, è sufficiente restituire r'[q] + c[r][s], doveq e r sono il quoziente e il resto della divisione tra i e k, e s rappresenta il (q + 1)-esimo segmento elementare di b. Mentre è ovvio che q = [_i/k.J e r = i — kq possonoessere calcolati in tempo costante, non altrettanto facile è calcolare s in tempo costante

(in effetti, se eseguissimo un semplice algoritmo di conversione da binario a decimale,questo richiederebbe tempo O(k) = O(logm)). Possiamo a tale scopo sostituire b con unvettore b' contenente m/k interi tale che b'[j] è uguale al numero intero rappresentatodalla sequenza di bit del (j + l)-esimo segmento di b (notiamo che 0 ^ b'[j] < V / TTI):

r'[j] 3 5 6 6 j 1 2 3 4

b'[j] 14 9 8 0i 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 •••

Rank 1 2 3 3 4 4 4 5 6 6 6 6 6 6 6 6

Pertanto, Rank(b,i) = r'[q] + c[r][b'[q + 1]]: ad esempio, Rank(b,6) = r'[l] +c[2][b'[2]] =r'[l] + c[2][9] =4.

In realtà abbiamo ancora troppi valori tabulati in r', in quanto questi richiedono 2mbit in totale. Introduciamo allora un ulteriore livello di campionamento, tabulando solo

un valore di Rank ogni j log2 m bit consecutivi, memorizzando tali valori in un array r"di soli 8m/log 2 m x logm = 0(m/logm) bit (la scelta di g è semplicemente dovuta alfatto di poter proseguire in questo modo l'esempio).

Page 154: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 154/373

 

O — ' O — O O ~ o — O —. o —' OO O — ' — O O — ' — O O — « O O — 'O O O O — . —. — o O O O —' — —O O O O O O O O — I — —H = 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1

h = 1 0 0 0 0 1 1 1 1 1 1 1 1 2 2 2 2h = 2 0 0 1 1 1 1 2 2 1 1 2 2 2 2 3 3h = 3 0 1 1 2 1 2 2 3 1 2 2 3 2 3 3 4

Figura 4. 12 L'array c per il numero di 1 contenuti nelle prime h. posizioni di tutti i possibilisegmenti elementari.

A questo punto, partizioniamo b in blocchi da g log 2 m bit e div idi amo ogni bl occo

in segmenti elementari, costruendo l 'array r' a esso relativo (in altre parole, è come se

avessimo un array T ' per ogni blocco):

r " 5 6r ' 3 5 1 1

Rank 1 2 3 3 4 4 4 5 6 6 6 6 6 6 6 6

b 1 1 1 0 1 0 0 1 1 0 0 0 0 0 0 0 •••i 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

Ne deriva che le somme parziali in r' hanno valore al più g log 2 m e, qu in di , cias cun

elemento d i r ' r ich iede ora so l tanto 0(loglogm) b i t : pertanto , lo spazio occupato da

r ' è O ( m l o g l o g m / l o g m ) b i t. S om ma nd o anche lo spaz io d i r " e c , o t t e n iamo o ( m)

bit in agg iun ta agli m bit richiesti da b (in realtà da b' ) . Per calcolare Ra n k ( b , i ) in

te mp o costa nte usan do b ' , r ' , r " e c, osserv iamo che i identifica un uni co blocco e

un unico segmento elementare al suo in terno: quindi , Rank(b , i ) è , come prima, pari

alla so mm a r'[q ] + c[r] [b'[ q + 1]] a cui pe rò ag gi un gi am o il valore ca mp io na to di R a n k

per il blocco corren te, ovvero r" [t ] dove t = 8 i / l o g m (nell 'esempi o, Ra nk (b , 12) =

r" [ l ] + r ' [3] + c[1][0] = 5 + 1 + 0 = 6) . Tali operazioni r ichi edono un te mp o costante

ciascuna e determinano il costo finale di un'invocazione a Rank.

L'operazione S e l e c t ha un a realizzazione a l ivelli s imile a quella di Ra nk , anch e se

leggermente più sofisticata in quanto la partizione di b in segmenti elementari è adattiva

rispetto al numero di 1 in essi contenuti.

ALVIE: rappresentazione succinta per ampiezza

Osserva, sperimenta e verifica

BreadthRepresentation

<s>®

cE> <E>

Page 155: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 155/373

 

4.3.4 Limite inferiore allo spazio delle rappresentazioni succinte

Nei paragrafi precedenti abbiamo mostrato come costruire una rappresentazione succintache richiede 2n + o(n ) bit aggiuntivi: in questo paragrafo, dimostriamo che questaquantità non è significativamente migliorabile per un qualunque albero binario, secondo

la seguente argomentazione tratta dalla teoria dell'informazione.Prendiamo un insieme di C oggetti distint i. Per indicarne uno qualunque, non

possiamo usare meno di k = flogC] bit (per esempio, abbiamo bisogno di almenok = 2 bit per distinguere C = 3 oggetti). Se così non fosse, basterebbero k' < k bitma confonderemmo due oggetti distinti poiché 2 k ' ^ < C, generando unacontraddizione: in generale, occorrono k = [log C] bit per rappresentare un qualunqueoggetto appartenente all'insieme di C oggetti.

Nel nostro caso, gli oggetti da considerare sono gli alberi binari di dimensione n e l'o-biettivo è quello di valutare il minimo numero di bit necessari per rappresentare in modo

distinto alberi diversi. Sia C n il numero di alberi binari distinti con n nodi (equivalen-temente, gli alberi binari con n nodi interni, dove ciascuno ha due figli). Tale quantitàè nota come numero di Catalan, C n = (2

T[l)/(n +1): quindi, occorrono [logC n] bitper rappresentare un qualunque albero binario con n nodi. La regola ricorsiva con cuiotteniamo C n è intuitiva: togliendo la radice da un albero binario di n nodi, i rimanentinodi costituiscono il sottoalbero sinistro di dimensione s (dove O ^ s ^ n — 1 ) e ilsottoalbero destro di dimensione n — s — 1. Ne deriva che il numero C n di alberi binaridistinti con n nodi soddisfa l'equazione ricorsiva

n— 1C n = ^ ( C s x C n _ s _ , ) (dove Co = Ci = 1) (4.1)

s=0

in quanto possiamo combinare tutti i possibili C s sottoalberi sinistri con tutti i possibiliC n _ s _ i sottoalberi destri, per ogni valore di s. La soluzione dell'equazione di ricorren-za (4.1) è il numero di Catalan C n = ( 2 ^)/(n + 1): la dimostrazione di ciò richiedetecniche di analisi combinatoria che possono essere facilmente trovate nella letteraturaspecializzata. Per valutare quindi k = [~logCn~|, osserviamo che

2n ^ _ (2n)! 2n 2n x (2n - 1 ) x (2n — 2 ) x - - - x 3 x 2 x l

n / n!n! 2 n n x n x ( n — l ) x ( n — 1 ) x • • • x 1 x 1

1 2 n x 2 n (2n - 1) x (2n - 2) 3 x 2— X X - X • • • x

2n n x n ( n - l ) x ( n — 1 ) l x l

1 ^ 2n x 2n ^ (2n — 2) x (2n — 2) ^ ^ 2 x 22 n n x n (n — l ) x ( n — 1) l x l

1 2 2 n

= — x 2 x 2 x 2 x 2 x - - - 2 x 2 = ——2n s v ' 2n

2n volte

Page 156: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 156/373

 

AnticipataC u ):

IF (u != nuli) {

PRINT u.dato;

FOR  (i = 0; i < k; i = i+1)

AnticipataC u.figlio[i] );

>

Cod ice 4 .1 5 Visita anticipata di un albero k-ario, derivata dal Codice 4.4.

dove la diseguaglianza deriva dal fatto che 2n — i > 2n — i — 1 per ogni i > 0. Conclu-diamo che k = [logC a l = l o g f ( 2 ^ ) / ( n + l ) ] > l o g [ 2 2 r v  / (2 n (n + 1))] = 2 n - 0 ( l o g n )bit sono necessari. Abbiamo quindi stabilito un limite inferiore all'occupazione in spaziodi una qualunque rappresentazione succinta di un albero binario: quella che abbiamoproposto nei paragrafi precedenti risulta dunque essere ottima a meno di un termineo(n) di ordine inferiore. Ricordiamo comunque che, per insiemi particolari di alberi bi-nari, possono esistere rappresentazioni che richiedono meno di 2u — O(logn) bit (comeosservato, ad esempio, nel caso di alberi completi a sinistra).

4.4 Alberi cardinali e ordinali, e parentesi bilanciate

Gli alberi binari finora discussi sono un caso particolare degli alberi cardinali o k-ari,

caratterizzati dal fatto che ogni nodo ha k riferimenti ai figli, i quali sono enumerati da0 a k — 1. Precisamente, un nodo u di un albero k-ario ha i campi u . d a t o e u . p a d r ecome negli alberi binari, mentre i riferimenti ai suoi figli sono memorizzati in un arrayu . f i g l i o di dimensione k, dove u . f i g l i o l i ] è il riferimento (eventualmente uguale an u l i ) al figlio i (0 ^ i < k - 1): per k = 2, abbiamo che u . f i g l i o [0] corrisponde au. sx mentre u . f i g l i o [ 1 ] corrisponde a u .d x. Per k = 3 ,4 , . . . , si ot tengono alberiternari, quaternari e così via, che possono essere definiti ricorsivamente come gli alberibinari: la maggior parte delle definizioni relative agli alberi binari si adattano facilmenteagli alberi cardinali.

Per esempio, un albero k-ario è completo se ogni nodo interno ha tutti i k figli nonvuoti: inoltre, è completamente bilanciato se tutte le foglie sono alla stessa profondità.L'altezza h. di un albero k-ario completamente bilanciato soddisfa la relazione 1 + k +k2 + k3 + • • • + k h = n sul nu mero n di nodi: qu indi , risulta h. = O(logk n) in quanto

_ k h + l — 12-1=0 ^ - k- i •

Gli algoritmi ricorsivi per gli alberi binari si estendono facilmente agli alberi k-ari.Per esempio, la visita anticipata del Codice 4.4 dà luogo al Codice 4.15 in cui, attraversoun ciclo aggiuntivo, visitiamo ricorsivamente tutti i figli del nodo corrente.

Page 157: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 157/373

 

Fi gu ra 4. 13 Due alberi binari distinti che risultano indistinguibili come alberi ordinali.

Gli alberi ordinali si differenziano da quelli cardinali, in quanto ogni nodo memo-rizza soltanto la lista ordinata dei riferimenti non vuoti ai suoi figli. Il numero di talifigli è variabile da nodo a nodo e viene chiamato grado: il grado è un intero compreso

tra 0 (nel caso di una foglia) e n — 1 (nel caso di una radice che ha i rimanenti nodi comefigli), e un nodo di grado d > 0 ha d figli che sono numerati consecutivamente da 0a d — 1. Un esempio è mostrato nella Figura 4.1 dove il grado massimo (denominatogrado dell'albero) è d = 3.

Osserviamo che gli alberi cardinali e gli alberi ordinali sono due strutture di datidifferenti, nonostante l'apparente somiglianza. Gli alberi nella Figura 4.13 sono distintise considerati come alberi cardinali, in quanto il nodo D è il figlio destro del nodo B nelprimo caso ed è il figlio sinistro nel secondo caso, mentre tali alberi sono indistinguibilicome alberi ordinali in quanto D è il primo (e unico) figlio di B.

Nel caso degli alberi ordinali, non conviene preallocare un nodo in modo da poterospitare il massimo numero di figli, essendo il suo grado variabile. Usiamo quindi la me-morizzazione binarizzata dell'albero, introducendo semplici nodi in cui, oltre al campou. dato, sono presenti anche i campi u. padre per il riferimento al padre, u. primo peril riferimento al primo figlio (quello con numero 0) e u . f r a t e l l o per il riferimeno alsuccessivo fratello nell'ordine filiare. Un esempio di memorizzazione binarizzata è mostra-to nella Figura 4.14. Osserviamo che, a differenza degli alberi cardinali in cui l'accesso aun qualunque figlio richiede sempre tempo costante, negli alberi ordinali per raggiungereil figlio i (con i > 0) è necessario O(i) tempo per scandire la lista dei figli con il seguente

frammento di codice:

  p = u.primo;

j = 0;

  WHILE ((p != nuli) && (j < i)) {

  p = p.fratello;

j = j+i;

>

Page 158: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 158/373

 

Fi gura 4. 14 Memorizzazione binarizzata dell'albero ordinale mostrato nella Figura 4.1. I riferi-menti u . f r a t e l l o sono rappresentati come frecce tratteggiate per distinguerli dairiferimenti u. primo.

Quindi la memorizzazione binarizzata dei nodi in un albero ordinale è analoga allarappresentazione degli alberi binari, ma la semantica delle due rappresentazioni è bendiversa! Allo stesso tempo, la memorizzazione binarizzata permette di stabilire l'esistenzadi una corrispondenza biunivoca tra gli alberi binari e gli alberi ordinali: ogni alberobinario di n nodi con radice r è la memorizzazione binarizzata di un distinto alberoordinale di n + 1 nodi in cui viene introdotta una nuova radice fittizia il cui primofiglio è r. Tale corrispondenza identifica il campo u. sx degli alberi binari con il campou. primo della memorizzazione binarizzata degli alberi ordinali e il il campo u. dx con il

campo u . f r a t e l l o , e viene esemplificata nella parte superiore della Figura 4.15, dovela radice fittizia è mostrata come un pallino (la radice fittizia è introdotta ai soli fini dellapresente discussione).

Un altro esempio deriva dai due alberi binari mostrati nella Figura 4.13, quandoquesti sono interpretati come la memorizzazione binarizzata di due distinti alberi ordi-nali: nel primo caso i nodi B e D sono fratelli, mentre nel secondo caso D è il primo (eunico) figlio di B.

E possibile stabilire anche una corrispondenza biunivoca tra alberi ordinali e sequen-ze di parentesi bilanciate, come mostrato nella parte inferiore della Figura 4.15, utiliz-

zando il Codice 4.16. La regola utilizzata è ricorsiva: ogni nodo u (di grado d) è associa-to a una coppia di parentesi bilanciate (• • • ) e i suoi figli sono ricorsivamente codificaticiascuno con una sequenza bilanciata di parentesi. Quindi , la codifica del sottoalberoradicato in u risulta nella sequenza di parentesi annidate

( ( • • • ) ( • • • ) • • • ( • • • ) )V v / 

d sottoalberi

dove la prima coppia di parentesi annidata corrisponde al primo figlio (e relativo sottoal-

Page 159: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 159/373

 

ParentesiOrdinali( u ):

PRINT '(';

p = u.primo;

 WHILE (p != nuli) {

ParentesiOrdinali( p );

p = p.fratello;>PRINT ')';

Codice 4 .1 6 Visita anticipata per la rappresentazione di un albero ordinale (non vuoto) mediantele parentesi bilanciate.

bero), la seconda coppia annidata corrisponde al secondo figlio (e relativo sottoalbero) e

così via (Figura 4.15) . Osserviamo che la sequenza di parentesi bilanciate così ottenutacodifica univocamente la struttura di un albero ordinale. Di conseguenza, vale la seguen-te (non ovvia) proprietà: esiste una corrispondenza biunivoca tra gli alberi binari di nnodi, gli alberi ordinali di n + 1 nodi e le sequenze bilanciate di 2n parentesi! Qu indila cardinalità di ciascuno di questi tre insiemi è data dal numero di Catalan discusso nelParagrafo 4.3.4.

4.4.1 Rappresentazione succinta mediante parentesi bilanciate

La corrispondenza tra alberi ordinali e parentesi bilanciate permette di utilizzare quest'ul-time per una rappresentazione succinta ottima usando 2n + o(n) bit (il limite inferiorebasato sul nu mero di Cata lan si applica anche a questa rappresentaz ione) . Utilizzandouna variante del Codice 4.16, i nodi dell'albero sono memorizzati in un array nodo inordine di visita anticipata e le correspondenti coppie di parentesi vengono codificate inun array binario p a r e n t e s i , dove i bit pari a 1 codificano le parentesi aperte e i bitpari a 0 codificano le parentesi chiuse. L'elemento nodo[ i] è associato alla (i + l)-esimaparentesi aperta. Nell'esempio della Figura 4.15, otteniamo la seguente configurazione

degli array no do e p a r e n t e s i (in cui viene messo in evidenza l'accoppiamento delleparentesi):

match

I n rr i ni n| • Fb DB RB XY Zy PB, 1 1 1 0 1 l o o 1 o o 1 o o ,

nodo parentesi

Page 160: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 160/373

 

X

n r n n0 1 2 3 4 5 6 7 8 9 10 11 12 13

( ( ( ) (J ( ) ) ( ) ) 1 ( 1 ) 1 )• F B D B - R b X y Z y PB

Figura 4 .15 Corrispondenza biunivoca tra un albero binario di n nodi, un albero ordinale di n + 1nodi e una sequenza di 2n parentesi bilanciate (ignorando la coppia per la radicefittizia, rappresentata con un pallino).

Oltre alle funzioni Rank e S e l e c t necessarie a passare dagli elementi dell'array no doai corrispondenti bit in p a r e n t e s i e viceversa, utilizziamo la seguente funzione:

• Mat ch (b, i) = intero j tale che i e ) sono le posizioni di una coppia di parentesi

bilanciate, per 0 ^ i, j ^ 2n — 1.Se p a r e n t e s i [ l ] = 1 e p a r e n t e s i [ r ] = 0 codificano la coppia di parentesi bilanciateche rappresenta nodo [il, allora 1 = Ma tc h( pa re n t es i , r ) e r = Ma tc h( pa re n t es i , l );inoltre, i = Ra nk ( pa re nt es i, l) e l = Se l e c t ( p a r e n t e s i , i ) . L'implementazionedi Ma tch segue la falsariga di quella di Rank e S e l e c t descritta nel Paragrafo 4.3.3,utilizzando però tecniche più sofisticate. Lo spazio totale utilizzato da p a r e n t e s i edalle implementazioni di Rank, S e l e c t e Match è pari a 2n + o(n) bit. Tale rappresen-tazione succinta è quindi adatta per la struttura dei documenti XML, che possono esserepropriamente modellati come alberi ordinali con nodi etichettati e che costituivano la

motivazione iniziale per la progettazione di rappresentazioni succinte.Per navigare all'interno della rappresentazione succinta, il Codice 4.17 illustra come

simulare i riferimenti per u = nodo[i]. Il riferimento u . p r i m o è simulato prendendola parentesi in posizione 1 + 1 , successiva a quella aperta corrispondente a nodo[i] inposizione 1 (riga 2). Se tale parentesi è chiusa (riga 3), vuol dire che il riferimento ènuli; altrimenti, la parentesi aperta in posizione 1+1 corrisponde a nodo[i+ 1], ovveroil primo figlio di nodo[i] (riga 6). Il riferimento u . f r a t e l l o è simulato calcolandola posizione r che contiene la parentesi chiusa della coppia corrispondente a nodo[i]

Page 161: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 161/373

 

IndicePrimoFiglio( i ):

1 = Select( parentesi, i );

IF (parentesi[1 + 1] == 0) {

RETURN nuli;

} ELSE {

RETURN I + 1;>

IndiceFratelloSuccessivo( i ):

1 = Select( parentesi, i );

r = Match( parentesi, 1 );

IF (parentesi[r + 1] == 0) {

RETURN nuli;

> ELSE {

RETURN Rank( parentesi, r + 1 ) - 1;

>Cod ice 4.17 Simulazione dei riferimenti primo e fratello per nodo[i]. Tali riferimenti, se diversi

da nuli, restituiscono la posizione del corrispondente elemento nell'array nodo.

(riga 3): se la parentesi in posizione r + 1 è chiusa, vuol dire che il rife rimento è n u l i ;altrimenti, essa corrisponde al fratello di nodo[i] e il numero di parentesi aperte che laprecedono indica la sua posizione nell'array nodo (riga 7).

ALVIE: rappresentazione succinta con parentesi bilanciate

Osserva, sperimenta e verifica

BracketRepresentation

La corrispondenza biunivoca, illustrata nella Figura 4.15, ci permette di rappresen-

tare anche gli alberi binari, in alternativa alla rappresentazione succinta per ampiezzadescritta nel Paragrafo 4.3.2. Notiamo che non possiamo rappresentare direttamente unalbero binario mediante parentesi perché quest'ultime non ci permettono di distinguerese un unico figlio sia sinistro o destro (come accade nella Figura 4.13). Conviene in-vece utilizzare la corrispondenza con gli alberi ordinali, secondo quanto illustrato nellaFigura 4.15. Notiamo che la rappresentazione succinta medi ante parentesi bilanciatepermette di fornire, in tempo costante, più funzionalità rispetto a quella per ampiezza,come ad esempio il calcolo della dimensione di un sottoalbero o la risposta alle query

Page 162: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 162/373

 

per il problema LCA discusso nel Paragrafo 4.2. Queste funzionalità sono rese operativeutilizzando sempre 2n + o(n) bit in totale.

RIEPILOGO

  In questo capitolo abbiamo descritto come effettuare computazioni e visite ricorsive neglialberi. Abbiamo poi mostrato una soluzione efficiente al problema del minimo antenatocomune. Infine, abbiamo considerato le classi degli alberi binari, cardinali e ordinali,mostrando come rappresentarli in modo efficiente dal punto di vista dello spazio.

ESERCIZI

1. Dimostrate per induzione che un albero binario completamente bilanciato dialtezza h. ha 2H — 1 nodi interni e 2 h foglie.

2. Dato un array a di n elementi, progettate un algoritmo che costruisca ricorsi-vamente, e in tempo 0(n), un albero binario bilanciato tale che a[i] sia l'(i +l)-esimo campo u . d a t o in ordine di visita anticipata. Considerate anche glialgoritmi per le altre visite: simmetrica, posticipata e per ampiezza.

3. Dato un albero binario, costruite un array bidimensionale m tale che, per ognicoppia di nodi u e v, l'elemento m[u] [v] contenga il minimo antenato comune diu e v. La costruzione deve seguire lo schema ricorsivo delineato nel Codice 4.3e deve richiedere tempo ottimo 0(n 2 ) (senza usare la query che richiede tempo

costante, descritta nel Paragrafo 4.2.1). Osservate che durante la ricorsione nelnodo corrente u, potete individuare quali coppie di nodi hanno u come minimoantenato comune.

4. Ipotizzando che un nodo u appaia per la prima volta prima di un nodo v lungoil ciclo Euleriano di un albero binario, dimostrate che il loro antenato comuneminimo è quello con profondità minima nel tratto di ciclo Euleriano che va da ua v (inclusi).

5. Dato un qualunque albero con n foglie, tale che ciascuno dei nodi interni ha dueo più figli, provate per induzione che il numero di nodi interni è strettamenteinferiore a n.

6. Progettate un algoritmo ricorsivo per i seguenti problemi:

• stabilire se un albero binario è completo a sinistra

• stabilire se un albero k-ario è completamente bilanciato

• calcolare il numero di foglie discendenti dalla radice di un albero ordinale.

Page 163: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 163/373

 

7. Dimostrate che l'altezza di un albero completo a sinistra di n nodi è pari a h. =O(logn), utilizzando la relazione tra l'altezza e la posizione (nell'array) dei no-di incontrati partendo dalla foglia più a sinistra e risalendo gli antenati fino allaradice.

8. Dimostrate per induzione la regola adottata nella rappresentazione implicita perassociare le posizioni dell'array ai riferimenti al padre e ai due figli e descritta nelParagrafo 4.3.1.

9. Analogamente al Codice 4.14, descrivete come simulare i riferimenti al padre e aifigli usando la rappresentazione degli alberi binari mediante parentesi bilanciate(come nella corrispondenza biunivoca nella Figura 4.15).

10. Utilizzando un array simile a quello mostrato nella Figura 4.12, mostrate come

implementare l'algoritmo per il minimo antenato comune utilizzando soltantoO(n) celle di memoria invece che O( n logn) (osservate che le profondità dei nodimemorizzate differiscono di uno).

Page 164: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 164/373

 

Capitolo 5

Dizionari

SOMMARIO In questo capitolo descriviamo la struttura di dati denominata dizionario e le operazioni daessa fornite. Mostriamo quindi come realizzare un dizionario utilizzando le liste doppie, letabelle hash, gli alberi di ricerca, gli alberi AVL, i B-alberi e, infine, i trie o alberi digitalidi ricerca, inclusi gli alberi dei suffissi e le liste invertite.

DIFFICOLTÀ

2 CFU.

5.1 Dizionari

Un dizionario memorizza una collezione di elementi e ne fornisce le operazioni di ricerca,inserimento e cancellazione. I dizionari trovano impiego in moltissime applicazioni:insieme agli algoritmi di ordinamento, costituiscono le componenti fondamentali per laprogettazione di algoritmi efficienti. Ai fini della discussione, ipotizziamo che ciascunodegli elementi e contenga una chiave di ricerca, indicata con e . c h i a v e , e che il restodelle informazioni in e siano considerate dei dati satellite, indicati con e . s a t : comeal solito, indicheremo l'elemento vuoto con n u l i . Defini to il dominio o universo Udelle chiavi di ricerca contenute negli elementi, un dizionario memorizza un insiemeS = {eo ,e i , . . . , e n _i) di elementi, dove n è la dimensione di S, e fornisce le seguentioperazioni per una qualunque chiave k e U:

• R icer ca( S , k): restituisce l'elemento e se k = e . c h i ave , oppure il valore n u l ise nessun elemento in S ha k come chiave;

• I n s e r i s c i ( S , e ) : estende l'insieme degli elementi ponendo S = S U {e}, conl'ipotesi che e. chiave sia una chiave distinta da quelle degli altri elementi in S(se e appartiene già a S, l'insieme non cambia);

Page 165: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 165/373

 

• C anc e l l a ( S , k): elimina dall'insieme l'elemento e tale che k = e . c h i a v e e poneS = S — {e} (se nessun elemento di S ha chiave k, l'insieme non cambia).

Poiché in diverse applicazioni il campo satellite e . s a t degli elementi è sempre vuoto eil dizionario memorizza soltanto l'insieme delle chiavi di ricerca distinte, l'operazione di

ricerca diventa semplicemente quella di appartenenza all'insieme:• A ppa r t i ene ( S , k): restituisce t r u e se e solo se k appartiene all'insieme S, ovveroRicer ca(S , k) / nu l i .

Il dizionario è detto statico se fornisce soltanto l'operazione di ricerca (Ricerca) eviene detto dinamico se fornisce anche le operazioni di inserimento ( I n s e r i s c i ) e dicancellazione ( Cance l l a ) . Quando una relazione di ordine totale è definita sulle chiavidell'universo U (ovvero, per ogni coppia di chiavi k e k' G U è sempre possibile verificarese vale k < k' ), il dizionario è det to ordinato: con un piccolo abuso di notazione,estendiamo gli operatori di confronto tra chiavi di ricerca ai rispettivi elementi, per cui

possiamo scrivere che gli elementi di S soddisfano la relazione eo < e\ < • < e n _ i(intendendo che le loro chiavi la soddisfano) e che, per esempio, vale k ^ et (intendendok ^ e^.chiave). II dizionario ordinato per S fornisce le seguenti ulteriori operazioni:

• Succes so r e ( S , k): restituisce l'elemento et tale che i è il minimo intero per cuik ^ e i e 0 ^ i < n , oppure il valore n u l i se k è maggiore di tutte le chiavi in S;

• P r e d e c e s s o r e ( S , k): restituisce l'elemento ei tale che i è il massimo intero percui e i ^ k e 0 ^ i < n , oppure n u l i se k è minore di tut te le chiavi in S;

• I n t e r v a l l o f S , k, k') : restituisce tutti gli elementi e € S tali che k ^ e ^ k',dove supponiamo k ^ k' , oppure n u l i se tali elementi non esistono in S;

• Rango(S, k): restituisce l'intero r che rappresenta il numero di chiavi in S chesono minori oppure uguali a k, dove 0 < r < n.

Da notare che le prime due operazioni restituiscono lo stesso valore di Ricerca(S,k)quando k e S, mentre la terza lo ottiene come caso speciale quando k = k' . Infine,la quarta operazione può simulare le prime tre se, dato un rango r, possiamo accedereall'elemento e r G S in modo efficiente.

5.2 Liste e dizionari

Le liste doppie (Paragrafo 3.1.2) sono un ottimo punto di partenza per l'implementa-zione efficiente dei dizionari. Nel seguito presumiamo che una lista doppia L abbia trecampi, ovvero due riferimenti L. i n i z i o e L . f i n e all'inizio e alla fine della lista e, inol-tre, un intero L. lunghezza contenente il numero di nodi nella lista. Utilizziamo unafunzione N u o v aL i s t a per inizializzare i primi due campi a n u l i e il terzo a 0.

Ricordiamo che ogni nodo p della lista L è composto da tre campi p . p r ed , p . succe p.dato, e faremo uso della funzione NuovoNodo per creare un nuovo nodo quandosia necessario farlo. Il campo p . d a t o contiene un elemento e G S: quindi possiamo

Page 166: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 166/373

 

InserisciCima(lista, e):

p = NuovoNodo( );

p.dato = e;

lun = lista.lunghezza;

IF (lun == 0) {

p.succ = p.pred = nuli;lista.inizio = p;

lista.fine = p;

> ELSE {

p.succ = lista.inizio ;

p.pred = nuli;

lista.inizio.pred = p;

lista.inizio = p;

>lista.lunghezza = lun + 1;

RETURN lista;

Cod ice 5.1 Inserimento in cima e in fondo a

InserisciFondo(lista, e):

p = NuovoNodo( );

p.dato = e;

lun = lista.lunghezza;

IF (lun == 0) {

p.succ = p.pred = nuli;lista.inizio = p;

lista.fine = p;

> ELSE {

p.succ = nuli;

p.pred = lista.fine;

lista.fine.succ = p;

lista.fine = p;

>lista.lunghezza = lun + 1;

RETURN lista;

lista doppia, componente dì un dizionario.

indicare con p . d a t o . c h i a v e e p . d a t o . s a t i campi dell'elemento memorizzato nelnodo corrente.

L'uso diretto delle liste doppie per implementare i dizionari non è consigliabile perinsiemi di grandi dimensioni, in quanto le operazioni richiederebbero O(n) tempo: leliste sono però una componente fondamentale di molte strutture di dati efficienti peri dizionari, per cui riportiamo il codice delle funzioni definite su di esse che sarannoutilizzate nel seguito. In particolare, nel Codice 5.1, il ruolo dell'operazione I n s e r i s c idel dizionario è svolto dalle due operazioni di inserimento in cima e in fondo alla lista,per poterne sfru ttare le potenziali tà nei dizionari che discuteremo più avanti. Per lostesso motivo, nel Codice 5.2, l'operazione Ricerca restituisce il puntatore p al nodocontenente l'elemento trovato (basta prendere p . d a t o per soddisfare le specifiche deidizionari date nel Paragrafo 5.1).

Osserviamo che le operazioni suddette implementano la gestione delle liste doppiediscussa nel Paragrafo 3.1 .2. Qu in di , la complessità delle operazioni di inser imen toriportate nel Codice 5.1 è costante indipendentemente dalla lunghezza della lista, mentrele operazioni di ricerca e cancellazione riportate nel Cod ice 5.2 richiedono O (n ) te mpodove n è la lunghezza della lista (anche se la cancellazione effettiva richiede 0 (1) avendoil riferimento al nodo, non utilizzeremo mai questa possibilità).

Un esempio di dizionario dinamico e ordinato, che abbiamo già incontrato e che sibasa sulle liste, è dato dalle liste randomizzate descritte nel Paragrafo 3.3, le cui opera-zioni richiedono un tempo atteso di O(l og n) . Nel seguito descriviamo altri dizionari

Page 167: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 167/373

 

Ricercai lista, k ):

p = lista.inizio;

 WHILE ((p != nuli) && (p.dato.chiave != k))

p = p.succ;

RETURN P;

Cancellai lista, k ):

p = Ricercai lista, k );

IF (p != nuli) {

IF (lista.lunghezza == 1) {

lista.inizio = lista.fine = nuli;

> ELSE IF (p.pred == nuli) {

p.succ.pred = nuli;

lista.inizio = p.succ;

)• ELSE IF (p.succ == nuli) {

p.pred.succ = nuli;lista.fine = p.pred;

> ELSE {

p.succ.pred = p.pred;

p.pred.succ = p.succ;

>lista.lunghezza = lista.lunghezza - 1;

>RETURN lista;

Codice 5 .2 Ricerca e cancel lazione in una lista doppia, componente di un dizionario.

di uso comune in applicazioni informatiche: notiamo che le operazioni che gestisconotali dizionari possono essere estese per permettere la memorizzazione di chiavi multiple,ossia la gestione di un multi-insieme di chiavi.

5.3 Opus libri: funzioni hash e peer-to-peer

Il termine inglese hash indica un polpettone ottenuto tritando della carne insieme adella vetdura, dando luogo a un composto di volume ridotto i cui ingredienti inizialisono mescolati e amalgamati.

Tale descrizione ben illustra quanto succede nelle funz ioni Has h : U > [0 ,m — 1],aventi l'universo li delle chiavi come dominio e l'intervallo di interi da 0 a m — 1 comecodomi nio (di solito, m è mol to piccolo se con fronta to con la dimensione di U): unafunzi one Ha sh (k ) = h. trita la chiave k € li rest ituendo come risultato un intero 0 ^

Page 168: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 168/373

 

h. ^ m — 1. Notiamo che tale funzione non necessariamente preserva l'ordine delle chiaviappartenenti all'universo LI.

Alcune funzioni hash sono semplici e utilizzano la codifica binaria delle chiavi (percui, nel seguito, identifichiamo una chiave k con la sua codifica in binario):

• Has h(k) = k % m calcola il mod ulo di k utilizzando un numero pri mo m;

• Hash(k ) = ko © k] © • • • ffi k s _ i spezza la codifica binaria di k nei blocchi ko,kj , . . . , k s „i di pari lunghezza, dove 0 < ki < m — l e l'operazione © indical'OR esclusivo.1

La seconda funzione hash è chiamata iterativa in quanto divide la chiave k in blocchi disequenze binarie ko, k[, ..., k s i e lavora su tali blocchi, di fatto ripiegando la chiavesu se stessa (i blocchi hanno la stessa lunghezza e alla chiave vengono aggiunti dei bit infondo se necessario).

Altre funzioni hash sono più sofisticate, per esempio quelle nate in ambito crittogra-fico come MD5 {Message-Digest Algorithm versione 5), inventata dal crittografo RonaldRivest ideatore del metodo RSA, e SHA-1 (Secure Hash Algorithm versione 1), introdottadalla National Security Agency del governo statunitense. Queste funzioni sono iterative elavorano su blocchi di 512 bit applicandovi un'opportuna sequenza di operazioni logi-che per manipolare i bit (per esempio, l'OR esclusivo o la rotazione dei bit) restituendocosi un intero a 128 bit (MD5) O a 160 bit (SHA-1). Ad esempio, la valutazione diMD5 (algoritmo) con la chiave "algoritmo" restituisce la sequenza esadecimale 2

446cead90f929el03816ff4eb92da6c2

mentre SHA-1 (algoritmo) restituisce

6f77f39f5ea82a55df8aaf4f094e2ff0e26d2adb

La caratteristica di queste funzioni hash è che, cambiando anche leggermente la chiave,l'intero risultante è completamente diverso. Ad esempio, MD5 (algori tmi) rest i tuisce

6a8af95d7f185bla223c5b20cc71eb4a

mentre SHA-1 (algoritmi) restituisce

1 4 7 d 4 0 1 a 6 a l e 3 c 2 0 e 7 d 6 7 9 6 b c a c 5 0 a 9 9 3 7 2 6 d 4 f aVolendo ottenere un valore hash nell'intervallo [0,m — 1] (dove m è molto minore di2128), possiamo utilizzare tali funzioni nel modo seguente.

• H ash (k ) = M D 5 ( k) % m

• Hash(k) = SHA-1 ( k ) % m

Osserviamo che, essendo la dimensione dell'universo U molto vasta, esistono sempre

due chiavi ko e k] in U tali che ko ^ k j e Hash(ko) = Ha sh (k ] ): una tale situazione è

'L'OR esclusivo tra due bit vale 1 se e solo se i due bit sono diversi (0 e 1, oppure 1 e 0): la notazioneQ © b = c indica l'OR esclusivo bitwise, in cui il bit i-esimo di c è l'OR esclusivo del bit i-esimo di a e b.

2La codifica esadecimale usa sedici cifre 0, 1, ..., 9, a, b f per codificare 24 possibili configurazionidi 4 bit.

Page 169: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 169/373

 

chiamata collisione. Tuttavia, la natura deterministica del calcolo delle suddette funzionihash, fa si che se Hash(ko) ^ Hash (k i) allora ko ^ k ( . Questa proprietà tipica dellefunzioni in generale, coniugata con la robustezza di MD5 e SHA-1 in ambito crittografico(soprattutto la versione più recente SHA-2 che restituisce un intero a 512 bit), trovaapplicazione anche nei sistemi distribuiti di condivisione dei file (peer-to-peer).

In tali sistemi distribuiti (come BitTorrent, FreeNet, Gnutella, E-Mule, Napster ecosì via), l'informazione è condivisa e distribuita tra tutti i client  o peer  piuttosto checoncentrata in pochi server, con un enorme vantaggio in termini di banda passante etolleranza ai guasti della rete: la partecipazione è su base volontaria e, al momento discaricare un determinato file, i suoi blocchi sono recuperati da vari punti della rete.Un numero sempre crescente di servizi operanti in ambiente distribuito usufruisce deiprotocolli creati per tali sistemi (ad esempio, la telefonia via Internet).

In tale scenario, lo stesso file può apparire con nomi diversi o file diversi possono

apparire con lo stesso nome. Essendo la dimensione di ciascun file dell'ordine di svariatimegabyte, quando i peer devono verificare quali file hanno in comune, è impensabileche questi si scambino direttamente il contenuto dei file: in tal modo, tutti i peer riceve-rebbero molti file da diversi altri peer e questo è impraticabile per la grande quantità didati coinvolti.

Prendiamo per esempio il caso di due peer P e P' che vogliano capire quali file hannoin comune. Non potendo affidarsi ai nomi dei file devono verificarne il contenuto e l'usodelle funzioni hash in tale contesto è formidabile: per ogni file f memorizzato, P calcolail valore SHA-1 (f ), chiamato impronta digitale {digitaifingerprint), spedendolo a P' (solo

160 bit) al posto di f (svariati megabyte). Da parte sua, P' riceve tali impronte digitali daP e calcola quelle dei propri file. Dal confronto delle impronte può dedurre con certezzaquali file sono diversi e, con altissima probabilità, quali file sono uguali.

Un altro uso dell'hash in tale scenario è quando P decide di scaricare un file f. Ta-le file è distribuito nella rete e, a tale scopo, è stato diviso in blocchi fo,fi,...,f s - i :oltre all' impronta h. = SHA-l( f) dell'intero file, sono disponibili anche le improntehi = SHA-1 (f i) dei singoli blocchi (per 0 ^ i ^ s — 1). A questo punto, dopo averrecuperato le sole impronte digitali h, ho, hi,..., h s ^i attraverso un'opportuna inter-rogazione, P lancia le richieste agli altri peer dif fondendo tali impronte . Appena ha

terminato di ricevere i rispettivi blocchi fi in modo distribuito, P ricostruisce f da taliblocchi e verifica che le impronte digitali corrispondano: la probabilità di commettereun errore con tali funzioni hash è estremamente basso.

Viste le loro proprietà, è naturale utilizzare le funzioni hash per realizzare un diziona-rio che memorizzi un insieme S = {eo, ei,..., e n _|} C U di elementi. I dizionari basatisull'hash sono noti come tabelle hash (hash map) e sono utili per implementare unastruttura di dati chiamata array associativo, i cui elementi sono indirizzati utilizzando lechiavi in S piuttosto che gli indici in [0, n — 1].

Page 170: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 170/373

 

La situazione ideale si presenta quando, fissando m = O( n), la funzione Hash. èperfetta su S, ovvero nessuna coppia di chiavi in S genera una collisione: in tal caso, ildizionario è realizzato mediante un semplice array binario t a b e l l a di m bit inizialmen-te uguali a 0, in cui ne vengono posti n pari a 1 con la regola che t a b e l l a [ h ] = 1 see solo se h. = Hash(ei. chiave) per ciascun elemento e^ dove 0 ^ i < n — 1. Essendouna funzione perfetta, Hash non necessita di ulteriori controlli: tuttavia, inserendo ocancellando elementi in S, può accadere che Hash facilmente perda la proprietà di essereperfetta su S.

Da notare che esistono dizionari dinamici basati su famiglie di hash che richiedo-no 0(1) tempo al caso pessimo per la ricerca e O ( 1 ) tempo medio ammortizzato perl'inserimento e la cancellazione.

Il limite di 0( 1) tempo per la ricerca non è in contrasto con quello di O( log n) con-fronti per la ricerca (Paragrafo 2.4.2): infatti, nel primo caso i bit della chiave k vengonomanipolati da una o più funzioni hash per ottenere un indice dell'array t a b e l l a mentrenel secondo caso k viene soltanto confrontata dalla ricerca binaria con le altre chiavi. Inaltre parole, il limite di n ( l ogn ) confronti vale supponendo che l'unica operazione per-messa sulle chiavi sia il loro confronto diretto con altre (oltre alla loro memorizzazione),mentre tale limite non vale se i bit delle chiavi possono essere usati per calcolare unafunzione diversa dal confronto di chiavi.

La costruzione dei suddetti dizionari dinamici basati sull'hash perfetto è piuttostomacchinosa e le loro prestazioni in pratica non sono sempre migliori dei dizionari chefanno uso di funzioni hash non necessariamente perfette. Questi ultimi, pur richiedendoper la ricerca 0(1) tempo in media invece che al caso pessimo, sono ampiamente diffusiper la loro efficienza in pratica e per la semplicità della loro gestione, che si riduce arisolvere le collisioni prodotte dalle chiavi. Nel seguito descriveremo due semplici modiper fare ciò: mediante liste di trabocco (che oltretutto garantiscono 0(1) tempo al casopessimo per inserire una chiave non presente in S) oppure con l'indirizzamento aperto.

5.3.1 Tabelle hash: liste di trabocco

Nelle tabelle hash con liste di trabocco (chaining), t a b e l l a è un array di m liste doppie,gestite secondo quanto descritto nel Paragrafo 5.2, e tabella[h] contiene le chiavi e

dell'insieme S tali che h. = Hash(e .ch iave) : in altre parole, le chiavi che collidonofornendo lo stesso valore h. di Hash sono memorizzate nella medesima lista, etichettatacon h. (ovviamente tale lista è vuota se nessuna chiave dà luogo a un valore hash h).

L'operazione di ricerca scandisce la lista associata al valore hash della chiave, mentrel'operazione d'inserimento, dopo aver verificato che la chiave dell'elemento non apparenella lista, inserisce un nuovo nodo con tale elemento in fondo alla lista. La cancellazioneverifica che la chiave sia presente e rimuove il corrispondente elemento, e quindi il nodo,dalla lista doppia corrispondente.

Page 171: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 171/373

 

Ricerca( tabella, k ) :

h = Hash(k);

p = tabella[h].Ricerca( k );

IF (p != nuli) RETURN p.dato ELSE RETURN nuli;

' Inserisci( tabella, e ):IF (Ricercai tabella, e.chiave ) == nuli) {

h = Hash( e.chiave );

tabella[h].InserisciFondo( e );

>

Cancellai tabella, k ):

IF (Ricercai tabella, k ) != nuli) {

h = Hash(k);

tabella[h].Cancella( k );

>

Codice 5 .3 Dizionario realizzato mediante tabelle hash con liste di trabocco.

Pur avendo un caso pessimo di 0(n) tempo (tutte le chiavi danno luogo allo stessovalore hash), ciascuna operazione è molto veloce in pratica se la funzione hash sceltaè buona: in effetti, il costo medio è 0 (1 ) tempo con l'ipotesi che la funzione Ha shdistribuisca in modo uniformemente casuale gli n elementi di S nelle m liste di trabocco.

In questo mod o, la lunghezza media di una qualunque delle liste è 0 ( ^ ), dove ^ = a èchiamato fattore di caricamento: quindi, le operazioni sulla tabella richiedono in mediaun tempo costante 0(1 + a) perché il loro costo è proporzionale alla lunghezza dellalista acceduta. Mantenendo l'invariante che m è circa il doppio di n (Paragrafo 2.1.3),abbiamo che a = 0( 1 ) e il costo delle operazioni diventa costante.

ALVIE: gestione di tabelle hash con liste di trabocco

J h ^ j ^ Osserva, sperimenta e verifica

ChainingHash

5.3.2 Tabelle hash: indirizzamento aperto

Nelle tabelle hash a indirizzamento aperto (open addressing), t a b e l l a è un array di m

celle in cui porre gli n elementi, dove m > n (quindi a < 1), in cui usiamo n u l i

Page 172: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 172/373

 

Ricerca( tabella, k ):

FOR  (i = 0; i < M; i = i+1) {

h = Hash[i](k);

IF (tabella[H] == null) RETURN -1;

IF (tabella[h].chiave == k) RETURN tabella[h];

>

Inserisci ( tabella, e ): {pre: tabella contiene ri < m chiavi)IF (Ricercai tabella, e.chiave ) == nuli) {

i = -1 ;

DO {

i = i+1;

h = Hash[i]( e.chiave );

IF (tabella[h] == nuli) tabella[h] = e;

> WHILE (tabella[h] != e);

Codice 5.4 Dizionario realizzato mediante tabelle hash con indirizzamento aperto.

per segnalare che la posiz ione corrente è vuota. Poiché le chiavi sono tut te collocatedirettamente nell'array, usiamo una sequenza di funzioni Hash[i] per 0 ^ i < m — 1,chiamata sequenza di scansione {probing), tali che i valori Hash[0](k ), Hash [l] (k) , . . . ,Hash[m — l](k) formano sempre una permutazione delle posizioni 0,1,..., m — 1, perogni chiave k € U. Tale permutaz ione rappresenta l'ordine con cui esaminiamo leposizioni di t a b e l l a dura nte le operazioni del dizionario.

Per comprendere l'organizzazione delle chiavi nella tabella hash, descriviamo primal'operazione di inserimento di un elemento. Il Codice 5.4 mostra tale operazione, in cuiiniziamo a esaminare le posizioni Hash[0](k), Hash[l](k), ..., Hash[m — l](k) fino atrovare la prima posizione Hash[i ](k) libera (poiché n < m, siamo sicuri che i < m) :tale procedimento è analogo a quando cerchiamo posto in treno, in cui andiamo avantifino a trovare un posto libero (ovviamente esaminiamo i posti in ordine lineare piuttostoche permutato). La ricerca di una chiave k segue questa falsariga: esaminiamo le suddetteposizioni fino a trovare l'elemento con chiave k e, nel caso giungiamo in una posizionelibera, siamo certi che la chiave non compare nella t a b e l l a (altrimenti l'inserimentoavrebbe posto in tale posizione libera un e lemento con chiave k). La cancellazione diuna chiave è solitamente realizzata in modo virtuale, sostituendo l'elemento con unamarca speciale, che indica che la posizione è libera durante l'operazione d'inserimento disuccessive chiavi e che la posizione è occupata ma con una chiave diversa da quella cercatadurante le successive operazioni di ricerca: quest'ultima condizione è necessaria poichéuna cancellazione che svuotasse la posizione della chiave rimossa, potrebbe ingannare

Page 173: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 173/373

 

una successiva ricerca (non necessariamente della stessa chiave) inducendola a fermarsierroneamente in quella posizione e dichiarare che la chiave cercata non è nel dizionario.Se prevediamo di effettuare molte cancellazioni, è quindi più conveniente usare unatabella con liste di trabocco perché si adatta meglio a realizzare dizionari molto dinamici.

Per la complessità temporale, osserviamo che il caso pessimo rimane O(n) tempoe che ciascuna operazione è molto veloce in pratica se la funzione hash scelta è buona:in effetti, il costo medio è 0(1) tempo con l'ipotesi che, per ogni chiave k, le posizioniin Hash[0](k), Hash[l](k), ..., Hash[m — l](k) formino una delle m! permutazionipossibili in modo uniformemente casuale. Poiché il costo è direttamente proporzionaleal valore di i tale che Hash[i](k) è la posizione individuata per la chiave, indichiamocon T(n, m) il valore medio di i: in altre parole, T(n, m) indica il numero medio diposizioni che troviamo occupate quando inseriamo un'ulteriore chiave in una tabella dim posizioni, contenente n elementi, dove n < m.

Utilizziamo un'equazione di ricorrenza per definire T(n,m), dove T( 0,m) = 1 inquanto ogni posizione esaminata è sempre libera in una tabella vuota. Per n > 0, os-serviamo che, essendo occupate n posizioni su m della tabella, la posizione esaminatarisulta occupata n volte su m (poiché tutte le permutazioni di posizioni sono equipro-babili): in tal caso, effettuiamo un solo accesso e ci fermiano su una posizione liberacon probabilità -n3^I1> mentre, con probabili tà troviamo la posizione occupata percui effettuiamo ulteriori T(n — l , m — 1) accessi alle rimanenti posizioni (oltre all'ac-cesso alla posizione occupata). Facendo la media pesata di tali costi (abbiamo già in-contrato un'analisi al caso medio di questo tipo nei Paragrafi 2.5.4 e 3.3), otteniamo

x l + ^ x (1 + T ( n - l , m - 1)), dando luogo alla seguente equazione di ricorrenza:

T(n ,m) = | | ^ i j ( n - l ,m— 1) l iniment i ( 5 > 1 )

Proviamo per induzione che la soluzione della (5.1) soddisfa la relazione T(n, m) ^™ • Il caso base per n = 0 e m > Oè immediato, mentre, per n > 0 e m > n,

abbiamo che

T(n ,m = 1 -I T u - l , m - 1 < 1 H x =

m m m — n m - nNe consegue che T(n, m) ^ = (1 — a ) - 1 = 0 ( 1 ) mantenendo l'invariante chem sia circa il doppio di n (Paragrafo 2.1.3). Da notare che i tempi medi calcolati perle tabelle hash con liste di trabocco e a indirizzamento aperto non sono direttamenteconfrontabili in quanto l'ipotesi di uniformità della funzione hash nel secondo caso èpiù forte di quella adottata nel primo caso.

Nella pratica, non possiamo generare una permutazione veramente casuale delle po-sizioni scandite con Hash[i] per 0 ^ i < m — 1. Per questo motivo, adottiamo alcune

Page 174: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 174/373

 

semplificazioni usando una o due funzioni Hash(k) e Hash'(k) (come quelle descritte

nel Paragrafo 5.3), impiegandole come base di partenza per le scansioni della tabella, la

cui dimensione m è un numero primo, in uno dei modi seguenti:

• Hash[i](k) = (Hash(k) + i) % m (scansione lineare);

• Hash[i](k) = (Hash(k) + ai2 + bi + c) % m (scansione quadratica);

• Hash[i](k) = (Hash(k) + i x (1 + Hash'(k))) % m (scansione basata su hash

doppio).

Nella scansione lineare, dopo aver calcolato il valore h = Hash(k), esaminiamo leposizioni H, h.+1, h + 2 e così via in mod o circolare. Tale scansione crea delle aggregazioni(,cluster ) di elementi, che occupano un segmento di posizioni contigue in t a b e l l a .Tali elementi sono stati originati da valori hash h. differenti, ma condividono lo stessocluster: la ricerca che ricade all'interno di tale cluster deve percorrerlo tu tto nel casonon trovi la chiave. Un più attento esame delle chiavi contenute nel cluster mostra che

quelle che andrebbero a finire in una stessa lista di trabocco (Paragrafo 5.3.1) sono tuttepresenti nello stesso cluster (e tale proprietà può valere per più liste, le cui chiavi possonocondividere lo stesso cluster). Pertanto, quando tali cluster sono di dimensione rilevante(seppure costante), conviene adottare le tabelle hash con liste di trabocco che hannoprestazioni migliori, quando sono associate a una buona funzione hash.

La scansione quadratica non migliora molto la situazione, in quanto i cluster appa-iono in altra forma , seguendo l'ordine specificato da Hash[i](k). La situazione cambiausando il dopp io hash, in qua nto l'incremento della posizione esaminata in t a b e l l adipende da una seconda funzione Hash': se due chiavi hanno una collisione sulla prima

funzione hash, c'è ancora un'ulteriore possibilità di evitare collisioni con la seconda.

A LV IE: gestione di tabelle hash con indirizzamento aperto

Osserva, sperimenta e verifica

OpenHash

5.4 Opus libri: kernel Linux e alberi binari di ricerca

Nel sistema operativo GNU/Linux, la gestione dei processi generati dai vari programmiin esecuzione è prevista nel suo nucleo {kernel). In particolare, ogni processo ha a dispo-sizione uno spazio virtuale degli indirizzi di memoria, detta memoria virtuale, in cui lecelle sono numer ate a partire da 0. La memoria virtuale fa sì che il calcolatore (utiliz-zando la memoria secondaria) sembri avere più me moria principale di quella fisicamente

Page 175: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 175/373

 

presente, condividendo quest'ultima tra tutti i processi che competono per il suo uso. Lamemoria virtuale di un processo è suddivisa in aree di memoria (VMA) di dimensionelimitata, ma solo un sottoinsieme di tutte le VMA associate a un processo risultano pre-senti fisicamente nella memoria principale. Quando un processo accede a un indirizzodi memoria virtuale, il kernel deve verificare che la VMA contenente quell'indirizzo siapresente in memoria principale: se è così, usa le informazioni associate alla VMA pereffettuare la traduzione dell'indirizzo virtuale nel corrispondente indirizzo fisico. In casocontrario, si verifica un page fault  che costringe il kernel a caricare in memoria princi-pale la VMA richiesta, eventualmente scaricando in memoria secondaria un'altra VMA(scelta, ad esempio, applicando la politica LRU discussa nel Paragrafo 3.4.2).

La ricerca della VMA deve essere eseguita in modo efficiente: a tale scopo, il kernelusa una strategia mista (tipica di Linux e applicata anche in altri contesti del sistemaoperativo), che è basata sull'uso di dizionari. Fintanto che il numero di VMA presenti inmemoria è limitato (circa una decina), le VMA assegnate a un processo sono mantenutein una lista e la ricerca di una specifica VMA viene eseguita attraverso di essa. Quando ilnumero di VMA supera un limite prefissato, la lista viene affiancata da una struttura didati più efficiente dal punto di vista della ricerca: tale struttura di dati è chiamata alberobinario di ricerca (fino alla versione 2.2 del kernel, venivano usati gli alberi AVL, mentrenelle versioni successive questi sono stati sostituiti dagli alberi rosso-neri). In effetti, glialberi binari di ricerca costituiscono uno degli strumenti fondamentali per il recuperoefficiente di informazioni e sono pertanto applicati in moltissimi altri contesti, oltre aquello appena discusso.

5.4.1 Alberi binari di ricerca

Un albero binario viene generalmente rappresentato nella memoria del calcolatore fa-cendo uso di tre campi, come mostrato nel Capitolo 4: dato un nodo u, indichiamocon u. sx il riferimento al figlio sinistro, con u. dx il riferimento al figlio destro e conu . d a t o il con tenuto del nodo , ovvero un elemento e e S nel caso di dizionari (precisa-mente, faremo riferimento a u . d a t o . c h i a v e e u . d a t o . s a t per indicare la chiave e idati satellite di tale elemento). Volendo impiegare gli alberi per realizzare un dizionarioordinato per un insieme S = (eo, ej,..., e n _i} di elementi, memorizziamo gli elementi

nei loro nodi in modo da soddisfare la seguente proprietà di ricerca per ogni nodo u:• tut ti gli elementi nel sottoalbero sinistro di u (riferito da u. sx) sono minori del-

l'elemento u. dato contenuto in u;• tut ti gli elementi nel sottoalbero destro di u (riferito da u. dx) sono maggiori di

u . d a t o .

Un albero binario di ricerca è un albero binario che soddisfa la suddetta proprietà diricerca: una conseguenza della proprietà è che una visita simmetrica dell'albero forniscela sequenza ordinata degli elementi in S, in tempo O(n).

Page 176: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 176/373

 

Ricercai u, k ):

IF (u == null) RETURN nuli;

IF (k == u.dato.chiave) {

RETURN u.dato;

> ELSE IF (k < u.dato.chiave) {

RETURN Ricercai u.sx, k );

> ELSE {

RETURN Ricercai u.dx, k );

>

Inserisci( u, e ) :

IF iu == nuli) {

u = NuovoNodoi);

u.dato = e;

u.sx = u.dx = nuli ;

} ELSE IF ie.chiave < u.dato.chiave) {u.sx = Inserisci( u.sx, e );

> ELSE IF (e.chiave > u.dato.chiave) {

u.dx = Inserisci( u.dx, e );

>RETURN U; {post: se k appare già in u, non viene memorizzata)

Codice 5.5 Algor itmi ricorsivi per la ricerca dell'elemento con chiave k e l'inser imento di unelemento e in un albero di ricerca con radice u.

La ricerca di una chiave k in tale albero segue la falsariga della versione ricorsiva della

ricerca binaria (Paragrafo 2.5). Il Codice 5.5 ricalca tale schema ricorsivo: partiamo dalla

radice dell'albero e confrontiamo il suo contenuto con k e, nel caso sia diverso, se k è

minore proseguiamo la ricerca a sinistra, altrimenti la proseguiamo a destra.

Per l'inserimento osserviamo che, quando raggiungiamo un riferimento vuoto, lo

sostituiamo con un riferimento a un nuovo nodo (righe 3—5), che restituiamo per farlo

propagare verso l'alto (riga 11) attraverso le chiamate ricorsive (abbiamo discusso tali

schemi ricorsivi nel Paragrafo 4.1.1): tale propagazione avviene notando che le chiamatericorsive alle righe 7 e 9 sovrascrivono il campo relativo al riferimento (figlio sinistro o

destro) su cui sono state invocate. Infatti se k è min ore della chiave nel nodo corrente,

l'inseriamo ricorsivamente nel figlio sinistro; se k è maggiore, l'inseriamo ricorsivamente

nel figlio destro; altrimenti , abb iamo un dupl icato e non l'inseriamo affatto. Il costo

della ricerca e dell 'inserimento è pari all'altezza h dell'albero, ovvero 0(h.) tempo.

La cancellazione dell'elemento con chiave k presenta più casi da esaminare, in quan-

to essa può disconnettere l'albero che va, in tal caso, opportunamente riconnesso per

mantenere la proprietà di ricerca, come descritto nel Codice 5.6, il cui schema ricorsivo

Page 177: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 177/373

 

Cancellai u, k ):

IF (u != nuli) {

IF (u.dato.chiave == k) {

IF (u.sx == nuli) {

u = u.dx;

} ELSE IF (u.dx == nuli) {u = u.sx;

> ELSE {

w = MinimoSottoAlbero( u.dx );

u.dato = w.dato;

u.dx = Cancella( u.dx, w.dato.chiave );

>> ELSE IF (k < u.dato.chiave) {

u.sx = Cancellai u.sx, k );

> ELSE IF (k > u.dato.chiave) {

u.dx = Cancellai u.dx, k );>

>RETURN u;

MinimoSottoAlbero( u ): {pre: u / nuli)

 WHILE (u.sx != nuli)

u = u.sx;

RETURN u;

Codi ce 5. 6 Algoritmo ricorsivo per la cancellazione dell'elemento con chiave k da un albero diricerca con radice u.

è simile a quello dell'inserimento. I casi più semplici sono quando il nodo u è una fogliaoppure ha un solo figlio (righe 5 e 7): eliminiamo la foglia mettendo a n u l i il riferi-mento a essa oppure, se il nodo ha un solo figlio, creiamo un "ponte" tra il padre di u eil figlio di li-

Quan do u. ha due figli non possiamo cancellarlo fisicamente (righe 9-1 1), ma dob-

biamo individuare il nodo w che contiene il successore di k nell'albero (riga 9), cherisulta essere il min imo del sottoalbero destro (u . d x non è n u l i ) . Sostituiamo quindil'elemento in u. con quello in w per mantenere la proprietà di ricerca dell'albero (riga 10)e, con una chiamata ricorsiva, cancelliamo fisicamente w in quanto contiene la chiavecopiata in u. (riga 11).

E importante osservare che quest'ultima s'imbatterà in un caso semplice con la can-

cellazione di w, in quanto w non può avere il figlio sinistro (altrimenti non conterrebbe

il minimo del sottoalbero destro). Come osservato in precedenza, propaghiamo l'effetto

Page 178: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 178/373

 

della cancellazione verso l'alto (riga 19) attraverso le chiamate ricorsive. Per la comples-sità temporale, osserviamo che il codice percorre il cammino dalla radice al nodo u incui si trova l'elemento con chiave k e poi percorre due volte il cammino da u al suodiscendente w. In totale, il costo rimane 0(h.) tempo anche in questo caso.

Purtroppo h = Q(n) al caso pessimo e un albero non sembra essere vantaggiosorispetto a una lista ordinata. Tuttavia, con elementi inseriti in maniera casuale, l'altezzamedia è O(logn) e i metodi discussi finora hanno una complessità O(logn) al casomedio. Vediamo come ottenere degli alberi binari di ricerca bilanciati, che hanno semprealtezza H = O( lo gn ) dopo qualu nque inser imento o cancellazione. Que sto fa sì che lacomplessità delle operazioni diventi O(logn) anche al caso pessimo.

ALV I E : ricerca, inserimento e cancellazione in alberi di ricerca

^Ry ' fi ©

Osserva, spe rimen ta e ver ifi ca © © © ©W jSSB^ © © ©BinarySearchTree % ©

5.4.2 AVL: alberi binari di ricerca bilanciati

L'albero AVL (acronimo derivato dalle iniziali degli autori russi Adel'son-Velsky e Landische lo inventarono negli anni '60) è un albero binario di ricerca che garantisce avere

un'altezza h. = O(logn) per n elementi memorizzati nei suoi nodi. Oltre alla proprietàdi ricerca menzionata nel Paragrafo 5.4.1, l'albero AVL soddisfa la proprietà di essere1-bilanciato al fine di garantire l'altezza logaritmica.

Dato un nodo u, indichiamo con h(u) la sua altezza, che indentifichiamo con l'al-tezza del sottoalbero radicato in u, dove h.(null) = — 1 (Paragrafo 4.1.1). Un nodo u è1-bilanciato se le altezze dei suoi due figli differiscono per al più di un'unità

|h.(u.sx) - h(u.dx)| s? 1 (5.2)

Un albero binario è 1-bilanciato se ogni suo nodo è 1-bilanciato. La connessione tra l'es-

sere 1 -bilanciato e avere altezza logaritmica non è immediata e passa attraverso gli alberidi Fibonacci, che sono un sottoinsieme degli alberi 1-bilanciati con il minor  numero dinodi a parità di altezza. In altre parole, indicato con Fibh un albero di Fibonacci dialtezza h e con n^ il suo numero di nodi, eliminando un solo nodo da Fib^ otteniamoche l'altezza diminuisce o che l'albero risultante non è più 1-bilanciato: nessun albero1-bilanciato con n nodi e altezza h può dun qu e avere meno di rih nodi, ossia n ^ n.h.Mostrando che n^ ^ c h per un 'oppor tuna costante c > 1, deriviamo che n ^ cH e,quindi, che h. = O( lo gn ).

Page 179: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 179/373

 

Fi bo Fib\ Fib-i

' 7 A

Figura 5.1 Alberi di Fibonacci.

Gli alberi di Fibonacci FibH di altezza h. sono defin iti ricorsivamente (Figura 5.1):per h. = 0, abbiamo un solo nodo, per cui no = 1, e, per h. = 1, abb iamo un alberocon n i = 2 nodi (la radice e un solo figlio, che nella figura è quello sinistro). Perh. > 1, l'albero Fibh è costruito prendendo un albero Fib^-i e un albero /7'£H-2> le c u i

radici diventano i figli di una nuova radice (quella di Fibh). In tal caso, abbiamo cherih = rih-1 + tvh-2 + 1) relazione ricorsiva che ricorda quella dei numeri di Fibonacci(Paragrafo 2.6.2) motivando così il nome di tali alberi.

Possiamo osservare nella Figura 5.1 che Fibo e Fib\  sono alberi 1-bilanciati di altezza0 e 1, rispettivamente, con il minimo numero di nodi possibile. Ipotizzando che, perinduzione, tale proprie tà valga per ogni i < h. con h. > 1, mostr iamo come ciò sia veroanche per FibH ragionando per assurdo. Supponiamo di poter rimuovere un nodo daFibh mantenend o la sua altezza h. e garanten do che rimanga 1-bilanciato: non pot endorimuovere la radice, tale nodo deve appartenere a Fib<^_ \  oppure a Fib^-i per costruzio-ne. Ciò è impossibile in quanto questi ultimi sono minimali per ipotesi induttiva e, seFi bh -\ cambiasse altezza, anche Fib^ la cambiarebbe , mentre se H ^h-2 cambiasse altez-za, Fibh non sarebbe più 1-bilanciato. Quindi, anche FibH è minimale e concludiamoche ogni albero 1-bilancia to di altezza h. con n nodi soddisfa n ^

Tabulando i primi 15 valori di n^ e altrettanti numeri di Fibonacci Fh, possiamoverificare per ispezione diretta che vale la relazione rih = Fh+3 — 1:

h 0 1 2 3 4 5 6 7 8 9 10 11 12 13 141 2 4 7 12 20 33 54 88 143 232 376 609 986 1596

Fh 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377

Utilizzando la nota forma chiusa dei numeri di Fibonacci

F ^ - ^ , dovecJ> = ! ± ^ l , 6 18 03 39 . . .v 5 2

possiamo affermare che FH > ^ ^ e che, quindi, esiste una costante c > 1 tale che

FH > cH per h. > 2. Pertanto, nn = F h + 3 — 1 ^ c h e, poiché n ^ possiamo

Fibì FibH

Page 180: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 180/373

 

InserisciC u, e ) :

IF (u == nuli) {

RETURN f = NuovaFoglia( e );

} ELSE IF (e.chiave < u.dato.chiave) {

u.sx = InserisciC u.sx, e );

IF (Altezza(u.sx) - Altezza(u.dx) == 2) {IF (e.chiave>u.sx.dato.chiave) u.sx=RuotaAntiOraria(u.sx);

u = RuotaOrariaC u );

>} ELSE IF (e.chiave > u.dato.chiave) {

u.dx = InserisciC u.dx, e );

IF (Altezza(u.dx) - AltezzaCu.sx) == 2) {

IF (e.chiave < u.dx.dato.chiave) u.dx = RuotaOraria(u.dx);

u = RuotaAntiOrariaC u );

>

u.altezza = max( Altezza(u.sx), Altezza(u.dx) ) +

RETURN U;

1;

AltezzaC u ):

IF (u == nuli) {

RETURN -1;

> ELSE {

RETURN u.altezza;

NuovaFogliaC e ):

u = NuovoNodoO;

u.dato = e;

u.altezza = 0;

u.sx = u.dx = nuli;

RETURN u;

Codice 5.7 Algoritmo per l'inserimento di un elemento e in un albero AVL con radice u.

concludere che n ^ c h : in altre parole, ogni albero 1-bilanciato di n nodi e altezza h.

verifica h. = O( lo gn ).

Per implementare gli alberi AVL, introduciamo un ulteriore campo u . a l t e z z a neisuoi nodi u, tale che u . a l t e z z a = h( u) . Not iamo che l'operazione di ricerca negli alberiAVL rimane identica a quella descritta nel Codice 5.5. Mostriamo quindi nel Codice 5.7

come estendere l'inserimento per garantire che l'albero AVL rimanga 1-bilanciato.Dopo la creazione della foglia f contenente l'elemento e (riga 3), aggiorniamo le

altezze ricorsivamente nei campi u . a l t e z z a degli antenati u di f, essendo questi ultimii soli nodi che possono cambiare altezza (riga 17). Allo stesso tempo, controlliamo se ilnodo corrente è 1-bilanciato: definiamo nodo critico il minimo antenato di f che violatale proprietà.

A tal fine, percorriamo in ordine inverso le chiamate ricorsive sugli antenati di f 

(righe 5 e 11), fino a individuare il nodo critico u, se esiste: in tal caso, se f discende da

Page 181: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 181/373

 

RuotaOraria( z ):

v = z.sx;

z.sx = v.dx;

v.dx = z;

z.altezza = max(

v.altezza = max(

RETURN V;

Altezza(z.sx),

Altezza(v.sx),

Altezza(z.dx)

Altezza(v.dx)

RuotaAntiOraria( v ):z = v.dx;

v.dx = z.sx;

z.sx = v;

v.altezza = max( Altezza(v.sx), Altezza(v.dx)

z.altezza = max( Altezza(z.sx), Altezza(z.dx)

RETURN Z;

i ;l ;

1;

Cod ice 5.8 Rotazioni oraria e antioraria.

u. sx, l'altezza del sottoalbero sinistro di u differisce di due rispetto a quella del destro(riga 6), mentre se f discende da u. dx, abbiamo che è l'altezza del sottoalbero destro diu a differire di due rispetto a quella del sinitro (riga 12). Comunque vada, aggiorniamol'altezza del nodo prima di terminare la chiamata attuale (riga 17).

Discutiamo ora come ristrutturare l'albero in corrispondenza del nodo critico u, uti-lizzando le rotazioni orarie e antiorarie per rendere nuovamente u un nodo 1-bilanciato(righe 7 - 8 e 13-1 4) : tali rotazioni sono illustrate e descritte nel Codice 5.8. Notiamo

che esse richiedono 0(1) tempo e preservano la proprietà di ricerca: le chiavi in a so-no minori di v. dato. chiave; quest'ultima è minore delle chiavi in (3, le quali sonominori di z. dato. chiave; infine, quest'ultima è minore delle chiavi in y.

Utilizziamo le rotazioni per trattare i quattro casi che si possono presentare (indivi-duati con un semplice codice mnemonico), in base alla posizione di f rispetto ai nipotidel nodo critico u (Figura 5.2):

1. caso SS: la foglia f apparti ene al sottoalbero a radicato in u . sx . s x;

2. caso SD: la foglia f appart iene al sottoalbero (3 radicato in u . sx . dx ;

Page 182: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 182/373

 

Figura 5.2 I quattro casi possibili di sbilanciamento del nodo crìtico u a causa della creazione dellafoglia f  (contenente la chiave k).

3. caso DS: la foglia f appartiene al sottoalbero y radicato in u. dx. sx;

4. caso DD: la foglia f appartiene al sottoalbero 6 radicato in u. dx . dx.

In tutti e quattro i casi, lo sbilanciamento conduce l'albero AVL in una configurazio-ne in cui due sottoalberi hanno un dislivello pari a due. Con riferimento all'inserimentodescritto nel Codice 5.7, trattiamo il caso SS con una rotazione oraria effettuata sul no-do critico u, riportando l'altezza del sottoalbero a quella immediatamente prima che la

foglia f venisse creata (riga 8). Se invece incontriamo il caso SD, prima effettuiamo unarotazione antioraria sul figlio sinistro di u (riga 7) e poi una oraria su u stesso (riga 8):anche in tal caso, l'altezza del sottoalbero torna a essere quella immediatamente primache la foglia f venisse creata (Figura 5.3). I casi DD e DS sono speculari: effettuiamouna rotazione antioraria su u (riga 14) oppure una rotazione oraria sul figlio destro diu seguita da una oraria su u (righe 13 e 14). Siccome ogni caso richiede una o duerotazioni, il costo è 0(1) tempo per eseguire le rotazioni (al più due), a cui va aggiuntoil tempo di inserimento O(logn).

Page 183: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 183/373

 

A L V I E : inserimento in alberi AVL

Osserva, sperimenta e ver if ica

A v l l n s e r t

Per la cancellazione, possiamo trattare dei casi simili a quelli dell'inserimento, soloche possono esserci più nodi critici tra gli antenati del nodo cancellato: preferiamo quindimarcare logicamente i nodi cancellati, che vengono ignorati ai fini della ricerca. Qu andouna frazione costante dei nodi sono marcati come cancellati, ricostruiamo l'albero con lesole chiavi valide ottenendo un costo ammortizzato di O(logn). Possiamo trasformare ilcosto ammortizzato in un costo al caso pessimo interlacciando la ricostruzione dell'albe-ro, che produce una copia dell'albero attuale, con le operazioni normalmente eseguite suquest'ultimo.

Nonostante siano stati concepiti diverso tempo fa, gli alberi AVL sono tuttora moltocompetitivi rispetto a strutture di dati più recenti: la maggior parte delle rotazioni av-vengono negli ultimi due livelli di nodi, per cui gli alberi AVL risultano molto efficientise implementati opportunamente (all'atto pratico, la loro forma è molto vicina a quelladi un albero completo e quasi perfettamente bilanciato).

5.5 Opus libri: basi dati e B-alberi

Le basi di dati (database) sono al centro dei sistemi informativi di aziende, enti pubblicie privati, in quanto permettono di organizzare, in forma permanente, una grande quan-tità di dati strutturati in un formato predefinito (record) mettendoli in relazione tra diloro e rendendoli disponibili, in modo uniforme e controllato, ad applicazioni eteroge-nee (attraverso le transazioni concorrenti che mantengono l'integrità dei dati e la lorospecifica in un opportuno linguaggio di interrogazione come, per esempio, SQL ovveroStructured Query Languagé).

I sistemi per la gestione di basi di dati (DBMS o Data Base Management System) uti-lizzano diverse strutture di dati, chiamate indici, per garantire un accesso veloce ai datirichiesti dalle transazioni in corso. Possiamo modellare, semplificandolo, il problema dicostruire un indice per un DBMS in termini di un insieme S = {eo, e¡,..., e n _i} din record, dove ciascun record è caratterizzato da una chiave di ricerca primaria che loidentifica univocamente (per esempio, il codice fiscale se il record è riferito alle persone).I record possono contenere ulteriori chiavi di ricerca, dette secondarie, che esprimono iloro attributi aggiuntivi, non necessariamente in modo univoco (per esempio, la nazio-nalità o la città di residenza). Possiamo realizzare un tale indice usando la struttura di

cEHD>cwiTì crnrn

CjTTTp (TT7TT) OUI!> OTTTTt)Ctna><EII>CIIi&CE3) cTTTTTtcrnT»dmr>

Page 184: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 184/373

 

dati del dizionario: oltre a liste e tabelle hash, la struttura di dati principale per realizzarel'indice nei DBMS è il B-albero ( B-tree).

Prima di descrivere in dettaglio il B-albero, presentiamo lo scenario entro cui valuta-re il suo costo computazionale estendendo il modello RAM (Paragrafo 1.4). Nel seguito

supponiamo che la RAM abbia accesso a due livelli di memoria: al primo livello, dettomemoria principale, aggiungiamo un secondo livello, detto memoria secondaria o discoe diviso in blocchi di memoria, ciascun blocco contenente un numero di celle di memo-ria consecutive pari a d i m ens i one Bl o cco . L'accesso alla memoria di secondo livellonon è diretto, contrariamente al primo livello, ma avviene esclusivamente attraverso del-le primitive che permettono di leggere o scrivere un blocco di memoria, consentendo ilsuo trasferimento da e verso la memoria principale. Tale blocco può essere quindi ela-borato con le operazioni della RAM nella memoria principale: ai nostri fini, trattiamoun blocco b, una volta caricato nella memoria principale, come un normale array b di

d i m ens i o neB l occo elementi. Nella memoria secondaria, i blocchi sono univocamenteidentificati da interi positivi distinti che consideriamo essere i loro indirizzi logici a tuttigli effetti. Il blocco b è quindi identificato dal suo indirizzo i > 0 quando decidiamodi trasferirlo tra i due livelli di memoria: not iamo che l'indirizzo logico 0 è riservatoe rappresenta l'indirizzo vuoto (ovvero, nessun blocco corrisponderà mai a tale indiriz-zo). Le seguenti primitive gestiscono il trasfer imento di blocchi, dove b è un array did i m ens i o neB l occo elementi nella memoria principale e i > 0 è l'indirizzo logico diun blocco nella memoria secondaria:

• b = Le ggiDaDisco(i ) trasferisce il blocco con indirizzo i dalla memoria secon-

daria alla memoria principale, dove diventa accessibile come array b;• i = Cr ea Su Di sco (b) trasferisce l'array b dalla memoria principale alla memoria

secondaria, creando lo spazio opportuno per ospitarlo come blocco e restituendol'indirizzo i assegnato a tale blocco nella memoria secondaria;

• Aggio rnaSuDiscofb , i) trasferisce l'array b dalla memoria principale alla me-moria secondaria, sovrascrivendo lo spazio di memoria secondaria allocato in pre-cedenza al blocco con indirizzo i.

Al costo computazionale in termini di tempo e di spazio, aggiungiamo un ulteriore costo,chiamato numero di trasferimenti, che conteggia il numero di blocchi trasferiti tra i duelivelli di memoria, ovvero il numero di volte che le suddette primitive sono invocate al-l'interno degli algoritmi: il motivo è che l'accesso al disco è di diversi ordini di grandezzapiù lento di quello alla memoria principale. Al termine dell'esecuzione di un algoritmo, idati persistenti sono esclusivamente quelli che risiedono in memoria secondaria, mentrequelli presenti nella memoria principale sono considerati persi (come avviene in molticalcolatori).

Il numero di trasferimenti richiesti dalle operazioni sugli alberi di ricerca come gliAVL è elevato nel modello a due livelli di memoria: infatti, ogni accesso a un nodo

Page 185: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 185/373

 

-OO c E29 18 36 o 13!

-oo A B , - C D - E F

0 97 43 »131 23 57   c   u 

     C    D 

   K    j

41 64 o 12

Fi gu ra 5. 4 Esempio di B-albero con B = 4 (primi due livelli di blocchi), visto come indice per n = 7record di cui uno fitt izio (ultimo livello), le cui chiavi primarie sono A, B F (oltrealla sentinella -oo). I numeri in corsivo rappresentano il grado dei nodi del B-albero, glialtri numeri sono gli indirizzi logici dei blocchi e le frecce segnalano il loro uso comeriferimenti. Ciascun blocco è rappresentato ripiegato su se stesso a fini illustrativi.

può causare uno o più trasferimenti e, pertanto, le operazioni sul dizionario richiedonoO(logn) trasferimenti ciascuna. Inoltre, non sfruttiamo il fatto che una volta trasferitoun blocco, abbiamo dimensioneBlocco elementi a disposizione.

Sia B ^ 4 il più grande intero pari tale che B ^ (d imens ion eBl oc co —2 )/2 (in altreparole, un blocco di memoria secondaria può contenere fino a 2 x B + 2 elementi). Persemplificare la discussione, ipotizziamo che ciascun record occupi esattemente un bloccodi memoria secondaria e abbia una chiave primaria su cui costruire l'indice di ricerca.

Mostriamo come ottenere un dizionario che richiede 0(log B n) trasferimenti, fornendoun costo ottimo nella ricerca per confronti in quanto possiamo mostrare, attraverso unageneralizzazione del limite inferiore discusso per la ricerca binaria (Paragrafo 2.4.2), cheoccorrono 0(logB n) trasferimenti.

Un B-albero (precisamente, un B+-albero) per n record ha 0(n/B) nodi, dove cia-scun nodo è di grado (numero di figli) variabile compreso tra B/2 e B — 1 (esclusa laradice, che può avere grado compreso tra 2 e B — 1) e contiene un sottoinsieme dellechiavi primarie contenute nei suoi figli, in numero pari al proprio grado: in particolare,le chiavi nei padri sono una replica della minima chiave contenuta in ciascuno dei figli

(e tale proprietà vale ricorsivamente nei figli). Inoltre, i nodi allo stesso livello (cioè allastessa profondità) sono collegati in una lista ordinata: questo è utile in particolare peraccedere' alla sequenza ordinata di tutte le chiavi distinte del B-albero, che sono quellememorizzate nelle foglie (queste ultime hanno tu tte lo stesso livello). Mostriamo unesempio di B-albero con B = 4 nella Figura 5.4, dove utilizziamo un record fittizio conla chiave speciale —oo < k, per ogni k e U, che funge da sentinella negli algoritmi chemantengono tale struttura di dati. Possiamo riassumere le caratteristiche di un B-alberocome segue (dove B ^ 4 pari è un parametro della definizione del B-albero, che dipende

Page 186: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 186/373

 

dalla dimensione del blocco di memoria e da cui dipende la complessità delle operazionidefinite sul B-albero):

1. ciascun nodo u del B-albero è implementato come un array u di 2 x B + 2 elementi(di cui almeno la metà sono utilizzati) e, risiedendo in un blocco di memoria, è

univocamente individuato dal suo indirizzo logico;2. i primi B elementi dell'array u contengono un numero variabile g di chiavi pri-marie dei record, dove B/2 ^ g < B tranne che per la radice, in cui 2 < g < B(la cardinalità g delle chiavi può variare da nodo a nodo e, per questo motivo, èmemorizzata nell'elemento u[2 x B + 1]);

3. i successivi B elementi dell 'array contengono g indirizzi logici associati alle chiavi(tali indirizzi conducono ai figli dei nodi interni oppure ai record collegati allefoglie): u[r] contiene la minima chiave primaria che appare nel nodo o recordindicato da u[B + r], per 0 ^ r ^ g;

4. le foglie sono tutte allo stesso livello, denominato 0, e se un nodo u ha livello i,allora suo padre ha livello i + 1 (inoltre, l'elemento u[2 x B] contiene l'indirizzodel nodo successivo a u nel livello i): in tal modo, l'altezza h. del B-albero è datadal livello della radice;

5. le chiavi in ogni nodo u soddisfano la proprietà di ricerca: per ogni 0 < r < g, lachiave in u[r] è maggiore delle chiavi in u[0, r— 1] e di quelle raggiungibili nei nodidiscendenti, attraverso i riferimenti in u[B, B + r — 1]; inoltre, essa è minore dellechiavi in u[r+ 1, g] e minore o uguale di quelle raggiungibili nei nodi discendenti,attraverso i riferimenti in u[B + r, B + g].

Oltre ai blocchi di un B-albero, dobbiamo memorizzare la sua altezza e l'indirizzologico della radice. Come si può osservare dalla Figura 5.4, il numero di nodi (blocchi)in un B-albero per n record è pari a 0 ( n / B ) e la sua altezza è h = 0( log B n) poiché,a parte la radice, ogni nodo ha grado minimo pari a B/2: quindi la radice ha almenodue figli, almeno 2 x B/2 nipoti, almeno 2 x (B/2)2 pronipoti, e così via, fino a trovarealmeno 2(B/2) h _ 1 foglie. Essendo queste ultime in numero pari a 0(n/B), abbiamoche 2(B/2) h _ 1 = 0(n/B), per cui H = O( ) = 0( lo gB TI). Nei moderni sistemi

di calcolo, il valore di d i m e n s i o n e B l o cc o è tale che B è dell'ordine delle migliaia:pertanto, con un B-albero di soli due livelli (h. = 1) possiamo indicizzare milioni di

record, mentre con un ulteriore livello arriviamo a indicizzarne miliardi.Usando le proprietà esposte sopra, possiamo vedere come effettuare un'operazione

Ricerca di una chiave k in un B-albero di cui conosciamo l'altezza e l'indirizzo logicodella radice, come mostrato nel Codice 5.9. Partendo dalla radice (di cui conosciamol'indirizzo) e dall'altezza dell'albero, che sono i parametri d'ingresso di R i c e r c a , cari-chiamo dalla memoria secondaria un nodo per ogni livello (riga 3): su tale nodo, orapresente in memoria principale, effettuiamo la ricerca binaria ricorsiva (riga 5) descrittanel Paragrafo 2.5, la quale permette di calcolare il rango di k nell'array ordinato formato

Page 187: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 187/373

 

Ricercai indirizzo, altezza, k ):

FOR (livello = altezza; livello >= 0; livello = livello - 1) {

nodo = LeggiDaDisco( indirizzo );

card = nodo[2 x B + 1];

rango = RicercaBinariaRicorsivaC nodo[0, card-I] , k) ;

indirizzo = nodo [B + rango - 1];>RETURN indirizzo;

Inserisci( indirizzoRadice, altezza, e ):

<chiave, indirizzoN> = InsRicC indirizzoRadice, altezza, e );

IF (indirizzoN != 0) {

buffer[0] = -oo;buffer[B] = indirizzoRadice;

buffer[1] = chiave;

buffer[B + 1] = indirizzoN;

buffer[2 x B] =0;

buffer[2 x B + 1] = 2;

indirizzoRadice = CreaSuDiscoC buffer );

altezza = altezza+1;

>RETURN <indirizzoRadice, altezza»;

Codi ce 5.9 Ricerca e inserimento in un B-albero di cui conosciamo l'al tezza (pari a - 1 se l'alberoè vuoto) e l'indirizzo logico della radice (pari a 0 se l'albero è vuoto).

dalle chiavi del nodo (ricordiamo che il rango è il numero di chiavi minori o uguali a

k). Il prossimo figlio da caricare è indicato quindi nel riferimento associato alla chiave

avente tale rango come posizione (riga 6). Terminiamo quando troviamo un record (ov-

vero, l i v e l l o è negativo), e qui ndi resti tuiamo tale record (se la chiave k appare, deve

apparire in tale record e come campo e. chiave).

No n è difficile modificare la ricerca in modo che esca dal ciclo quando l i v e l l o

è zero, così da elencare i record e con e. c h i ave ^ k seguendo l'o rdine indicato dalle

foglie del B-albero (usando il riferimento alla prossima foglia memorizzato nell'elementof[2 x B] della foglia corrente f) . Poiché effett uiamo il tras ferimento di h = 0(l ogB n)

blocchi, il numero totale di trasferimenti è 0(log B n) . Inoltre il tempo di ricerca, oltre

a quello necessario per i trasferimenti, è OflogB x logB n) = O(logn) perché la ricerca

binaria richiede O(logB) tempo per ciascuno degli h. livelli.

Per qua nto riguarda l'operazione I n s e r i s c i , osserviamo che la modalità di bilan-

ciamento del B-albero differisce da quella dell'albero AVL, basata sulle rotazioni e sulla

crescita dell'albero verso le foglie: nel caso del B-albero, la crescita è verso l'alto, con la

Page 188: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 188/373

 

creazione di nuove radici. Precisamente, quando un nodo u contiene B chiavi, alloracreiamo un nuovo fratello u' alla sua destra, che prende le ultime B/2 chiavi (quelle piùgrandi) da u. In tal modo, otteniamo due nodi con B/2 chiavi ciascuno e, inoltre, unacopia della chiave minima in u ' sale, insieme all'indirizzo logico di u ' , nel padre dei duenodi u e u'.

Tale operazione prende il nome di divisione (split) e richiede l'inserimento di unanuova chiave (e l'indirizzo a essa associato) nel padre, il quale a sua volta può arrivarea contenere B chiavi, richiedendo esso stesso una divisione. Il meccanismo può qu indipropagarsi nel caso pessimo di figlio in padre fino alla radice, la cui divisione crea unanuova radice con due figli (motivando la condizione che 2 ^ g < B per la sola radice).Inoltre, l'uso della chiave fittizia —oo permette di mantenere l'invariante che l'inserimen-to di una chiave non cambia mai la minima chiave del nodo corrente u, evitando così didover riorganizzare l'associazione tra chiavi u[r] e indirizzi u[B + r].

Il meccanismo suddetto è descritto nel Codice 5.9, il quale inserisce il record e (ri-ga 2) ricorsivamente nella radice (che può anche essere vuota, per cui va posta la suaaltezza al valore —1 la prima volta che invochiamo I n s e r i s c i ) . Se necessario, crea unanuova radice utilizzando un array di appoggio b u f f e r ponendo la chiave speciale —oo

come minima nel nuovo nodo (righe 4 e 5) e la chiave salita dal basso nella posizionesuccessiva (righe 6 e 7).

Gli indirizzi logici dei figli subiscono un trattamento analogo. Inoltre, il riferimen-to al successore è sempre nullo per la radice (riga 8), e l'elemento in ultima posizioneassume come valore la cardinalirà dell'insieme delle due chiavi nella radice (riga 9). Infi-

ne, la nuova radice viene trasferita in memoria secondaria, annotando l'indirizzo logicoassegnatole (riga 10) e incrementando l'altezza (riga 11).

Resta da discutere l'inserimento ricorsivo, descritto nel Codice 5.10, e come essogestisce le divisioni che possono propagarsi verso l'alto. La ricorsione ha come caso base(riga 2) l'allocazione in memoria secondaria del record e che contiene la chiave k (dovee . ch iave = k), restituendo la coppia formata dalla chiave k stessa e dall'indirizzo logicoassegnato al record e (in modo da poterli inserire nell'opportuna foglia del B-albero).

Nello schema ricorsivo, dopo aver caricato in memoria principale il nodo del livel-lo corrente (riga 3) e aver determinato il numero di chiavi in esso contenute (riga 4),

effettuiamo una ricerca binaria ricorsiva per determinare il rango (riga 5), come nell'o-perazione Ricerca. A questo punto, inseriamo ricorsivamente la chiave k nel figlio delnodo nella posizione individuata dal rango (riga 6). La chiamata ricorsiva restituisce unacoppia formata da una chiave e dall'indirizzo logico corrispondente (quello di u' se ilfiglio u ha subito una divisione oppure, se siamo in una foglia, quello del nuovo recordappena inserito nel caso base).

Se l'indirizzo logico è 0, vuol dire che non dobbiamo propagare alcunché nel nodocorrente e nei suoi antenati (riga 28). Altrimenti (righe 7—27), dobbiamo far posto alla

Page 189: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 189/373

 

InsRicC indirizzo, livello, e ):

IF (livello < 0) RETURN <e.chiave, CreaSuDisco(e)>;

nodo = LeggiDaDiscoC indirizzo );

card = nodo[2 x B + 1];

rango = RicercaBinariaRicorsivaC nodo[0, card-1], e.chiave );

<chiave, indirizzoN> = InsRicC nodo[B+rango-1], livello-1, e);IF (indirizzoN != 0) {

FOR (i = card; i > rango; i = i - 1) {

nodo[i] = nodo[i-l];

nodo[B + i] = nodo[B + (i - 1)];

>nodo[rango] = chiave;

nodo[B + rango] = indirizzoN;

nodo[2 x B + 1] = card + 1;

AggiornaSuDiscoC nodo, indirizzo );

IF (nodo[2 x B + 1] == B) {FOR  (i = 0; i < B/2; i = i + 1) {

buffer [i] = nodo[B/2+i];

buffer[B + i] = nodo[B + (B/2+i)];

>buffer[2 x B + 1] = nodo[2 x B + 1] = B/2;

buffer[2 x B] = nodo[2 x B];

nodo[2 x B] = CreaSuDiscoC buffer );

AggiornaSuDiscoC nodo, indirizzo );

RETURN <buffer[0], nodo[2 x B]>;

>>

RETURN <0, 0>

Codice 5.10 Inserimento ricorsivo in un B-albero.

chiave salita dal basso (righe 8-11) ponendola nella posizione corrispondente al rangoprecedentemente calcolato con la ricerca binaria ricorsiva (riga 12), con l'indirizzo asso-

ciato a essa nella posizione corrispondente (riga 13), e incrementando il numero di chiavipresenti nel nodo (riga 14).

Dopo aver salvato il nodo sulla memoria secondaria (riga 15), controlliamo se vadiviso (riga 16): in tal caso prendiamo le B/2 chiavi più grandi e i loro indirizzi associatie le ponia mo in un array di appoggio b u f f e r (righe 17-2 0), che diventerà il fratellodestro del nodo corrente.

Aggiustiamo la loro cardinalità (riga 21) e colleghiamo il nuovo nodo mediante unriferimento (riga 22) per agganciarlo al resto dei nodi nella lista ordinata per quel livel-

Page 190: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 190/373

 

lo. Possiamo quindi salvare nella memor ia secondaria b u f f e r come fratello destro delnodo corrente (riga 23), che a sua volta va salvato con la sua nuova configurazione dellechiavi (riga 24). Facciamo quindi salire al padre del nodo corrente la chiave minima cheappare nella posizione zero del suo nuovo fratello, associandogli l'indirizzo logico di esso

(riga 25).L'inserimento in un B-albero richiede 0(1) trasferimenti per livello e, quindi, ha

un costo totale di 0(log B n ) trasferimenti, come la ricerca. A differenza della ricerca,il tempo totale di calcolo, a parte i trasferimenti, è 0(Blog B n) perché ciascun livellorichiede 0(B) tempo: tuttavia, tale tempo è trascurabile rispetto a quello richiesto pertrasferire i blocchi da e verso la memoria secondaria. Per quanto riguarda l'operazione dicancellazione, anch'essa può essere realizzata con lo stesso costo dell'inserimento, solo cheinvece dell'operazione di divisione utilizziamo l'operazione di fusione. Analogamente aquanto detto per gli alberi AVL, consigliamo però di cancellare logicamente le chiavi edi ricostruire periodicamente il B-albero con le sole chiavi non marcate come cancellate.

Un'ult ima osservazione riguarda l'uso dei B-alberi in memoria principale: fissandoB = 4 otteniamo un albero di ricerca bilanciato alternativo agli alberi AVL, chiamato2-3-albero, che ha lo stesso costo O(logn) per operazione. Inoltre, il modo con cui rico-piamo le chiavi nei livelli superiori è reminiscente di quanto succede nell'organizzazionedelle skip list (Paragrafo 3.3). Infine, modifi cando leggermente il B-albero (B = 4) ecolorando alternativamente i livelli dei nodi con il rosso e il nero, è possibile ottenereuna forma equivalente a un'altra famiglia di alberi bilanciati, detti alberi rosso-neri (noticome red-black tree e usati per gestire le VMA del kernel Linux).

A L V I E : ricerca e inserimento in B-alberi

Osserva, sperimenta e verifica

BTree

5.6 Opus libri: liste invertite e trieNei sistemi di recupero dei documenti (information retrieval), i dati sono documentitestuali e il loro contenuto è relativamente stabile: pertanto, in tali sistemi lo scopo prin-cipale è quello di fornire una risposta veloce alle numerose interrogazioni degli utenti(contrariamente alle basi di dati che sono progettate per garantire un alto flusso di tran-sazioni che ne modificano i contenuti). In tal caso, la scelta adottata è quella di elaborarepreliminarmente l'archivio dei documenti per ottenere un indice in cui le ricerche siano

Page 191: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 191/373

 

molto veloci, di fatto evitando di scandire l'intera collezione dei documenti a ogni inter-rogazione (come vedremo, i motori di ricerca sul Web rappresentano un noto esempiodi questa strategia). Infatt i, il tempo di calcolo aggiuntivo che è richiesto nella costru-zione degli indici, viene ampiamente ripagato dal guadagno in velocità di risposta alleinnumerevoli richieste che pervengono a tali sistemi.

Le liste invertite (chiamate anche file invertiti, file dei posting o concordanze) costi-tuiscono uno dei cardini nell'organizzazione di documenti in tale scenario. Le conside-riamo un'organizzazione logica dei dati piuttosto che una specifica struttura di dati, inquanto le componenti possono essere realizzate utilizzando strutture di dati differenti.La nozione di liste invertite si basa sulla possibilità di definire in modo preciso la no-zione di termine (inteso come una parola o una sequenza massimale alfanumerica), inmodo da partizionare ciascun documento o testo T della collezione di documenti D insegmenti disgiunti corrispondenti alle occorrenze dei vari termini (quindi le occorrenzedi due termini non possono sovrapporsi in T). La struttura delle liste invertite è infattiriconducibile alle seguenti due componenti (ipotizzando che il testo sia visto come unasequenza di caratteri):

• il vocabolario o lessico V, contenente l'insieme dei termini dist inti che appaiononei testi di D;

• la lista invertita Lp (detta dei posting) per ciascun termine P G V, contenente unriferimento alla posizione iniziale di ciascuna occorrenza di P nei testi T E D: inaltre parole, la lista per P contiene la coppia (T, i) se il segmento T[i, i + |P| — 1]del testo è proprio uguale a P (ogni documento T € D ha un suo identificatore

numerico che lo contraddistingue dagli altri documenti in D).Notiamo che le liste invertite sono spesso mantenute in forma compressa per ridurre lospazio a circa il 10-25% del testo e, inoltre, le occorrenze sono spesso riferite al numerodi linea piuttosto che alla posizione precisa nel testo. Solitamente, la lista invertita Lp

memorizza anche la sua lunghezza in quanto rappresenta la frequenza del termine P neidocument i in D. Per completezza, osserviamo che esistono metodi alternativi alle listeinvertite come le bitmap e i signature file, ma osserviamo anche che tali metodi nonrisultano superiori alle liste invertite come prestazioni.

La realizzazione delle liste invertite prevede l'utilizzo di un dizionario (tabella hash

o albero di ricerca) per memorizzare i termini P € V: nello specifico, ciascun elemen-to e memorizzato nel dizionario ha un distinto termine P G V contenuto nel campoe . c h i a v e e ha un'implementazione della corrispondente lista Lp nel campo e . s a t .Nel seguito, ipotizziamo quindi che e . s a t sia una lista doppia che fornisce le opera-zioni descritte nel Paragrafo 5.2; inoltre, ipotizziamo che un termine sia una sequenzamassimale alfanumerica nel testo dato in ingresso.

Il Codice 5.11 descrive come costruire le liste invertite usando un dizionario permemorizzare i termini distinti , secondo quanto abbiamo osservato sopra. Lo scopo è

Page 192: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 192/373

 

CostruzioneListelnvertiteC T ) : (pre: T e D è un testo di lunghezza n)i = 0;

WHILE ( i < n ) {WHILE (i < n kk ¡AlfaNumericoC T[i] ))

i = i+1;

3 = ì;WHILE (j < n ft& AlfaNumericoC T[j] ))

j = j+i;

e = dizionarioListelnvertite.Ricercai T[i,j-1] );

IF (e != nuli) {

e.sat.InserisciFondoC <T,i> );

> ELSE {

elemento.chiave = T[i,j-1];

elemento.sat = NuovaListaC );

elemento.sat.InserisciFondoC <T,i> );

dizionarioListelnvertite.Inserisci( elemento )>i = j;

>

AlfaNumericoC c ):

RETURN ('a' <= c <= 'z' Il 'A' <= c <= 'Z'  Il '0' <= c <= '9');

Codice 5.11 Costruzione di liste invertite di un testo T e D (elemento indica un nuovo elemento).

quello di identificare una sequenza alfanumerica massima rappresentata dal segmento ditesto T[i, j — 1] per i < j (righe 4—8): tale termine viene cercato nel dizionar io (riga 9) e,se appare in esso, la coppia (T, i) che ne denota l'occorrenza in T viene posta in fondo allacorr ispondente lista invertita (righa 11); se invece T[i, j — 1] non appare nel dizionario,tale lista viene creata e memorizzata nel dizionario (righe 13—16).

A L V I E : costruzione delle liste invertite

Osserva, sperimenta e verificaInver tedL i s t

Supponendo che n sia il numero totale di caratteri esaminati, il costo della costru-zione descritta nel Codice 5.11 è pari a O(n) al caso medio usando le tabelle hash eO(nlogn) usando gli alberi di ricerca bilanciati. Nei casi reali, la costruzione è concet-

CE><Œ>

&> -

Page 193: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 193/373

 

tualmente simile a quella descritta nel Codice 5.11 ma notevolmente differente nelle pre-stazioni. Per esempio, dovremmo aspettarci di applicare tale codice a una vasta collezionedi documenti che, per la sua dimensione, viene memorizzata nella memoria secondariain cui l'accesso è a blocchi (come specificato nel Paragrafo 5.5). Per le liste invertite,potremmo memorizzare anche le varie liste Lp nella memoria secondaria mentre il di-zionario per il solo vocabolario V rimarrebbe in memoria principale; oppure, potremmousare un B-albero per mantenere anche V in memoria secondaria.

Ne risulta che, per l'analisi del costo finale, possiamo utilizzare il modello di me-moria a due livelli valutando le varie decisioni progettuali: di solito, il vocabolario V èmantenuto in memoria principale, mentre le liste e i documenti risiedono in memoriasecondaria; tuttavia, diversi motori di ricerca memorizzano anche le liste invertite (manon i documenti) nella memoria principale utilizzando decine di migliaia di macchinein rete, ciascuna dotata di ampia memoria prinicipale a basso costo.

Per quanto riguarda le interrogazioni effettuate in diversi motori di ricerca, esse pre-vedono l'uso di termini collegati tra loro mediante gli operatori booleani: l'operatoredi congiunzione (AND) tra due termini prevede che entrambi i termini occorrano neldocumento; quello di disgiunzione (OR) prevede che almeno uno dei termini occorra;infine, l'operatore di negazione (NOT) indica che il termine non debba occorrere.

E possibile usufruire anche dell'operatore di vicinanza (NEAR) come estensione del-l'operatore di congiunzione, in cui viene espresso che i termini specificati occorrano apoche posizioni l'uno dall'altro. Tali interrogazioni possono essere composte come unaqualunque espressione booleana, anche se le statistiche mostrano che la maggior parte

delle interrogazioni nei motori di ricerca consiste nella ricerca di due termini connessidall'operatore di congiunzione.

Le liste invertite aiutano a formulare tali interrogazioni in termini di operazioni ele-mentari su liste, supponendo che ciascuna lista Lp sia ordinata. Per esempio, l'interro-gazione (P AND Q) altro non è che l'intersezione tra Lp e Lq , mentre (P OR Q) ne èl'unione senza ripetizioni: entrambe le operazioni possono essere svolte efficientementecon una variante dell'algoritmo di fusione adoperato per il mergesort (Paragrafo 2.5.3).L'operazione di negazione è il complemento della lista e, infine, l'interrogazione (P NEARQ) è anch'essa una variante della fusione delle liste Lp e Lq , come discutiamo ora in det-

taglio a scopo illustrativo (le interrogazioni AND e OR sono trattate in modo analogo): atale scopo, ipotizziamo che le coppie in Lp e Lq siano in ordine lessicografico crescente.

Il Codice 5.12 descrive come procedere per trovare le occorrenze di due termini Pe Q che distano tra di loro per al più maxPos posizioni, facendo uso del Codice 5.13per verificare tale condizione evitando di esaminare tutte le 0(|Lp| x |Lq|): le occorrenzerestituite in uscita sono delle triple composte da T, i e j, dove 0 ^ j — i ^ maxPos, aindicare che T [ i , i + |P| - 1] = P e T[ j, j + |Q| - 1] = Q oppure T[ i, i + |Q| - 1] = Q eT[j, j + |P| — 1] = P. Notiamo che tali triple sono fornite in uscita in ordine lessicografico

Page 194: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 194/373

 

InterrogazioneNearC P, Q, maxPos ): (pre: maxPos 0)

LP = Ricercai dizionarioListelnvertite, P );

LQ = Ricercai dizionarioListelnvertite, Q );

IF (LP != nuli && LQ != nuli) {

listaP = LP.sat.inizio;

listaQ = LQ.sat.inizio ; WHILE (listaP != nuli && listaQ != nuli) {

<testoP, posP> = listaP.dato;

ctestoQ, posQ> = listaQ.dato;

IF (testoP < testoQ) {

listaP = listaP.succ;

} ELSE IF (testoP > testoQ) {

listaQ = listaQ.succ;

} ELSE IF (posP <= posQ) {

VerificaNeari listaP, listaQ, maxPos );

listaP = listaP.succ;> ELSE {

VerificaNeari listaQ, listaP, maxPos );

listaQ = listaQ.succ;

>}

>

Codice 5.1 2 Algoritmo per la risoluzione dell'inter rogazione (P NEAR Q), in cui specifichiamoanche il massimo numero di posizioni in cui P e Q possono distare.

da Verif icaNear, considerando nell'ordinamento delle triple prima il valore di T, poiil minimo tra i e j e poi il loro massimo: tale ordine lessicografico permette di esaminareogni testo in ordine di identificatore e di scorrere le occorrenze al suo interno riportatenell'ordine simulato di scansione del testo, fornendo quindi un utile formato di uscitaall'utente dell'interrogazione (in quanto non deve saltare da una parte all'altra del testo).

La funzione I n t e r r o g a z i o n e N e a r nel Codice 5.12 provvede quindi a cercare Pe Q nel vocabolario utilizzando il dizionario e supponendo che i testi T nella collezione

sia identificati da interi (righe 2 e 3). Nel caso che le liste invert ite Lp e Lq non sianoentrambe vuote, il codice provvede a scandirle come se volesse eseguire una fusione di taliliste (righe 4—22): ricordiamo che ciascuna lista è composta da una coppia che memorizzal'identificatore del testo e la posizione all'interno del testo dell'occorrenza del terminecorrispettivo (P o Q).

Di conseguenza, se i testi sono diversi nel nodo corrente delle due liste, la funzio-ne avanza di una posizione sulla lista che contiene il testo con identificatore minore(righe 10—13). Alt rimenti , i testi sono i medesimi per cui essa deve produrre t ut te le

Page 195: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 195/373

 

VerificaNear( listaX, listaY, M ): {pre: posX posY)

<testoX, posX> = listaX.dato;

ctestoY, posY> = listaY.dato;

 WHILE (listaY != null && testoX == testoY && posY-posX <= M) {

PRINT testoX, posX, posY;

listaY = listaY.succ;<testoY, posY> = listaY.dato;

Codice 5 . 1 3 Algoritmo di verifica di vicinanza per l'interrogazione (P NEAR Q).

occorrenze vicine usando Verif icaNear e distinguendo il caso in cui l'occorrenza di Psia precedente a quella di Q o viceversa (righe 14—19).

A L V I E : interrogazione di vicinanza per le l iste invertite

Osserva, sperimenta e verifica

NearQuery

Per la complessità notiamo che, se r indica il numero di triple che soddisfano l'inter-

rogazione di vicinanza e che, quindi, sono fornite in uscita con il Codice 5.12, il tempototale impiegato da esso è pari a 0(|Lp| + |Lq| + r) e quindi dipende dalla quantità r dirisultati forniti in uscita (output sensitive). Tale codice può essere modificato in modo dasostituire la verifica di distanza sulle posizioni con quella sulle linee del testo.

Gli altri tipi di interrogazione sono trattati in modo analogo a quella per vicinanza.Per esempio, l'interrogazione con la congiunzione (AND) calcolata come intersezione diLp e Lq richiede 0(|Lp| + |Lq|) tempo: da notare però, che se memorizziamo tali listeusando degli alberi di ricerca, possiamo effettuare l'intersezione attraverso una ricerca diogni elemento della lista più corta tra Lp e Lq nell'albero che memorizza la più lunga.

Quindi, se m = min{|Lp|, |Lq |} e n = max{|Lp|, |Lq|}, il costo è pari a O ( ml og n) e che,qu ando m è molto min ore di n , risulta inferiore al costo 0 ( m + n) stabilito sopra. Ingenerale, il costo ottimo per l'intersezione è pari 0(m.log(n/m)) che è sempre miglioresia di O( ml og n ) che di 0 ( m + n). Possiamo ottenere tale costo utilizzando gli alberi chepermettono la finger search, in quanto ognuna delle m ricerche richiede O(logd) tempoinvece che O(logn) tempo, dove d < n è il numero di chiavi che intercorrono, in ordinedi visita simmetrico, tra il nodo a cui perveniamo con la chiave attualmente cercata eil nodo a cui siamo pervenuti con la precedente chiave cercata: al caso pessimo, abbia-

Page 196: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 196/373

 

mo che le m ricerche conducono a m nodi equidistanti tra loro, per cui d = 0 ( n / m )massimizza tale costo cumulativo fornendo O( ml og (n/ m)) tempo.

Tale costo motiva la strategia di risoluzione delle interrogazioni che vedono la con-giunzione di t > 2 termini (invece che di soli due): prima ordiniamo le t liste invertitedei termini in ordine crescente di frequenza (pari alla loro lunghezza); poi, calcoliamol'intersezione tra le prime due liste e, per 3 ^ i ^ t, effettuiamo l'intersezione tra l'i-esima lista e il risultato calcolato fino a quel momento con le prime i — 1 liste in ordinenon decrescente di frequenza. Nel considerare anche le espressioni in disgiunzione (OR),procediamo come prima per ordine di frequenza delle liste, utilizzando una stima basatasulla somma delle loro lunghezze.

Notiamo, infine, che alternativamente le liste invertite possono essere mantenuteordinate in base a un ordine o rango di importanza delle occorrenze. Se tale ordineè preservato in modo coerente in tutte le liste, possiamo procedere come sopra con ilvantaggio di esaminare solo i primi elementi di ciascuna lista invertita, in quanto ter-miniamo la scansione delle liste non appena raggiungiamo un numero sufficiente dioccorrenze che sono necessariamente di rango maggiore rispetto a quelle che scoprirem-mo successivamente, proseguendo con la scansione: in tal modo, possiamo mediamenteevitare di esaminare tutti gli elementi delle liste invertite.

5.6.1 Trie o alberi digitali di ricerca

Per realizzare il vocabolario V nelle liste invertite abbiamo utilizzato finora le tabelle hashoppure gli alberi di ricerca. Nel seguito, model liamo i termini da memorizzare in V

come stringhe di lunghezza variabile, ossia come sequenze di simboli o caratteri presi daun alfabeto L di a simboli, dove o è un valore prefissato che non dipende dalla lunghezzae dal numero delle stringhe prese in considerazione (per esempio, a = 256 per l'alfabetoASCII mentre o = 65536 per l'alfabeto UNICODE/UTF8). Ne deriva che ciascuna delleoperazioni di un dizionario per una stringa di m caratteri, richiede O(m) tempo mediocon le tabelle hash oppure O (mlogn) tempo con gli alberi di ricerca, dove n = |V|:in quest'ultimo caso, abbiamo O(logn) confronti da eseguire e ciascuno di essi richiedeO(m) tempo.

Mostriamo come ottenere un dizionario che garantisce O(m) tempo per operazione

utilizzando una struttura di dati denominata trie o albero digitale di ricerca, largamen-te impiegata per memorizzare un insieme S = ciò, ai> • • • » a n-i di n stringhe. Il ter-mine trie si pronuncia come la parola inglese try e deriva dalla parola inglese retrievalutilizzata per descrivere il recupero delle informazioni. I trie hanno innumerevoli ap-plicazioni in quanto permettono di estendere la ricerca in un dizionario ai prefissi dellastringa cercata: in altre parole, oltre a verificare se una data stringa P appare in S, essipermettono di trovare il più lungo prefisso di P che appare come prefisso di una dellestringhe in S (tale operazione non è immediata con le tabelle hash e con gli alberi di

Page 197: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 197/373

 

c „ a „ t „a n i a- O O O O O 0

V z a r o

P i s a' - O - C H C H I ]

,c e I l i

0 0 0 0 - 0n „ a

c „ a t a n iO O O O V H O ]

VP ^ - o V A a

O O 0

Figura 5.5 Trie per i nomi di al cune province (a sinistra) e sua versione potata (a destra).

ricerca). A tal fine, definiamo il prefisso i della stringa P di lunghezza m, come la suapar te iniziale P[0, i], dove 0 ^ i ^ m — 1. Per esempio, la ricerca del prefisso ve nell'in-sieme S composto dai nomi di alcune province italiane, ovvero Catania, Catanza ro ,

 pisa, p i s t o i a , v e r b a n i a , Vercelli e verona, fornisce come risultato le stringhev e r b a n i a , V e r c e l l i e v e r o n a .

Il trie per un insieme S = ao, Qi,..., a n _ i di n stringhe definite su un alfabeto I èun albero cardinale cr-ario (Paragrafo 4.4), i cui i nodi hanno cr figli (vuoti o meno), e la

cui seguente definizione è ricorsiva in termini di S:

1. se l'insieme S delle stringhe è vuoto, il trie corrispondente a S è vuoto e vienerappresentato con nuli;

2. se S non è vuoto, sia S c l'insieme delle stringhe in S aventi c come carattereiniziale, dove c e I (ai soli fini della definizione ricorsiva del trie, il carattereiniziale c comune alle stringhe in S c viene ignorato e temporaneamente rimosso):il trie corrispondente a S è quindi composto da un nodo u chiamato radice tale

che u.f iglio[c] memorizza il trie ricorsivamente definito per S c (alcuni dei figlipossono essere vuoti).

Per poter realizzare un dizionario, ciascun nodo u. di un trie è dotato di un campou . d a t o in cui memorizzare un elemento e: ricordiamo che e ha un campo di ricercae . c h i a v e , che è una stringa in questo scenario, e un campo e . s a t per i dati satellite,come specificato in precedenza. Mostriamo, nella parte sinistra della Figura 5.5, il triecorrispondente all'insieme S di stringhe composto dai sette nomi di provincia menzionatiprecedentemente.

Page 198: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 198/373

 

Ricercai radiceTrie, P ): (pre: P è una stringa di lunghezza m)

u = radiceTrie;

FOR  (i = 0; i < m; i = i+1) {

IF (u.figlio[ P[i] ] != nuli) {

u = u.figlio[ P[i] ];

> ELSE {

RETURN nuli;

>>

RETURN u . d a t o ;

Codice 5.14 Algoritmo di ricerca in un trie.

Ogni stringa in S corrisponde a un nodo u del trie ed è quindi memorizzata nelcampo u . d a t o . ch i a v e (e gli eventuali dati satellite sono in u . d a t o . s a t ) : per esem-

pio, la foglia 5 corrisponde a Vercelli come mostrato in Figura 5.5, dove le foglie del

trie sono numera te da 0 a 6, e la foglia nume ro i memorizza il nome della ( i + 1 )-esima

provincia (0 ^ i ^ 6).

In generale, estendendo tutte le stringhe con un simbolo speciale, da appendere in

fondo a esse come terminatore di stringa, e memorizzando le stringhe così estese nel trie,

otteniamo la proprietà che queste stringhe sono associate in modo univoco alle foglie del

trie (quest'estensione non è necessaria se nessuna stringa in S è a sua volta prefisso di

un'altra stringa in S).

Notiamo che l'altezza di un trie è data dalla lunghezza massima tra le stringhe.

Nel nostro esempio, l'altezza del trie è 9 in quanto la stringa più lunga nell'insieme è

Catanzaro. Potando i sottoalberi che portano a una sola foglia, come mostrato nella

parte destra della Figura 5.5, l'altezza media non dipende dalla lunghezza delle stringhe,

ma è limitata superiormente da 2log a n + 0(1), dove n è il numero di stringhe nel-

l'insieme S. Tale potatura presume che le stringhe siano memorizzate altrove, per cui è

possibile ricostruire il sottoalbero potato in quant o è un filamento di nodi contenente la

sequenza di caratteri finali di una delle stringhe (alternativamente, tali caratteri possono

essere memorizzati nella foglia ottenuta dalla potatura).

Per quanto riguarda la dimensione del trie, indicando con N la lunghezza totale

delle n stringhe memorizzate in S, ovvero la somma delle loro lunghezze, abbiamo che la

dimensione di un trie è al più N + 1. Tale valore viene raggiunto quando ciascuna stringa

in S ha il primo carattere differente da quello delle altre, per cui il trie è composto da

una radice e poi da |S| filamenti di nodi, ciascun filamento corrispondente a una stringa.

Tuttavia, se si adotta la versione potata del trie, la dimensione al caso medio non dipende

dalla lunghezza delle stringhe e vale all' ino ra l , 4 4 n .

Page 199: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 199/373

 

RicercaPrefissi ( radiceTrie, P ): (pre: P è una stringa di lunghezza m)

u = radiceTrie;

fine = false;

FOR (i = 0; Ifine kk i < M; i = i+1) {

IF (u.figlioC P[i] ] != nuli) {

u = u.figlio[ P[i] ];> ELSE {

fine = true;

>>numStringhe = 0;

Recuperai u, elenco );

RETURN elenco;

Recuperai u, elenco ):

IF (u != nuli) {

IF (u.dato != nuli) {

elenco[numStringhe]= u.dato;

numStringhe = numStringhe + 1 ;

>FOR (c = 0; c < sigma; c = c + 1)Recuperai u.figlio[c], elenco );

>

Codice 5.15 Algoritmo di ricerca per prefissi in un trie (numStringlie è una variabile globale).

Come possiamo notare dall'esempio nella Figura 5.5, i nodi del trie rappresentatoprefissi delle stringhe memorizzate, e le stringhe che sono memorizzate nei nodi discen-denti da un nodo u hann o in comune il prefisso associato a u . Nel nostro esempio,le foglie discendent i dal nodo che memorizza il prefisso p i ha nn o associate le stringhep i s a e p i s t o i a .

Sfruttiamo questa proprietà per implementare in modo semplice la ricerca di unastringa di lunghezza m in un trie utilizzando la sua definizione ricorsiva, come mostratonel Codice 5.14: partiamo dalla radice per decidere quale figlio scegliere a livello i = 0e, al generico passo in cui dobbiamo scegliere un figlio a livello i del nodo corrente u,esaminiamo il carattere P[i] della stringa da cercare. Osserviamo che, se il corrispondentefiglio no n è vuo to, cont inuiamo la ricerca por tando il livello a i + 1. Se invece talefiglio è vuoto, possiamo concludere che P non è memorizzata nel dizionario. Qu andoi = m, abbiamo esaminato tutta la stringa P con successo pervenendo al nodo u di cuirestituiamo l'elemento con ten uto nel campo u . d a t o : in tal caso, osserviamo che P èmemorizzato nel dizionario se e solo se tale campo è diverso da n u l i .

Page 200: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 200/373

 

La parte interessante della ricerca mostrata nel Codice 5.14 è che, a differenza dellaricerca mostrata per le tabelle hash e per gli alberi di ricerca, essa può facilmente identi-ficare il nodo u che corrisponde al più lungo prefisso di P che appare nel trie: a tal fine,possiamo modificare la riga 7 del codice in modo che restituisca il nodo u (al posto din u l i ) .

Di conseguenza, tutte le stringhe in S, che hanno tale prefisso di P come loro pre-fisso, possono essere recuperate nei nodi che discendono da u (incluso) attraverso unasemplice visita, ottenendo il Codice 5.15: notiamo che le ricerche di stringhe lunghe,quando queste ultime non compaiono nel dizionario, sono generalmente più veloci dellecorrispondenti ricerche nei dizionari implementati con le tabelle hash, poiché la ricercanei trie si ferma non appena trova un prefisso di P che non occorre nel trie mentre lafunzione hash è comunque calcolata sull'intera stringa prima di effettuare la ricerca.

Non è difficile analizzare il costo della ricerca, in quanto vengono visitati O(m ) nodi :

poiché ogni nodo richiede tempo costante, abbiamo O(m ) tempo di ricerca. Diamoun piccolo esempio quantitativo sulla velocità di ricerca dei trie, supponendo di volerememorizzare i codici fiscali in un trie: ricordiamo che un codice fiscale contiene 9 lettereprese dall'alfabeto A . . . Z di 26 caratteri, e 7 cifre prese dall'alfabeto 0 . . . 9 di 10 caratteri,per un totale di 26 9

x IO7 possibili codici fiscali. Cercare un codice fiscale in un trierichiede di attraversare al più 16 nodi, indipendentemente dal numero di codici fiscalimemorizzati nel trie, in quanto l'altezza del trie è 16. In contrasto, la ricerca binaria oquella in un albero AVL, per esempio, avrebbe una dipendenza dal numero di chiavi: nelcaso estremo, memorizzando metà dei possibili codici fiscali in un array ordinato, tale

ricerca richiederebbe circa log(269

x IO7

) — 1 ^ 6 4 confronti tra chiavi. Il trie è quindiuna struttura di dati molto efficiente per la ricerca di sequenze.

L'inserimento di un nuovo elemento nel dizionario rappresentato con un trie seguenuovamente la sua definizione ricorsiva, come mostrato nel Codice 5.16: se il trie èvuoto viene creata una radice (righe 2-7) e, quindi, dopo aver verificato che la chiavedell'elemento non appaia nel dizionario (riga 8), il trie viene attraversato fino a trovareun figlio vuoto in cui inserire ricorsivamente il trie per il resto dei caratteri (righe 14—18)oppure il nodo esiste già ma l'elemento da inserire deve essergli associato (riga 21).

In altre parole, l'inserimento della stringa P cerca prima il suo più lungo prefisso x

che occorre in un nodo u del trie, analogamente alla procedura Ricerca, e scomponeP come xy: se y non è vuoto sostituisce il sottoalbero vuoto raggiunto con la ricercadi x, mettendo al suo posto il trie per y; altrimenti, sempl icemente associa P al nodo uidentificato.

Il costo dell'inserimento è O(m) tempo in accordo a quanto discusso per la ricerca(e la cancellazione può essere trattata in modo analogo): da notare che non occorronooperazioni di ribilanciamento (rotazioni o divisioni) come succede negli alberi di ricerca.Infatti, un'importante proprietà è che la forma del trie è determinata univocamente dalle

Page 201: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 201/373

 

Inserisci ( radiceTrie, e ): (pre: e. chiave ha lunghezza m)

IF (radiceTrie == nuli) {

radiceTrie = NuovoNodo( );

radiceTrie.dato = nuli;

FOR (c < 0; c < sigma; c = c + 1)

radiceTrie.figlio[c] = nuli;>IF (Ricercai radiceTrie, e.chiave ) == nuli) {

u = radiceTrie;

FOR  (i = 0; i < m; i = i+1) {

IF (u.figlio[ e.chiave[i] ] != nuli) {

u = u.figlio[ e.chiave[i] ];

> ELSE {

u.figlioli e.chiave[i] ] = NuovoNodo( );

u = u.figlio[ e.chiave[i] ];

u.dato = nuli;FOR (C < 0; C < sigma; C = C + 1)u.figlio[c] = nuli;

}>u.dato = e;

>

RETURN radiceTrie;

Codice 5.16 Algoritmo di inserimento di un elemento in un trie.

stringhe in esso contenute e non dal loro ordine di inserimento (contrariamente aglialberi di ricerca).

A L V I E : ricerca e inserimento in un trie

Osserva, sperimenta e verifica

Trie

Inoltre, poiché i trie preservano l'ordine lessicografico delle stringhe in esso conte-nute, possiamo fornire un semplice metodo per ordinare un array S di stringhe comemostrato nel Codice 5.17, in cui adoperiamo la funzione Recupera del Codice 5.15per effettuare una visita simmetrica del trie costruito attraverso l'inserimento iterativodelle stringhe contenute nell'array S.

•O- O 'O- O- O 'O ' •O • O ' o •

• -o • o • •

Page 202: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 202/373

 

OrdinaLessicograficamente( S ) : (pre: S è un array din stringhe)

radiceTrie = nuli;

elemento.sat = nuli;

FOR  (i = 0; i < n; i = i+1) {

elemento.chiave = S[i];

radiceTrie = Inserisci( radiceTrie, elemento );>numStringhe = 0;

Recuperai radiceTrie, S );

RETURN S;

Codice 5.17 Algoritmo di ordinamento di stringhe (fa uso di una variabile globale numStringhe).

La complessità dell'ordinamento proposto nel Codice 5.17 è O(N) tempo se la som-ma delle lunghezze delle n stringhe in S è pari a N. Un algoritmo ottimo per confronti,come il mergesort, impiegherebbe O(nlogn) confronti, dove però ciascun confronto ri-chiederebbe di accedere med iament e a N / n caratteri di una stringa, per un totale diO( ^ -n l o gn ) = 0 (N log n) tempo: l'ordinamento di stringhe con un trie è quindi piùefficiente in tal caso (radixsort). Analogamente a quanto discusso per la ricerca in tabellehash, il limite così ottenuto non contraddice il limite inferiore (in questo caso sull'ordi-nameno) poiché i caratteri negli elementi da ordinare sono utilizzati per altre operazionioltre ai confronti.

A L V I E : ordinamento in un trie

Osserva, sperimenta e verifica_ . „TrieSort »-o o - •

Nati per ricerche come quelle discusse finora, i trie sono talmente flessibili da risulta-re utili per altri tipi di ricerche più complesse. Inoltre, sono utilizzati nella compressionedei dati, nei compilatori e negli analizzatori sintattici. Servono a completare termini spe-cificati solo in parte; per esempio, i comandi nella shell, le parole nella composizione deitesti, i numeri telefonici e i messaggi SMS nei telefoni cellulari, gli indirizzi del Web odella posta elettronica. Permettono la realizzazione efficiente di correttori ortografici, dianalizzatori di linguaggio naturale e di sistemi per il recupero di informazioni mediantebasi di conoscenza. Forniscono operazioni di ricerca più complesse di quella per prefissi,come la ricerca con espressioni regolari e con errori. Permettono di individuare ripeti-zioni nelle stringhe (utili, per esempio, nell'analisi degli stili di scrittura di vari autori) e

Page 203: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 203/373

 

di recuperare le stringhe comprese in un certo intervallo. Le loro prestazioni ne hannofavorito l'impiego anche nel trattamento di dati multidimensionali, nell'elaborazione deisegnali e nelle telecomunicazioni. Per esempio sono uti lmente impiegati nella codificae decodifica dei messaggi, nella risoluzione dei conflitti nell'accesso ai canali e nell'istra-damento veloce dei router in Internet . A fronte della loro dutti lità, i trie hanno unastruttura sorprendemente semplice.

Purtroppo essi presentano alcuni svantaggi dal punto di vista dello spazio occupatoper alfabeti grandi, in quanto ciascuno dei loro nodi richiede l'allocazione di un arraydi ff elementi: inoltre, le loro prestazioni possono peggiorare se il linguaggio di pro-grammazione adottato non rende disponibile un accesso efficiente ai singoli caratteridelle stringhe. Esistono diverse alternative per l'effettiva rappresentazione in memoria diun trie che sono basate sulla rappresentazioni dei suoi nodi mediante strutture di datialternative agli array come le liste, le tabelle hash e gli alberi binari.

5.6.2 Trie compatti e alberi dei suffissi

I trie discussi finora hanno una certa ridondanza nel numero dei nodi, in quanto quellicon un solo figlio non vuoto rappresentano una scelta obbligata e non raffinano ulterior-mente la ricerca nei trie, al contrario dei nodi che hanno due o più figli non vuoti. Taleridondanza è rimossa nel trie compatto, mostrato nella parte sinistra della Figura 5.6,dove i nodi con un solo figlio non vuoto sono altrimenti rappresentati per preservare lefunzionalità del trie: a tal fine, gli archi sono etichettati utilizzando le sottostringhe dellechiavi appartenenti agli elementi dell'insieme S, invece che i loro singoli caratteri.

Formalmente, dato il trie per le chiavi contenute negli elementi dell'insieme S, classi-fichiamo un nodo del trie come unar io se ha esattamente un figlio non vuoto. Una catenadi nodi unari è una sequenza massimale uo,ui,... ,u r _ i di r ^ 2 nodi nel trie tale checiascun nodo Ut è unario per 1 ^ i ^ r — 2 (notiamo che Uo potrebbe essere la radice op-pure u r _ i potrebbe essere una foglia). Sia Ci il carattere per cui Ui = Ui_i .f ig l io [ c i ] e(3 = C] • • • c r _i la sottostringa ottenuta dalla concatenazione dei caratteri incontrati per-correndo la catena da ito a u r _i: definiamo l'operazione di compattazione della catenasostituendo l'intera catena con la coppia di nodi uo e u r _ j collegati da un singolo arco(uo,uT_i ), che è concettualmente etichettato con |3.

Notiamo infatti che l'esplicita memorizzazione di (3 non è necessaria se associamo,a ciascun nodo u, il prefisso ottenuto percorrendo il cammino dalla radice fino a u (laradice ha quindi un prefisso vuoto): in tal modo, indicando con a il prefisso nel padredi u e con y quello in u, possiamo ricavare 3 per differenza in quanto a(3 = y e lasottostringa |3 è data dagli ultimi r — 1 = ||3| caratteri di y.

Il trie compatto per l'insieme S è ottenuto dal trie costruito su S applicando l'opera-zione di compattazione a tutte le catene presenti nel trie stesso. Ne risulta che i nodi deltrie compat to sono foglie oppure hanno almeno due figli non vuoti. Per implementare

Page 204: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 204/373

 

Figura 5.6 A sinistra, il trie compatto per memorizzare i nomi di alcune province; a destra, laversione con le sottostringhe rappresentate mediante triple.

un trie compatto, estendiamo l'approccio adottato per i trie, utilizzando la rappresenta-zione degli alberi cardinali cr-ari (Paragrafo 4.4): ciascun nodo u di un trie compatto èdotato di un campo u . d a t o in cui memorizzare un elemento e s S (quindi i campi die sono indicati come u . d a t o . c h i a v e e u . d a t o . s a t ) a cui aggiungiamo un campou . p r e f i s s o per memorizzare il prefisso associato a u.

Tale prefisso è memorizzato mediante una coppia (e, j) per indicare che esso è datodai primi j caratteri della stringa contenuta nel campo e . ch i a v e : il vantaggio è che rap-

presentiamo ciascun prefisso con soli due interi indipendentemente dalla lunghezza delprefisso stesso, perché lo spazio richiesto da ciascun nodo rimane O(a) (purché i campichiave degli elementi siano memorizzati a parte). La parte destra della Figura 5.6 mostraun esempio di tale rappresentazione, dove possiamo osservare che un nodo interno u ap-pare nel trie compatto se e solo se, prendendo il prefisso a associato a u, esistono almenodue caratteri c / c' dell'alfabeto L tali che ent rambi ac e ac ' sono prefissi delle chiavidi alcuni elementi in S.

La presenza di due chiavi, una prefisso dell'altra, potrebbe introdurre nodi unari chenon possiamo rimuovere. Per tale ragione, estendiamo tutte le chiavi degli elementi in Scon un ulteriore carattere $, che è un simbolo speciale da usare come terminatore distringa (analogamente al carattere '\0' nel linguaggio C). In tal modo, poiché $ appa-re solo in fondo alle stringhe, nessuna può essere prefisso dell'altra e, come osservatoin precedenza, esiste una corrispondenza biunivoca tra le n chiavi e le n foglie del triecompatto costruito su di esse: quindi, presumiamo che ciascuna delle n foglie contengaun distinto elemento e € S (in particolare, illustriamo questa corrispondenza nella Fi-gura 5.6 etichettando con i la foglia contenente ti). Utilizzando il simbolo speciale e larappresentazione dei prefissi mediante coppie, il trie compatto ha al più n nodi interni e

Page 205: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 205/373

 

RicercaPref issi( radiceTrieCompatto, P ): {pre: P contienem caratteri)

u = radiceTrieCompatto;

fine = false;

i = 0;

 WHILE (¡fine kk i < M) {

IF (u.figlioli P[i] ] != nuli) {u = u.figliot P[i] ];

<e, j> = u.prefisso;

 WHILE (Ci < m) kk (i < j) kk (P[i] == e.chiave[i]))

i = i + 1;

fine = (i < m) kk (i < j);

} ELSE {

fine = true;

>>

numStringhe = 0;Recuperai u, elenco );

Co di ce 5 .1 8 Algori tmo di ricerca per prefissi in un trie compatto (fa uso di una variabile globalenumStringhe e della funzione Recupera del Codice 5.15).

n foglie, e quindi richiede O(na) spazio, dove a = 0(1) per le nostre ipotesi: lo spazio

dipende quindi solo dal numero n delle stringhe nel caso pessimo e non dalla somma N

delle loro lunghezze, contrariamente al trie.La rappresentazione compatta di un trie non ne pregiudica le caratteristiche discusse

finora. Per esempio la ricerca per prefissi in un trie comp at to simula quella per prefissi in

un trie (Codice 5.15) ed è mostrata nel Codice 5.18: la differenza risiede nelle righe 8-

11 dove, dopo aver raggiunto il nodo u, ne prendiamo il prefisso a esso associato e

ne confrontiamo i caratteri con P, dalla posizione i in poi, fino alla fine di una delle

due stringhe oppure quando troviamo due caratteri differenti (riga 9). Analogamente

alla ricerca nei trie, terminiamo di effettuare confronti quando tutti caratteri di P sono

stati esaminati con successo oppure troviamo il suo più lungo prefisso che occorre nel

trie compatto, e il costo del Codice 5.18 rimane pari a 0(m) tempo più il numero dioccorrenze riportate. L'operazione R i c e r c a è realizzata in mo do analogo a quella per

prefissi e mantien e la complessità di 0( m ) tempo.

Analogamente alla ricerca, l'inserimento di un nuovo elemento e nel trie compatto

richiede 0 ( m ) tem po come mostrato nel Codice 5.19. Do po aver creato la radice (ri-

ghe 2-8), se necessario, a cui associamo il prefisso vuoto (di lunghezza 0), verifichiamo

che l'el emento non sia già nel dizionario . A questo pun to , procediamo come nel caso

della ricerca per prefissi per identificare il più lungo prefisso x della chiave di e che oc-

Page 206: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 206/373

 

Inserisci( radiceTrieCompatto, e ): (pire: e. chiave ha lunghezza m)

IF (radiceCompattoTrie == nuli) {

radiceTrieCompatto = NuovoNodo( );

radiceTrieCompatto.prefisso = <e, 0>;

radiceTrieCompatto.dato = nuli;

FOR (c < 0; c < sigma; c = c + 1)radiceTrieCompatto.figlio[c] = nuli;

>IF (Ricercai radiceTrieCompatto, e.chiave ) == nuli) {

u = radiceTrieCompatto; fine = false; i = 0;

 WHILE (Ifine Sete i < m) {

v = u;

indice = i;

IF (u.figlio[ e.chiave[i] ] != nuli) {

u = u.figlio[ e.chiave[i] ];

<p, j> = u.prefisso; WHILE (i < M  && i < j && p.chiave[i] == e.chiave[i])

i = i + 1;

fine = (i < m) && (i < j) ;

> ELSE {

fine = true;

>>IF (fine) CreaFoglia( v, u, indice, i );

>

RETURN radiceTrieCompatto;

Codice 5.19 Algoritmo per l'inserimento di un elemento in trie compatto.

corre nel trie compatto, dove P = xy. Sia u il nodo raggiunto e v il nodo calcolato nellerighe 1 1-2 3 e sia i = |x|: se x coincide con il prefisso associato a u, allora v = u ; seinvece, x è più breve del prefisso associato a u , allora v è il padre di u . Invochiamo oraCreaFoglia, descritta nel Codice 5.20, che fa la seguente cosa: se u ^ v, spezza l'arco(u,v) in due creando un nodo intermedio a cui associa x come prefisso (corrispondenteai primi i caratteri di e. chiave) e a cui aggancia la nuova foglia che memorizza l'ele-mento e, la cui chiave ne diventa il prefisso di lunghezza m (in quanto prendiamo tuttii caratteri di e. chiave); se invece u = v, crea soltanto la nuova foglia come descrittosopra, agganciandola però a u. Ricordiamo che, non essendoci una chiave prefisso diun'altra, ogni inserimento di un nuovo elemento crea sicuramente una foglia.

Infine, la cancellazione del l'elemento avente chiave uguale a P, specificata in ingresso,richiede la rimozione della foglia raggiunta con la ricerca di P, nonché dell'arco che

Page 207: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 207/373

 

CreaFoglia ( v , u , ind ice , i ) : (pre: e.chiave ha lunghezza m)IF (v != u) {v.figlio[ e.chiave[indice] ] = NuovoNodo( );

v = v.figlio[ e.chiave[indice] ];

v.prefisso = <e, i>;

v.dato = nuli;FOR (C < 0; C < sigma; C = C + 1)

v.figlio[c] = nuli;

<p, j> = u.prefisso;

v.figlio[ p.chiave[i] ] = u;

u = v;

>u.figlio[ e.chiave[i] ] = NuovoNodo( );

u = u.figliof e.chiavefi] ];

u.prefisso = <e, m>;

u.dato = e;FOR (c < 0; c < sigma; c = c + 1)

u.figlio[c] = nuli;

Codice 5.20 Algoritmo per la creazione di una foglia e di un eventuale nodo (suo padre).

collega la foglia a suo padre u. Se u diventa unario, allora dobbiamo rimuovere u, il suoarco entrante e il suo arco uscente, sostituendoli con un unico arco la cui etichetta è la

concatenazione della sottostringa nell'arco entrante con quella nell'arco uscente.Tuttavia dobbiamo stare attenti a non usare elementi cancellati per i prefissi associati

ai nodi u del trie compatto: se e viene cancellato e un antenato u della corrispondentefoglia contiene il prefisso (e, j), allora è sufficiente individuare un altro elemento e' con-tenuto in una foglia che discende da u e sostituire quel prefisso con (e', j). Il costo dellacancellazione è O( m) poiché a = 0(1) .

A L V I E : trie compatto

Osserva, sperimenta e verificaCompactTrie

Uno dei motivi per introdurre le complicazioni della gestione dei trie compatti, ri-

spetto a quella più semplice dei trie, è il loro utilizzo per una struttura di dati con un

numero sempre crescente di applicazioni. Dato un testo T di n caratteri in cui l'ultimo

Page 208: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 208/373

 

Figura 5.7 Albero dei suffissi per T = banana$.

carattere è il simbolo speciale $, definiamo il suo suffisso i come il segmento compostodagli ultimi caratteri T[i, n — 1], per 0 ^ i ^ n — 1.

Un albero dei suffissi (su f f i x tree) per T è il trie compatto costruito su n chiavi, chesono tut ti i suffissi i di T per 0 ^ i ^ n — 1. Un esempio dell'albero dei suffissi per iltesto T = banana$ è mostrato nella Figura 5.7, come risulta dalla costruzione del triecompatto sulle seguenti chiavi (che sono i suffissi di T numerati da 0 a 6):

0. ba na na $ 1. ana ii a$ 2. na na $ 3. an a$ 4. na $ 5. a$ 6 . $Gli alberi dei suffissi vengono studiati per la loro importanza nella costruzione di

indici testuali che risultano più potenti delle liste invertite: infatti, l'utilizzo degli alberidei suffissi non necessita di dividere il testo in segmenti che rappresentano i terminima, piuttosto, ognuno dei (2) = 0(n 2 ) segmenti del testo è un potenziale termine diricerca, come succede nelle sequenze biologiche. Il vantaggio di impiegare un alberodei suffissi, essendo un trie compatto con n foglie, è che lo spazio occupato è linearea fronte di un numero 0(n 2 ) di potenziali termini: è chiaro che memorizzando talitermini nel vocabolario V delle liste invertite avremmo spazio quadratico o superiore,

mentre con l'albero dei suffissi tale spazio è soltanto di O(n) celle di memoria inclusequelle necessarie a memorizzare T. Not iamo che, applicando l'inser imento descrittonel Codice 5.19 ai vari suffissi, possiamo costruire l'albero dei suffissi per T in tempoLr =o |T[i ,n - 1]| = Lr=O' (T-L — i) = 0(n 2 ) (otteniamo tale costo quando, ad esempio,i primi n — 1 caratteri di T sono tutti uguali): esistono vari algoritmi che permettono lacostruzione di tale struttura di dati richiedendo soltanto O(n) tempo.

L'impiego degli alberi dei suffissi nella costruzione degli indici per documenti, si ba-sa sulla seguente proprietà fondamentale delle occorrenze: un termine P di m caratteri

Page 209: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 209/373

 

occorre in posizione i del testo T (ovvero P = T[i, i — m + 1]) se e solo se P è prefissodel suffisso T[i, n — 1]. In altre parole, il problema della ricerca in un testo {pattern mat-ching) può essere formulato in termini della ricerca per prefissi descritta nel Codice 5.18.Tale ricerca non è applicata a stringhe indipendenti, bensì ai suffissi del testo come ab-biamo mostrato sopra: una volta individuato il nodo u che corrisponde a P, le foglie idiscendenti da u corrispondono esattamente alle posizioni i del testo in cui P occorre.

Per esempio, nella Figura 5.7, la ricerca di P = ana conduce al nodo le cui fogliediscendenti sono i = 1 e i = 3, che sono anche le occorrenze di P. La proprietà fon-damentale riduce quindi il problema di cercare P in T al problema di cercare P nel triecompatto e, quindi, il costo della ricerca è indipendente dalla lunghezza del testo, ovveroè O(m) più il numero di occorrenze trovate (osserviamo che la proprietà fondamenta-le vale non solo per la ricerca esatta, ma anche per altri tipi di ricerca che utilizzanol'albero dei suffissi). Volendo utilizzare l'albero dei suffissi per una collezione di docu-

menti D = {To,Ti,..., Ts_i}, basta che lo costruiamo sul testo T = To$Ti$ • • • $T s_i$ottenuto concatenando i testi in D separati dal simbolo speciale $: poiché P non contienetale simbolo, le sue occorrenze sono esclusivamente all'interno di ciascun documento.

Concludiamo il paragrafo discutendo alcune tra le molteplici applicazioni dell'alberodei suffissi che dimostra avere "una miriade di virtù" (alcuni studi recenti mostrano comeridurre ulteriormente lo spazio richiesto dagli alberi dei suffissi per renderlo paragonabilea quello richiesto dalle liste invertite).

Una prima applicazione è quella di memorizzare, in modo compatto, la statisticasulle sottostringhe. Prima dell'invenzione dell'albero dei suffissi, alcuni problemi erano

ritenuti difficili: un esempio è dato dal trovare, in tempo O(n), la più lunga sottostrin-ga che appare in T almeno due volte. La soluzione diventa semplice con l'utilizzo delsuddetto albero, in quanto tale sottostringa corrisponde a uno dei prefissi più lunghimemorizzati in un nodo interno dell'albero.

Una seconda applicazione è nella compressione dei dati con i metodi basati su di-zionario, come l'algoritmo di sostituzione testuale Lempel-Ziv su cui si basa il com-pressore gzip, oppure con i metodi basati sull'ordinamento, come l'algoritmo dellatrasformata di Burrows-Wheeler su cui si basa il compressore b z i p . In quest'ul timocaso, dobbiamo ordinare lessicograficamente i suffissi del testo e questo ordinamento è

facilmente reperibile eseguendo una visita simmetrica nell'albero dei suffissi (analoga alCodice 5.17).

Un'ulteriore applicazione è nella ricerca approssimata di stringhe, in cui è rilevan-te l'uso di una primitiva lcp(i, j) che restituisce il massimo numero di caratteri inizialiuguali tra i due suffissi T[i,n — 1] e T[j,n — 1], per 0 ^ i,) < n, in tempo costante.Dopo aver applicato l'algoritmo del minimo antenato comune (Paragrafo 4.2) all'alberodei suffissi, ogni volta che perviene un'interrogazione lcp(i, j), possiamo restituire ilvalore richiesto in tempo costante prendendo le foglie i e j nell'albero dei suffissi, indi-

Page 210: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 210/373

 

viduando il loro minimo antenato comune u. e restituendo la seconda componente diu . p r e f i s s o , che rappresenta la lunghezza del prefisso corrispondente a u.

RIEPILOGO

  In questo capitolo abbiamo descritto come realizzare un dizionario utilizzando le tabellehash, gli alberi di ricerca, gli alberi AVL, i B-alberi e, infine, i trie o alberi digitali diricerca, inclusi gli alberi dei suffissi e le liste invertite, discutendo la complessità di ciascunarealizzazione.

ESERCIZI

1. Mostrate come estendere i dizionari discussi nel capitolo in modo che possanogestire multi-insiemi con chiavi eventualmente ripetute.

2. Valutate il costo della rappresentazione dei grafi con liste di adiacenza realizzate

mediante dizionari.

3. Descrivete la cancellazione fisica da tabelle hash a indirizzamento aperto.

4. Dimostrate per induzione su h. che rin = Fk+3 — 1 negli alberi di Fibonacci.

5. Mostrate che la complessità dell'inserimento in un AVL cambia significativamentese sostituiamo la funzione Altezza del Codice 5.7 con quella ricorsiva definitanel Capitolo 4.

6. Mostrate che, dopo aver ribilanciato tramite le rotazioni un nodo critico a seguitodi un inserimento, non ci sono altri nodi critici.

7. Mostrate che, analogamente a quanto fatto per la ricerca binaria, D(Iog B n) è illimite inferiore per il numero di trasferimenti necessari nella ricerca di chiavi perconfronti (ogni nuovo blocco caricato in memoria principale aumenta le possibiliscelte di una fattore almeno pari a B/2).

8. Supponendo che la lista di nodi su ciascun livello del B-albero sia bidirezionale eutilizzando il B-albero in memoria principale con B = 4 e i puntatori ai padri,

descrivete un algoritmo di ricerca chiamato finger search, il cui costo è O(logd)tempo dove d < n è il numero di chiavi che intercorrono nella lista delle fo-glie tra la chiave attualmente cercata k e l'ultima chiave cercata k' (mantenete unriferimento chiamato finger a k', aggiornandolo con ogni ricerca).

9. Mostrate come gestire il campo u. p a d r e negli alberi binari di ricerca, negli alberiAVL e nei B-alberi (in questi ultimi, l'inserimento può richiedere 0(B logB n) tra-sferimenti ma il costo ammortizzato diventa 0(logB n) utilizzando il riferimentoalla lista dei nodi sullo stesso livello).

Page 211: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 211/373

 

10. Discutete come realizzare, per i dizionari ordinati, le operazioni S u c c e s s o r e ,Predecessore, Intervallo e Rango descritte nel Paragrafo 5.1, utilizzandogli alberi AVL, i B-alberi e i trie, analizzandone la complessità.

11. Un'occorrenza della stringa P nella posizione i del testo T ha al più k mismatch se

il numero di posizioni in cui i caratteri di P e T[i, i + m— 1] differiscono è al più k,dove m è la lunghezza di P e k ^ m. Utilizzando la primitiva l c p , mostrate cometrovare tutte le occorrenze di P in T con al più k mismatch in tempo O(nk), doven è la lunghezza di T.

Page 212: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 212/373

 

Capitolo 6

Grafi

SOMMARIO  In questo capitolo esaminiamo le caratteristiche principali dei grafi, fornendo le definizio-ni relative a tali strutture. Mostriamo come attraverso di essi sia possibile modellare unaquantità di situazioni e come molti problemi possano essere interpretati come problemi su

grafi. Introduciamo poi il paradigma dell'algoritmo goloso applicandolo alla risoluzione del  problema della colorazione e di quello del massimo insieme indipendente nel caso di grafia intervalli. Infine, studiamo il concetto di rete complessa e mostriamo come sia possibilesfruttare la struttura a grafo del World Wide Web per rendere più efficaci le ricerche didocumenti in esso contenuti.

DIFFICOLTÀ

I CFU

6.1 Grafi

II collegamento tra due nodi nelle liste rappresenta la relazione tra predecessore e suc-cessore, mentre il collegamento negli alberi rappresenta la relazione tra figlio e padre. Icollegamenti nei grafi rappresentano una generalizzazione di tali relazioni e includono,come caso particolare, la relazione espressa da liste e alberi: il collegamento tra due nodiin un grafo rappresenta una relazione di adiacenza o di vicinanza tra tali nodi.

L'importanza dei grafi deriva dal fatto che una grande quantità di situazioni può es-

sere modellata e rappresentata mediante essi, e quindi una grande quantità di problemipuò essere espressa per mezzo di problemi su grafi: gli algoritmi efficienti su grafi rappre-sentano alcuni strumenti generali per la risoluzione di numerosi problemi di rilevanzapratica e teorica.

Un grafo G è definito come una coppia di insiemi finiti G = (V, E), dove V rappre-senta l'insieme dei nodi o vertici e le coppie di nodi in E C V x V sono chiamate archi o

Page 213: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 213/373

 

Figura 6.1 Rotte areee di collegamento tra alcune capitali europee.

lati. Il numero n = |V| di nodi è detto ordine del grafo, mentre m = |E| indica il numerodi archi (i due numeri possono essere estremamente variabili, l'uno rispetto all'altro).

La dimensione del grafo è data dal numero n + m totale di nodi e di archi, per cuila dimensione dei dati in ingresso è espressa usando due parametri nel caso dei grafi,contrariamente al singolo parametro adottato per la dimensione di array, liste e alberi.

Infatti, n e m vengono considerati parametri indipendenti, per cui nel caso di grafi lacomplessità lineare viene riferita al costo 0 ( n + m) di entrambi i parametri. In generale,vale 0 m ^ (2) poiché il numero massimo di archi è dato dal numero (2) = 0 ( n 2 ) ditutte le possibili coppie di nodi: il grafo è sparso se m = O(n) e denso se m = @(n2).

Nella trattazione di grafi un arco viene inteso come un collegamento tra due nodiu e v e viene rappresentato con la notazione (u, v) (che, come vedremo, è un piccoloabuso per semplificare la notazione del libro). Ciò è motivato dalla descrizione graficautilizzata per rappresentare grafi, in cui i nodi sono elementi grafici (punti o cerchi) e

Page 214: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 214/373

 

gli archi sono linee colleganti le relative coppie di nodi, questi ultimi detti terminali o

estremi degli archi.

Consideriamo l'esempio mostrato nella Figura 6.1, che riporta le rotte di una no-ta compagnia aerea relativamente all'insieme V degli aeroporti dislocati presso alcune

capitali europee, identificate mediante il codice internazionale del corrispondente aero-porto: BVA (Parigi), CIA (Roma), CRL (Bruxelles), DUB (Dublino), MAD (Madrid), NYO(Stoccolma), STN (Londra), SXF (Berlino), TRF (Oslo).

Possiamo rappresentare le rotte usando una forma tabellare come quella riportata inbasso nella Figura 6.2, in cui la casella all'incrocio tra la riga x e la colonna y contiene iltempo di volo (in minuti ) per la rotta che collega gli aeroport i x e y. La casella è vuotase non esiste una rotta aerea.

Tale rappresentazione è mostrata graficamente mediante uno dei due grafi in altonella Figura 6.2, dove ciascun arco (x ,y) € E rappresenta la rotta tra x e y etichettata

con il tempo di volo corrispondente (osserviamo che una lista lineare o un albero nonriescono a modellare l'insieme delle rotte).

Un grafo G = ( V, E) è detto pesato o etichettato sugli archi se è definita una funzioneW : E i-> 1 che assegna un valore (reale) a ogni arco del grafo. Nell'esempio dellaFigura 6.2, i pesi sono dati dai tempi di volo. Nel seguito, con il termine grafo pesatoG = (V, E, W) indicheremo un grafo pesato sugli archi.

L'esempio mostrato nelle Figure 6.1 e 6.2 illustra una serie di nozioni sulla percor-ribilità e raggiungibilità dei nodi di un grafo. Dato un arco (u,v) , di remo che i nodiu e v sono adiacenti e che l'arco (u, v) è incidente a ciascuno di essi: in altri termini,

un arco (u,v) è incidente al nodo x se e solo se x = u oppure x = v. Il numero diarchi incidenti a un nodo è detto grado del nodo e un nodo di grado 0 è detto isolato.Facendo riferimento al grafo nell'esempio, il nodo CIA ha grado pari a 5 mentre il nodoMAD è isolato.

Una proprietà che viene spesso utilizzata è che la somma dei gradi dei nodi è paria 2m (il doppio del numero di archi). Per mostrare ciò, dobbiamo vedere ciascun arcocome incidente a due nodi, per cui la presenza di un arco fa aumentare di 1 il grado dientrambi i suoi due estremi: il contributo di ogni arco alla somma dei gradi è pari a 2.Invece di sommare i gradi di tutti i nodi, possiamo calcolare tale valore moltiplicandoper 2 il numero m di archi. Nell 'esempio, m = 12 e la somma dei gradi è 24.

E naturale chiederci se, a partire da un nodo, è possibile raggiungere altri nodi at-traversando gli archi: relativamente al nostro esempio, vogliamo sapere se è possibileandare da una città a un'altra prendendo uno o più voli. Tale percorso viene modellatonei grafi attraverso un cammino da un nodo u a un nodo z, definito come una sequenzadi nodi x0,xi,x2,...,x^ tale che xo = u, x^ = z e (xi,xi + i ) e E per ogni 0 ^ i < k:l'intero k ^ 0 è detto lunghezza del cammino. Un ciclo è un cammino per cui valex0 = xk , ovvero un cammino che ritorna nel nodo di partenza. Un cammino (o un

Page 215: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 215/373

 

BVA90 "

120DUB- 140

'9095 120 * 175

CRL- 130 -NY0160 I 3 5

MAD STN

110 1 1 5

190 CIA120 120

BVA90 "

175DUB- 140 160

95

'CRL-130 >NYO

135

MAD STN

HO 115

SXF TRF SXF. TRF

SXF CRL DUB STN MAD TRF BVA CIA NY0SXF - - - 110 - - - - -

CRL - - 95 - - - - 120 130DUB - 95 - - - - 90 190 -

STN 110 - - - - 115 - 160 135MAD -

TRF - - - 115 - - - - -

BVA - - 90 - - - - 120 140CIA - 120 190 160 - - 120 - 175NY0 - 130 - 135 - - 140 175 -

Figura 6.2 Rappresentazione a grafo (con n = 9 nodi e m = 12 archi) e tabel lare delle rottemostrate nella Figura 6.1.

ciclo) è semplice se non attraversa alcun nodo più di una volta, ossia se non esiste alcunciclo annidato al suo interno.

Nella Figura 6.2 esiste un cammino semplice di lunghezza k = 3 da u = BVA az = SXF, dato da BVA, NY0, STN, SXF. Il cammino BVA, CIA, DUB, CRL, CIA, NY0, STN,SXF non è semplice a causa del ciclo CIA, DUB, CRL, CIA. Invece, STN, TRF, SXF non è

un cammino in quanto TRF e SXF non sono collegati da un arco.Un cammino minimo da u a z è caratterizzato dall'avere lunghezza minima tra tutti

i possibili cammini da u. a z: in altre parole, vogliamo sapere qual è il modo di andaredalla città u alla città z usando il minor numero di voli.

Nell 'esempio, sia BVA, CIA, STN, SXF che BVA, NY0, STN, SXF sono cammini mi-nimi. La distanza tra due nodi u e z è pari alla lunghezza di un cammino min imo cheli congiunge e, se tale cammino non esiste, è pari a +oo: la distanza tra BVA e SXF è 3mentre tra BVA e MAD è +oo.

Page 216: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 216/373

 

190 CIA 190 CIA120

BVA BVA;

90 ' 90 . '175 '' \ 175

DUB DUB \ 140

NfO NYO

MAD MAD

TRF TRF

Figura 6 .3 Un sottografo e un sottografo indotto del grafo a destra nella Figura 6.2.

Nel caso di grafi pesati ha senso definire il peso di un cammino come la somma deipesi degli archi attraversati (che sono quindi intesi come "lunghezze" degli archi), ovverocome ^ J q W(xi,xi + i): nel nostro esempio, ipotizzando che il tempo di commutazio-ne tra un volo e il successivo sia nullo, vogliamo sapere qual è il modo più veloce perandare da una città a un'altra (a differenza del cammino minimo).

Il cammino minimo pesato è il cammino di peso minimo tra due nodi e la distanzapesata è il suo peso (oppure +oo se non esiste alcun cammino tra i due nodi). Nel nostroesempio, il cammino minimo pesato è BVA, NYO, STN, SXF (e quindi la distanza pesataè 385) perché il cammino BVA, CIA, STN, SXF ha peso pari a 390 (non è detto che uncammino minimo pesato debba essere anche un cammino minimo).

I cammini permettono di stabilire se i nodi del grafo sono raggiungibili: due nodiu e z sono detti connessi se esiste un cammino tra di essi. Nell 'esempio i nodi BVA eSXF sono connessi, in quanto esiste il cammino BVA, NYO, STN, SXF che li congiunge,mentre i nodi BVA e MAD non lo sono. Un grafo in cui ogni coppia di nodi è connessa èdetto a sua volta connesso.

Dato un grafo G = (V, E), un sottografo di G è un grafo G' = (V', E') composto daun sottoinsieme dei nodi e degli archi presenti in G: ossia, V' C V ed E' C V' x V' e,inoltre, vale E' C E. Nella Figura 6.3 è mostrato a sinistra un sottografo del grafo presen-tato nella Figura 6.2. Se vale la condizione aggiuntiva che in E' appaiono tutti gli archidi E che connettono nodi di V', allora G' viene denominato sottografo indotto da V'.E sufficiente specificare solo V' in tal caso: il grafo mostrato a destra nella Figura 6.3 è ilsottografo indotto dall'insieme di nodi V' = {BVA, CIA, DUB, NYO, MAD, TRF}.

Possiamo quindi definire una componente connessa di un grafo G come un sotto-grafo G' connesso e massimale di G, vale a dire un sottografo di G avente tutti nodi

Page 217: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 217/373

 

Fig ur a 6 .4 Parte della rete stradale della città di Pisa, dove le strade a doppio senso di circolazionesono rappresentate mediante una coppia di archi aventi etichetta comune.

connessi tra loro e che non può essere esteso, in quanto non esistono ulteriori nodi inG che siano connessi ai nodi di G'. All'interno di una componente connessa possiamoraggiungere qualunque nodo della componente stessa, mentre non possiamo passare dauna componente all'altra percorrendo gli archi del grafo: la richiesta di massimalità nellecomponenti connesse è motivata dall'esigenza di determinare con precisione tutti i nodiraggiungibili. Facendo riferimento al grafo nella Figura 6.2, il sottografo indotto dai no-di STN, SXF, TRF è connesso, ma non è una componente connessa del grafo, in quantopuò essere esteso, ad esempio, aggiungendo il nodo CIA. In effetti, il grafo in questionerisulta composto da due componenti connesse: la prima indotta dal nodo isolato MAD

e la seconda indot ta dai restanti nodi. Come possiamo osservare, un grafo connesso ècomposto da una sola componente connessa.1

Un grafo completo o cricca (clique) è caratterizzato dall'avere tutti i suoi nodi a duea due adiacenti. Facendo riferimento al grafo nella Figura 6.2, il sottografo indotto dainodi CIA, CRL, DUB è una cricca (anche se non è una componente connessa in quantoesistono altri nodi, come BVA, che sono collegati alla cricca). La notazione K r è usata perindicare una cricca di r vertici e, nel nostro grafo, compaiono diversi sottografi che sonoK3 (ma nessun K4 vi appare).

I grafi discussi finora sono detti non orientati in quanto un arco (u, v) non è distin-guibile da un arco (v,u), per cui (u,v) = (v,u): entrambi rappresentanto simmetrica-mente un collegamento tra u e v. In diverse situazioni, tale simmetria è volutamenteevitata, come nel grafo illustrato nella Figura 6.4, che rappresenta la viabilità stradale dialcuni punti nella città di Pisa, con i sensi unici indicati da singoli archi orientati e lestrade a doppio senso di circolazione indicate da coppie di archi. Gli archi hanno un

 Lungarno Fibonacci

V. Bruno

' È interessante notare che un albero può essere equivalentemente visto come un grafo che è connesso enon contiene cicli, in cui un nodo viene designato come radice.

Page 218: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 218/373

 

senso di percorrenza e la notazione (u, v) indica che l'arco va percorso dal nodo u. versoil nodo v, e quindi (u ,v) ^ (v, u) , in quanto il grafo è orientato o diretto.2

L'arco (u, v) viene detto diretto da u a v, quindi uscente da u ed entrante in v. Ilnodo u è denominato nodo iniziale o di partenza mentre v è denominato nodo finale,

di arrivo o di destinazione. Il grado in uscita di un nodo è pari al numero di archiuscenti da esso, mentre il grado in ingresso è dato dal numero di archi entranti . Facendoriferimento al grafo nella Figura 6.4, il nodo B ha grado in ingresso pari a 4 e gradoin uscita pari a 1, mentre il nodo C ha grado in ingresso pari a l e grado in uscitapari a 3. Il grado è la somma del grado d'ingresso e di quello d'uscita: in un grafoorientato la somma dei gradi dei nodi in uscita è uguale a m, poiché ciascun arco fornisceun contributo pari a 1 nella somma dei gradi in uscita. Inoltre, il numero di archi è0 ^ m ^ 2 x (" ) poiché otteniamo il massimo numero di archi quando ci sono duearchi diretti per ciascuna coppia di nodi. Nel seguito, sarà sempre chiaro dal contesto seil grafo sarà orientato o meno.

Le definizioni viste finora per i grafi non orientati si ada ttano ai grafi orientat i. Uncammino (orientato) da un nodo u a un nodo z soddisfa la condizione che tutti gli archipercorsi nella sequenza di nodi sono orientati da u a z. In questo caso, diciamo che ilnodo u è connesso al nodo z (e questo non implica che z sia connesso a u perché ladirezione è opposta) . Allo stesso modo, possiamo definire un ciclo orientato come uncammino orientato da un nodo verso se stesso. Usando i pesi degli archi, la definizionedi cammino minimo (pesato o non) e la nozione di distanza rimangono inalterate. Lastessa cosa avviene per la definizione di sottografo. E importante evidenziare il concetto

di grafo fortemente connesso, quando ogni coppia di nodi è connessa, e di componentefortemente connessa: quest'ultima va intesa come un sottografo massimale tale che,per ogni coppia di nodi u e z, in esso esistono due cammini orientati all'interno delsottografo, uno da u a z e l'altro da z a u. Nel grafo nella Figura 6.4, le due componentifortemente connesse risultano dai sottografi indotti rispettivamente dai nodi A, B e daC, D, E, F, G. Nel presente e nei seguenti capitoli, discuteremo alcuni algoritmi che usanole nozioni introdotte finora, ipotizzando che i nodi siano numerati V = {0,1,... ,n — 1}.

6.1.1 Alcuni problemi su grafi

La versatilità dei grafi nel modellare molte situazioni, e i relativi problemi computazio-nali che ne derivano, sono ben illustrati da un esempio "giocattolo" in cui vogliamoorganizzare una gita in montagna per n persone.

2Nella teoria dei grafi, un arco che collega due nodi u e v di un grafo non orientato viene rappresentatocome un insieme di due nodi {u, v), spesso abbreviato come uv, m ent re se il grafo è orie nta to l'arco vienerappresentato con la coppia (u, v) (infatt i, {u, v) = {v,u} mentre ( u, v) ^ (v ,u )) . Con un picco lo abuso dinotazione, nel libro useremo (u , v) anche per gli archi non orienta ti, e in tal caso varrà (u , v) = (u, v), inquanto sarà sempre chiaro dal contesto se il grafo è orientato o meno.

Page 219: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 219/373

 

Figura 6. 5 Esempio di grafo delle conoscenze.

Per il viaggio, le poltrone nel pullman sono disposte a coppie e si vogliono assegnare

le poltrone ai partecipanti in modo tale che due persone siano assegnate a una coppia dipoltrone soltanto se si conoscono già (supponiamo che n sia un numero pari).

Una tale situazione può essere modellata mediante un grafo G = (V, E) delle "cono-scenze", in cui V corrisponde all'insieme degli n partecipanti ed E contiene l'arco (x,y)se e solo se le persone x e y si conoscono: nella Figura 6.5 è fornito un esempio di grafodi tale tipo.

In questo modello, un assegnamento dei posti che soddisfi le condizioni richiestecorrisponde a un sottoinsieme di archi E ' C E tale che tutt i i nodi in V siano incidentiagli archi di E' (quindi tutti i partecipanti abbiano un compagno di viaggio) e ogni nodo

in V compaia soltanto in un arco di E' (quindi ciascun partecipante abbia esattamen-te un compagno): tale sottoinsieme viene denominato abbinamento o accoppiamentoperfetto {perfect matching) dei nodi di G.

Nel caso del grafo mostrato nella Figura 6.5 esistono due abbinamenti diversi: ilprimo è {(v0,vi), (v2,v4), (v3,v 5)}, mentre il secondo è {(v0,v 4), (vi,v2), (v3,v5)}.

Un problema simile lo abbiamo già incontrato nel caso dei matrimoni stabili discussinel Capitolo 3: quest'ultimo può essere ora modellato come un abbinamento su un grafobipart ito G = (Vo, Vi ,E), caratterizzato dal fatto di avere due insiemi di vertici Vo e Vj(i clienti e le clienti nel nostro caso) tali che ogni arco (x ,y) e E ha gli estremi in insiemi

diversi, quindi x 6 Vo,y G Vi oppure x € Vi,y e Vo-Tornando alla gita, supponiamo che sia prevista un'escursione in quota per cui i

partecipanti devono procedere in fila indiana lungo vari tratti del percorso. Ancora unavolta, i partecipanti preferiscono che ognuno conosca sia chi lo precede che chi lo segue.

In tal caso, si cerca un cammino hamiltoniano (dal nome del matematico del XIXsecolo William Rowan Hamilton) ovvero un cammino che passi attraverso tutti i nodiuna e una sola volta: si tratta quindi di trovare una permutazione (7to,7ti,... , 7 t n _ i )

dei nodi che sia un cammino, ovvero (7ii,7ti +i) € E per ogni 0 ^ i ^ n — 2. Nel

Page 220: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 220/373

 

Fig ura 6.6 La città di Königsberg (immagine proveniente da ww v. wi ki p ed i a .o rg ) con eviden-ziati i ponti rappresentati da Euler come archi di un multigrafo che viene trasformatoin un grafo non orientato.

grafo considerato esistono quattro cammini hamiltoniani diversi, dati dalle sequenze( V 3 , V  5 , V 0 , V  1 , V 2 , V  4 ) , ( V 3 , V  5 , V o , V i , V  4 , V 2 ) , ( v  3 , V 5 , V o , V  4 , V 2 , V i ) e ( V 3 , V  5 , V 0 , V 4 , V  1 , V 2 ) . 3

Consideriamo quindi il caso in cui, giunti al ristorante del rifugio montano, vo-gliamo disporre i partecipanti intorno a un tavolo in modo tale che ognuno conosca isuoi vicini, di destra e di sinistra. Quel che vogliamo, ora, è un cammino hamiltonia-no nel grafo delle conoscenze in cui valga l'ulteriore condizione (7tn_i,7to) € E: tale

ordinamento prende il nome di ciclo hamiltoniano. A differenza del caso precedente,possiamo notare come una permutazione di tale tipo non esista per il grafo consideratonell'esempio.

Infine, tornati a valle, i partecipanti visitano un parco naturale ricco di torrenti cheformano una serie di isole collegate da ponti di legno. I partecipanti vogliono saperese è possibile effettuare un giro del parco attraversando tutti i ponti una e una solavolta, tornando al punto di partenza della gita. Il problema non è nuovo e, infatt i,il principale matematico del XVIII secolo, Leonhard Euler, lo studiò relativamente aiponti della città di Königsberg mostrati nella Figura 6.6, per cui l'origine della teoria

dei grafi viene fatta risalire a Euler. Le zone delimitate dai fiumi sono i vertici di ungrafo e gli archi sono i ponti da attraversare: nel caso che più ponti colleghino due stessezone, ne risulta un multigrafo ovvero un grafo in cui la stessa coppia di vertici è collegatada archi multipli, come nel caso della Figura 6.6. In tal caso, sost ituiamo ciascun arcomultiplo (x,y) da una coppia di archi (x,w) e (w, z), dove w è un nuovo vertice usatosoltanto per (x, y). In termini moderni, ne risulta un grafo G in cui vogliamo trovare un

3In realtà, i cammini sarebbero otto, considerando quelli risultanti da un percorso "al contrario" deiquattro elencati.

Page 221: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 221/373

 

ciclo euleriano, ovvero un ciclo (non necessariamente semplice) che attraversa tutti gliarchi una e una sola volta (mentre un ciclo hamiltoniano attraversa tutti i nodi una e unasola volta). E possibile attraversare tut ti i ponti come.richiesto se e solo se G ammetteun ciclo euleriano: Euler dimostrò che la condizione necessaria e sufficiente perché ciòavvenga è che G sia connesso e i suoi nodi abbiano tutti grado pari, pertanto il grafonella parte destra della Figura 6.6 non contiene un ciclo euleriano, in quanto presenta4 nodi di grado dispari, mentre è fàcile verificare che K5 ammette un ciclo euleriano inquanto tut ti i suoi nodi hanno grado 4. Euler dimostrò anche che se esattamente duenodi hanno grado dispari allora il grafo contiene un cammino euleriano, che comprendequindi ogni arco una e una sola volta: il grafo nella Figura 6.6 non contiene neanche uncammino euleriano. Come vedremo nei prossimi capitoli, la verifica che G sia connessorichiede tempo lineare, quindi il problema di Euler richiede 0(n + m) tempo e spazio.

I problemi esaminati sinora derivano dalla modellazione di numerosi problemi reali,e sono stati studiati nell'ambito della teoria degli algoritmi. È interessante a tale propo-sito osservare che, pur avendo tali problemi una descrizione molto semplice, l'efficienzadella loro soluzione è molto diversa: mentre per il problema dell'abbinamento e del cicloeuleriano sono noti algoritmi operanti in tempo polinomiale nella dimensione del grafo,per i problemi del cammino e del ciclo hamiltoniano non sono noti algoritmi polino-miali. Dato che è possibile verificare in tempo polinomiale se un cammino o un ciclopassano una e una sola volta per tutti i nodi, e quindi se sono hamiltoniani, ne derivache tali problemi appartengono alla classe NP, e in effetti è anche possibile dimostrarnela NP-completezza.

6.1.2 Rappresentazione di grafi

La rappresentazione utilizzata per un grafo è un aspetto rilevante per la gestione efficien-te del grafo stesso, e viene realizzata secondo due modalità principali (di cui esistonovarianti): le matrici di adiacenza e le liste di adiacenza.

Dato un grafo G = (V, E), la matrice di adiacenza A di G è un array bidimensionaledi n x n elementi in {0,1} tale che A[i][j] = 1 se e solo se (i, j) e E per 0 ^ i, j ^ n — 1.In altre parole, A[i][j] = 1 se esiste un arco tra il nodo i e il nodo j, mentre A[i][j] = 0altrimenti. Nella Figura 6.7 è fornita, a titolo di esempio, la matr ice di adiacenza del

grafo non orientato mostrato nella Figura 6.2 in cui il nodo i è associato alla riga i ealla colonna i, dove 0 ^ i < n, così fornendo, in corrispondenza degli elementi convalore 1; l'elenco dei nodi adiacenti a tale nodo. Se consideriamo grafi non orientati,vale A[i][j] = A[j][i] per ogni O ^ i , j ^ n — 1, e quindi A è una matrice simmetrica.Volendo rappresentare un grafo pesato, possiamo associare alla matrice di adiacenza unamatrice P dei pesi che rappresenta in forma tabellare la funzione W, come mostrato nellatabella in basso nella Figura 6.2 (talvolta le matrici A e P vengono combinate in un'unicamatrice per occupare meno spazio).

Page 222: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 222/373

 

SXF CRL DUB STN MAD TRF BVA CIA NYOSXF 0 0 0 1 0 0 0 0 0CRL 0 0 1 0 0 0 0 1 1DUB 0 1 0 0 0 0 1 1 0STN 1 0 0 0 0 1 0 1 1

MAD 0 0 0 0 0 0 0 0 0TRF 0 0 0 1 0 0 0 0 0BVA 0 0 1 0 0 0 0 1 1CIA 0 1 1 1 0 0 1 0 1NYO 0 1 0 1 0 0 1 1 0

Fi gu ra 6. 7 Matrice di adiacenza per il grafo nella Figura 6.2.

La rappresentazione di un grafo mediante matrice di adiacenza consente di verificarein tempo 0(1) l'esistenza di un arco tra due nodi ma, dato un nodo i, richiede tempoO(n) per scandire l'insieme dei nodi adiacenti, anche se tale insieme include un numerodi nodi molto inferiore a n, come mostrato dalle seguenti istruzioni:

FOR  ( j = 0; j < n; j = j+ 1) {IF (A[i] [j] ! = 0) {

PRINT a r c o ( i , j ) ;PRINT peso P [ i ] [ j ] de l l ' a rc o , s e p rev i s to ;

>>

Per scandire efficientemente i vertici adiacenti, conviene utilizzare un array conte-nente n liste di adiacenza. In questa rappresentazione, a ogni nodo I del grafo è associatala lista dei nodi adiacenti, detta lista di adiacenza e indicata con l i s t a A d i a c e n z a [ i ] ,che implementiamo come un dizionario a lista (Paragrafo 5.2) di lunghezza pari al gradodel nodo. Se il nodo ha grado zero, la lista è vuota. Altrimenti, l i s t a A d i a c e n z a [ i ]è una lista doppia con un riferimento sia all'elemento iniziale che a quello finale della

lista di adiacenza per il nodo i: ogni elemento x di tale lista corrisponde a un arco (i, j)incidente a i e il corrispettivo campo x.dato contiene l'altro estremo j. Per esempio, ilgrafo mostrato nella Figura 6.2 viene rappresentato mediante liste di adiacenza come il-lustrato nella Figura 6.8. Volendo rappresentare un grafo pesato, è sufficiente aggiungereun campo x.peso contenente il peso W(i, j) dell'arco (i, j).

La scansione degli archi incidenti a un dato nodo i può essere effettuata mediantela scansione della corrispondente lista di adiacenza, in tempo pari al grado di i, comeillustrato nelle istruzioni del seguente codice.

Page 223: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 223/373

 

SXF

CRL

DUB

STO

MAD

TRF

BVA

CIA

NYO

STO

DUB | 1 CIA | 1 NYO

CRL | 1 BVA | 1 CIA |

SXF | 1 TRF | 1 CIA | [ NYO

STO

DUB | 1 CIA | 1 NYO |

DUB | 1 STN | 1 BVA | 1 NYO

STN | 1 BVA

CRL

CRL CIA

Figu ra 6 .8 Lista di adiacenza per il grafo nella Figura 6.2, in cui SXF, CRL, DUB, STN, MAD, TRF, BVA,

CIA, NYO sono implicitamente enumerati 0,1,2 8, in quest'ordine.

x = listaAdiacenza[i].inizio;

 WHILE (x != null) {

j = x.dato;

PRINT (i,j);

PRINT x.peso (se previsto);

x = x.succ;

Tuttavia, la verifica della presenza di un arco tra una generica coppia di nodi i e j richiedela scansione della lista di adiacenza di i oppure di j, mentre tale verifica richiede tempocostante nelle matrici di adiacenza. Non esiste una rappresentazione preferibile all'altra,in quanto ciascuna delle due rappresentazioni presenta quindi vantaggi e svantaggi che

vanno ponderati al momento della loro applicazione, valutandone la convenienza intermini di complessità in tempo e spazio che ne derivano.

Per quanto riguarda lo spazio utilizzato, la rappresentazione del grafo mediante ma-trice di adiacenza richiede spazio 0(n 2 ), indipendentemente dal numero m di archipresenti: ciò risulta ottimo per grafi densi ma poco conveniente nel caso in cui trattiamografi sparsi in quanto hanno m = O(m) oppure, in generale, per grafi in cui il numerodi archi sia m = o(n 2 ). Per tali grafi, la rappresentazione mediante matrice di adiacenzarisulta poco efficiente dal punt o di vista dello spazio utilizzato. Invece, lo spazio è pari

Page 224: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 224/373

 

Figura 6.9 I grafi completi e K5 e il grafo bipartito completo K33.

a 0(n + m) celle di memoria nella rappresentazione mediante liste di adiacenza: infattila lista per ciascun nodo è di lunghezza pari al grado del nodo stesso e, come abbiamovisto, la somma dei gradi di tutti i nodi risulta essere O(m); inoltre, usiamo spazio O(n)per l'array dei riferimenti.

La rappresentazione con liste di adiacenza può essere vista anche come una rappre-sentazione compatta della matrice di adiacenza, in cui ogni lista corrisponde a una rigadella matrice e include i soli indici delle colonne corrispondenti a valori pari a 1. Per ave-re un metro di paragone per la complessità in spazio, occorre confrontare lo spazio per

memorizzare 0(n + m) interi e riferimenti, come richiesto dalle liste di adiacenza, conlo spazio per memorizzare un array bidimensionale di 0(n 2 ) bit, come richiesto dallamatrice di adiacenza.

Per grafi particolari, possiamo usare rappresentazioni succinte nel caso statico comequelle discusse per gli alberi statici (Capitolo 4). Appartengono a questo tipo di grafi siagli alberi (che hanno n — 1 archi) che i grafi planari, che possono essere sempre disegnatisul piano senza intersezioni degli archi: Euler dimostrò infatti che un grafo planare din vertici contiene m = O(n) archi e quindi è sparso. Un esempio di grafo planare èmostrato nella Figura 6.2, dove è riportata a destra una sua disposizione nel piano senza

intersezioni degli archi {embedding planare). Ment re il grafo completo K4 è planare, comemostrato dal suo embedding planare nella Figura 6.9, non lo sono K5 e il grafo bipartitocompleto K33 in quanto è possibile dimostrare che non hanno un embedding planare.

Da quanto detto deriva che un grafo planare non può contenere né K5 né K3 3 comesottografo: sorprendentemente, questi due grafi completi consentono di caratterizzare igrafi planari. Preso un grafo G, definiamo la sua contrazione G' come il grafo ottenutocollassando i vertici w di grado 2, per cui le coppie di archi (u, w) e (w,v) a essi incidentidiventano un unico arco (u, v): nella Figura 6.6, il grafo G a destra ha il grafo G' al centrocome contrazione. Il teorema di Kuratowski-Pontryagin-Wagner afferma che G non è

Page 225: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 225/373

 

planare se e solo se esiste un suo sottografo G' la cui contrazione fornisce K5 oppure 1^3.Tale proprietà può essere impiegata per certificare che un grafo non è planare, esibendoG ' come prova che non è possibile trovare un embedding planare di G.

Tornando alla rappresentazione nel calcolatore dei grafi, consideriamo il caso di quel-la per grafi orientati: notiamo che essa non presenta differenze sostanziali rispetto allarappresentazione discussa finora per i grafi non orientat i. La matrice di adiacenza nonè più simmetrica come nel caso di grafi non orientati e, inoltre, vengono solitamenterappresentati gli archi uscenti nelle liste di adiacenza. L'arco orientato (i, j) viene memo-rizzato solo nella lista l i s t a A d i a c e n z a [ i ] (come elemento x contenente j nel campox.dato) in quanto è differente dall'arco orientato (j,i) (che, se esiste, va memorizza-to nella lista l i s t a A d i a c e n z a [ j ] come elemento contenente i). Osserviamo che neigrafi non orientati l'arco (i, j) è invece memorizzato sia in l i s t a A d i a c e n z a f t ] che inl i s t a A d i a c e n z a [ j ] . Nel seguito, ciascuna lista di adiacenza è ordinata in ordine cre-scente di numerazione dei vertici in essa contenuti, se non specificato diversamente, efornisce tutte le operazioni su liste doppie indicate nel Paragrafo 5.2.

Infine notiamo che, mentre la rappresentazione di un grafo lo identifica in modounivoco, non è vero il contrario. Infatti, esistono n! modi per enumerare i vertici convalori distinti in V = {0,1,... ,n — 1} e, quindi, altret tanti modi per rappresentare lostesso grafo. Per esempio, i due grafi di seguito sono apparentemente distinti:

La distinzione nasce dall'artificio di enumerare arbitrariamente gli stessi vertici ma è chia-ro che la relazione tra i vertici è la medesima se ignoriamo la numerazione (ebbene sì,il cubo a destra è un grafo bipartito). Tali grafi sono detti isomorfi in quanto una sem-plice rinumerazione dei vertici li rende uguali e, nel nostro esempio, i vertici del grafo a

sinistra vanno rinumerati come

0 - 4 , 1 - 1 , 2 - 6 , 3 - 3 , 4 - 5 , 5 - 0 , 6 - 7 , 7 - 2 ,

per ottenere il grafo a destra: il problema di decidere in tempo polinomiale se due grafiarbitrari di n vertici sono isomorfi equivale a trovare tale rinumerazione, se esiste, intempo polinomiale in n ed è uno dei problemi algoritmici fondamentali tuttora irrisolti,con molte implicazioni (per esempio, stabilire se due grafi arbitrari non sono isomorfi hadelle importanti implicazioni nella crittografia).

Page 226: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 226/373

 

0 1 2 3 4 5 6 7 8 9 100 1 1 0 0 1 1 0 0 0 0 01 1 1 1 1 1 0 0 1 0 0 02 0 1 1 0 1 0 0 0 0 0 03 0 1 0 1 0 1 1 1 0 0 04 1 1 1 0 1 0 0 0 0 0 0

5 1 0 0 1 0 1 1 1 0 0 06 0 0 0 1 0 1 1 1 0 0 07 0 1 0 1 0 1 1 1 0 0 08 0 0 0 0 0 0 0 0 1 1 19 0 0 0 0 0 0 0 0 1 1 1

10 0 0 0 0 0 0 0 0 1 1 1

Figura 6 .10 Matrice di adiacenza modificata per il calcolo della chiusura transitiva.

6.1.3 Cammini minimi, chiusura transitiva e prodotto di matrici

La rappresentazione di un grafo G = (V, E) mediante matrice di adiacenza fornisce unsemplice metodo per il calcolo della chiusura transitiva del grafo stesso: un grafo G* =(V, E* ) è la chiusura transitiva di G se, per ogni coppia di vertici i e j in V, vale (i, j ) € E*se e solo se esiste un cammino in G da i a j.

Sia A la matrice di adiacenza del grafo G, modificata in modo che gli elemen-ti della diagonale principale hanno tutti valori pari a 1 (come mostrato nella Figu-ra 6.10). Calcolando il prodot to booleano A2 = A x A dove l'operazione di somma

tra elementi è l'OR e la moltiplicazione è l'AND, possiamo notare che un elementoA2[i][j] = A[i][k] • A[k][j] di tale matrice è pari a 1 se e solo se esiste almeno un

indice 0 ^ t ^ n - 1 tale che A[i][t] = A[t][j] = 1 (nella Figura 6.11 a sinistra è mostratala matrice di adiacenza A2 corrispondente a quella della Figura 6.10).

Tenendo conto dell'interpretazione dei valori degli elementi della matrice A, ne de-riva che, dati due nodi i e j, vale A2[i][j] = 1 se e solo se esiste un nodo t, dove0 ^ t ^ n — 1, adiacente sia a i che a j, e quindi se e solo se esiste un cammino dilunghezza al più 2 tra i e j. L'eventualità che il cammino abbia lunghezza inferiore a 2deriva dal caso in cui t = i oppure t = j (motivando così la nostra scelta di porre a 1

tutti gli elementi della diagonale principale di A).

Moltiplicando la matrice A2 per A otteniamo, in base alle considerazioni precedenti,la matrice A3 tale che A3[i][j] = 1 se e solo se i nodi i e j sono collegati da un camminodi lunghezza al più 3: nella Figura 6.11 a destra è mostrata la matrice di adiacenza A 3

corrispondente alla matrice di adiacenza della Figura 6.10.

Possiamo verificare che, moltiplicando A 3 per A, la matrice risultante è uguale adA3: da ciò deriva che A l=A3 per ogni i ^ 3, e che quindi A 3 rappresenta la relazione diconnessione tra i nodi per cammini di lunghezza qualunque. Indicheremo in generale

Page 227: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 227/373

 

0 1 2 3 4 5 6 7 8 9 10 0 1 2 3 4 5 6 7 8 9 100 1 1 1 1 1 1 1 1 0 0 0 0 1 1 1 1 1 1 1 1 0 0 01 1 1 1 1 1 1 1 1 0 0 0 1 1 1 1 1 1 1 1 1 0 0 02 1 1 1 1 1 0 0 1 0 0 0 2 1 1 1 1 1 1 1 1 0 0 03 1 1 1 1 1 1 1 1 0 0 0 3 1 1 1 1 1 1 1 1 0 0 04 1 1 1 1 1 1 0 1 0 0 0 4 1 1 1 1 1 1 1 1 0 0 0

5 1 1 0 1 1 1 1 1 0 0 0 5 1 1 1 1 1 1 1 1 0 0 06 1 1 0 1 0 1 1 1 0 0 0 6 1 1 1 1 1 1 1 1 0 0 07 1 1 1 1 1 1 1 1 0 0 0 7 1 1 1 1 1 1 1 1 0 0 08 0 0 0 0 0 0 0 0 1 1 1 8 0 0 0 0 0 0 0 0 1 1 19 0 0 0 0 0 0 0 0 1 1 1 9 0 0 0 0 0 0 0 0 1 1 1

10 0 0 0 0 0 0 0 0 1 1 1 10 0 0 0 0 0 0 0 0 1 1 1

Fi gu ra 6.11 Matrici di adiacenza A2 (a sinistra) e A3 = A* (a destra).

tale matrice come A*, osservando che essa rappresenta il grafo G* di chiusura transitivadel grafo G.Per la corrispondenza tra nodi adiacenti in G* e nodi connessi in G, la matrice A*

consente di verificare in tempo costante la presenza di un cammino in G tra due nodi(non consente però di ottenere il cammino, se esiste), oltre che di ottenere in tempoO(n), dato un nodo, l'insieme dei nodi nella stessa componente connessa in G.

Poniamoci ora il problema del calcolo efficiente di A* a partire da A, esemplificatodal codice seguente.

A' = A;DO {

B = A*;

A* = B x B ;

> WHILE (A* != B) ;

RETURN A* ;

Il codice, a partire da A, moltiplica la matrice per se stessa, ottenendo in questo modola sequenza A2 , A 4, A 8 ,..., fino a quando la matrice risultante non viene più modificatada tale moltiplicazione, ottenendo così A*. Valutiamo il numero di passi eseguiti da tale

codice: l'istruzione a riga 1 viene eseguita una sola volta e ha costo 0(n 2 ), richiedendola copia degli n 2 elementi di A, mentre l'istruzione alla riga 3 e il controllo alla riga 5sono eseguiti un numero di volte pari al numero di iterazioni del ciclo, richiedendo uncosto 0(n 2 ) a ogni iterazione. Per quanto riguarda l'istruzione alla riga 4, anch'essa vieneeseguita a ogni iterazione e ha un costo pari a quello della moltiplicazione di una matricen x n per se stessa, costo che indichiamo per ora con Cvi(n).

Per contare il numero massimo di iterazioni, consideriamo che la matrice A 1 rappre-senta come adiacenti elementi a distanza al più i e che il diametro (la massima distanza tra

Page 228: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 228/373

 

Figura 6.12 Grafo rappresentante i 25 stati confinanti nell'Unione Europea in cui la numerazionedei vertici da 0 a 24 è sostituita dalla rispettiva sigla internazionale.

due nodi) di un grafo con n vertici è al più n— 1. Dato che a ogni iterazione la potenza idella matrice A1 calcolata raddoppia, saranno necessarie al più log(n— 1 ) = 0(logn) ite-razioni per ottenere la matrice A*. Da ciò consegue che il costo computazionale del codi-ce precedente sarà pari a 0((TI2+C M (n.)) logn). Come discusso nel Capitolo 2, abbiamoche C M (TX) = n ( n 2 ) e che C M (TI) = 0(n 3 ) con il metodo classico di moltiplicazionedi matrici: quindi il costo totale di calcolo di A* è 0(n 3 logn). Una riduzione del costo

C M(TI) può essere ottenuta utilizzando algoritmi più efficienti per la moltiplicazione dimatrici, come ad esempio l'algoritmo di Strassen introdotto nel Paragrafo 2.6.1.

6.2 Opus libri: colorazione di grafi e algoritmi golosi

I grafi permetteno di esprimere i problemi che riguardano lo scheduling di risorse e diattività (come ad esempio l'allocazione di registri nei compilatori e l'assegnamento difrequenze nei cellulari) come un problema di colorazione, in cui vogliamo colorare i ver-tici con il minimo numero possibile di colori in modo tale che i vertici adiacenti siano

colorati diversamente. Prima di discutere un'applicazione reale di tale problema, illu-striamo un esempio basato sulla colorazione della cartina dei paesi dell'Unione Europea(in realtà i cartografi usano altri criteri come il bilanciamento dei colori piuttosto che ilmin imo numero di colori usati). In questo caso, il grafo non orientato G = (V, E) nellaFigura 6.12 ha V uguale all'insieme degli n = 25 paesi dell'Unione: inoltre (i, j) € E see solo se i paesi i e j sono confinanti, per 0 ^ i, j ^ n — 1.

Dato un qualunque intero k > 0, una k-colorazione è una funzione x : V >—>{0,1, . . . , k — 1}, definita sui vertici e indicata con la lettera greca x (chi), che assegna

Page 229: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 229/373

 

colori diversi a vertici adiacenti, ovvero x(i-) x( j) P e r °g n i a r c o (i> j) £ E: il minimonumero k per il quale esiste una k-colorazione viene denominato numero cromatico xodel grafo in questione. Per il grafo considerato, una possibile 4-colorazione è quella cheassegna colore 0 ai nodi IRL, E, D, I, SK, S, LIT e EST, colore 1 ai nodi UK, F, NL,DK, PL, A, SF, LTV, GR, CYP e MLT, colore 2 ai nodi B, CZ e SL, e colore 3 ai nodiP, L e H. È possibile d'altra parte verificare che 4 colori sono necessari perché B, F, D, Lformano una cricca e, essendo l'uno adiacente agli altri, richiedono colori tutti diversi:pertanto, nel caso del grafo nella Figura 6.12 abbiamo che xo = 4.

Per la colorazione vale quanto detto per il cammino e per il ciclo hamiltoniano: datoun intero k ^ 3 non è noto nessun algoritmo che in tempo polinomiale determini se ungrafo arbitrario ha numero cromatico k. D'al tra parte, osserviamo che, dato un valorek < 0 e un'assegnazione di colori ai nodi, è facile verificare in tempo polinomiale se essaè una k-colorazione: pertanto il problema della k-colorazione è in NP. Come vedremosuccessivamente, tale problema è in effetti NP-completo.

Per alcune famiglie importanti di grafi, tuttavia, la colorazione può essere trovata intempo lineare. Per esempio, abbiamo appena visto che la cricca Kr ha xo = r in quantoogni coppia di vertici è adiacente. Nel caso di grafi planari, come quello nella Figu-ra 6.12, è sempre possibile trovare una 4-colorazione in tempo lineare, per cui xo ^ 4:questo risultato non è affatto ovvio e discende da una famosa congettura del XIX secolorisolta quasi un secolo dopo da due matematici statunitensi, Kenneth Appel e WolfgangHaken, con l'ausilio di tecniche algoritmiche e del calcolatore. Pur esistendo dei grafiplanari che richiedono Xo = 4 colori, come quello mostrato nella Figura 6.12, altri grafiplanari potrebbero richiedere Xo < 4 colori: stabilire se un grafo planare ammette una3-colorazione è un problema NP-completo.

Il problema del calcolo del numero cromatico xo n e ' c a s o di grafi planari è quindi, altre ttanto difficile quanto quello per grafi arbitrari. Ciò è sorprendente perché si tratta

di stabilire "soltanto" se xo è pari a 3 o 4: infat ti, per Xo = L il grafo è composto da uninsieme di nodi isolati mentre per xo = 2 è facile progettare un algoritmo che risolve ilproblema in tempo polinomiale su grafi arbitrari.

Vediamo in questo paragrafo che il problema diventa facilmente risolvibile per un'al-tra classe speciale di grafi, chiamati grafi a intervalli, per cui sviluppiamo algoritmi

appositi che ne sfruttano le particolari proprietà.

6.2.1 II problema dell'assegnazione delle lunghezze d'onda

In una rete ottica, i segnali luminosi viaggiano simultaneamente su lunghezze d'ondadifferenti con una tecnologia che si chiama multiplazione a divisione di lunghezza d'on-da. La banda di trasmissione di una fibra ottica viene partizionata in canali multipli,ciascuno dei quali opera a una diversa lunghezza d'onda: ad esempio, una fibra otticastandard a modo singolo può ospitare 128 lunghezze d'onda con una banda pari a 10

Page 230: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 230/373

 

gigabit al secondo per ciascuna lunghezza d'onda. Quando un segnale cambia lunghezzad'onda è necessario trasformarlo in un segnale elettrico, instradarlo e riconvertirlo in unsegnale ottico con l'opportuna lunghezza d'onda: questo genera un ritardo significativoin quanto il segnale elettrico viaggia molto più lentamente del segnale ottico. 'E quindi

più efficiente evitare, per quanto possibile, il cambiamento di lunghezza d'onda del se-gnale trasmesso: sfruttando la tecnologia ottica, in una rete trasparente i segnali viaggianoda un collegamento all'altro senza cambiare lunghezza d'onda.

Un sistema lineare ottico è una rete trasparente composta da un certo numero dicollegamenti o linee interconnessi tra loro a formare una sequenza lineare lo, li,..., l s - 1 .Una richiesta di trasmissione del segnale ottico viene rappresentata con l'intervallo chiu-so [a,b] se necessita della sequenza di collegamenti l a , l Q + i . . . , l b , dove 0 ^ a ^b ^ s — 1. Tale richiesta prevede che il segnale viaggi sulla stessa lunghezza d'onda inlQ , l a +i • • •, lb- Tuttavia, bisogna prestare attenzione ad assegnare le lunghezze d 'ond a

alle richieste in modo che non vi siano due segnali che viaggino sulla stessa lunghez-za d'o nda e interessino uno stesso collegamento. Que sto problema, spesso co mbi natocon quello dell'instradamento delle richieste, è stato molto studiato e risulta essere mol-to difficile in reti ottiche generali: per la sua semplici tà, il sistema lineare ottico vieneimpiegato come base per la progettazione di reti ottiche.

Sia R l'insieme di n richieste [ao,bo], [a i. b i] , . . . , [ a n _ i , b n _i] da soddisfare. Siainoltre fi la lunghezza d'onda assegnata alla richiesta [ai.bi], per cui l'insieme R è sod-disfatto se vale fi ^ f j per ogni coppia di richieste distinte [Qi,bi] e [a j, bj ] che condi-vidono almeno un collegamento (ovvero, gli intervalli hanno intersezione non vuota).

Il problema dell'assegnazione delle lunghezze d'onda consiste nel soddisfare l'insieme Rutilizzando il minimo numero di lunghezze d'onda distinte.

Ad esempio, supponi amo che s = 2 , n = 3 e R = {[0,1], [0,2], [1,2]}. In questocaso sono sufficienti due lunghezze d 'onda: la pri ma è assegnata alle richieste [0,1] e[1,2], mentre la seconda è assegnata a [0,2], D'alt ra parte, due lunghezze d' on da sononecessarie in quanto esistono due intervalli che si intersecano.

6.2.2 Grafi a intervalli

Vediamo ora come sia possibile modellare il problema dell'assegnazione delle lunghezzed'onda mediante un problema di colorazione di grafi. In particolare, dato l'insieme R din richieste [do,boi, [ai,b]],..., [ a n _i, b n _i], definiamo il grafo GR = (VR , ER ) ottenu-to da R come segue e come illustrato nella Figura 6.13: l'insieme VR = {0,1,... ,n—1} deivertici rappresenta gli intervalli in R, dove i e VR corrisponde all'intervallo [at, bi]. L'in-sieme degli archi ER è definito in base all'intersezione degli intervalli, ovvero (i, j) S ER 

se e solo se gli intervalli [ai, bi] e [cij, bj] hanno intersezione non vuota (incluso il caso incui si intersecano in uno degli estremi). Ogni grafo così ottenuto, ovvero che possa essererappresentato come grafo di intersezione di intervalli, viene detto grafo a intervalli.

Page 231: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 231/373

 

Figura 6.13 Un insieme di intervalli e il grafo a intervalli corrispondente.

Usando la formulazione mediante grafo a intervalli, osserviamo che il problema del-l'assegnazione delle lunghezze d'onda si riduce al problema della colorazione minima delgrafo GR , in cui i colori sono in corrispondenza biunivoca con le lunghezze d'onda. In al-

tre parole, dobbiamo calcolare il numero cromatico xo di GR , e tale numero corrispondeal numero minimo p. = xo di lunghezze d'onda che soddisfano le richieste.

Prima di descrivere un algoritmo polinomiale per la risoluzione di quest'ultimo pro-blema, osserviamo che i grafi a intervalli rappresentano gli archi in forma implicita: peresempio, la cricca K n è un grafo a intervalli, in quanto generato da n intervalli, l'unoannidato dentro l'altro. In tal modo, ogni coppia di intervalli ha intersezione non vuota,dando luogo a m = (") archi.

Notiamo che i grafi a intervalli sono un sottoinsieme proprio dei grafi: per esempio,il grafo a forma di quadrato (ossia 4 vertici e altrettanti archi) non è un grafo a intervalli.

I grafi a intervalli consentono di modellare diverse altre situazioni in cui dobbiamoallocare risorse ad attività da svolgere in un certo intervallo di tempo.

Ad esempio, supponiamo di avere un insieme di lezioni da dover pianificare, e chea ognuna sia associato un intervallo temporale all'interno del quale essa debba esseresvolta. Se due lezioni si sovrappongono, a esse non può essere assegnata lo stessa aula.Il problema di calcolare il minor numero possibile di aule sufficienti a svolgere tutte lelezioni è ancora una volta equivalente a quello di dover calcolare il numero cromaticodel grafo a intervalli corrispondente all'insieme delle lezioni.

Page 232: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 232/373

 

I grafi a intervalli hanno anche altre applicazioni, nel campo dell'archeologia (perdeterminare un ordinamento dell'età di un insieme di artefatti) oppure nel campo dellabiologia (per la costruzione di mappe del DNA o per la ricerca di geni), per citare alcuniesempi in altre discipline.

6.2.3 Colorazione di grafi a intervalli

L'algoritmo di colorazione del grafo a intervalli sfrutta alcune proprietà geometriche degliintervalli, che supponiamo di avere ordinato in ordine non decrescente in base al loroestremo sinistro, disponendo la sequenza ordinata parallelamente all'asse delle ascissenel piano cartesiano. Utilizziamo una linea immaginaria di scansione (sweeping line),parallela all'asse delle ordinate, che procede da sinistra verso destra.

In ogni istante, la linea interseca un certo numero di intervalli: il numero croma-tico xo è almeno pari al massimo numero di intervalli intersecati dalla linea durante lasua scansione. Se consider iamo gli eventi rilevanti che occorrono durante la scansionedella linea immaginaria, possiamo notare che il numero di intervalli intersecati cambiasoltanto quando la linea incontra un estremo di un intervallo:

• se l'estremo è quello sinistro, e quindi incontriamo un nuovo intervallo, a que-st'ultimo viene assegnato uno qualunque dei colori non utilizzati dagli intervalliintersecati dalla linea;

• se l'estremo è quello destro, e quindi riscontriamo la fine di un intervallo, que-

st'ultimo rilascia il colore assegnatogli affinché qualcun altro possa usarlo.

Per poter essere riutilizzati in seguito, i colori temporaneamente rilasciati vengono man-tenuti in un secchiel lo (bucket), realizzato come un array non ordinato i cui elementisono inseriti ed estratti in fondo, richiedendo tempo costante per operazione. L'algorit-mo garantisce che i colori appartengano all'insieme {0,1,... ,xo — 1} e che due intervalliche si intersecano ricevano colori diversi.

L'algoritmo di colorazione di un grafo a intervalli GR è mostrato nel Codice 6.1,dove i parametri d'ingresso sono gli intervalli [do, boi, [a j, bj], . . . , [ a n _ i , b n _]]. Dopo

aver azzerato il numero di colori disponibili nel secchiello (che inizialmente è vuoto) eil contatore per il numero di colori usati, procediamo a ordinare gli intervalli in base alloro estremo sinistro (righe 3—7).

In particolare, utilizziamo un array di 2n elementi in cui poniamo le triple (Qv,0,i)e (bi, l,i) in corrispondenza dell'intervallo [di, b*], per 0 ^ i < n. — 1: la prima compo-nente della tripla contiene un estremo dell'intervallo, la seconda specifica se è un estremodestro (vale 1) o sinistro (vale 0), e la terza contiene l'indice i dell'intervallo. Non pos-siamo generare due triple uguali, e il successivo ordinamento lessicografico (riga 7) fa sì

Page 233: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 233/373

 

ColoraGraf olntervalli( [a[0], b[0]], [a[l], b[l]],..., [a[n - 1], b[n - 1]] ) :

coloriLiberi = numeroColori = 0;

FOR  (i = 0; i < N; i = i + 1) {

estremo[2 x i] = <a[i],0,i>;

estremo[2 x i + 1] = <b[i],l,i>;

>OrdinaCrescente( estremo );FÜR  (j = 0; j < 2 x n; j = j + 1) {

<x,estremoDestro,i> = estremo[j] ;

IF (estremoDestro == 1) {

secchiello[coloriLiberi] = colore [i];

coloriLiberi = coloriLiberi + 1;

> ELSE IF (coloriLiberi > 0) {

colore[i] = secchiello[coloriLiberi-1];

coloriLiberi = coloriLiberi - 1;

> ELSE {colore[i] = numeroColori;

numeroColori = numeroColori + 1 ;

>

Codic e 6.1 Algoritmo per la colorazione di grafi a intervalli, dove s e c c h i e l l o è una semplicesequenza non ordinata.

che gli intervalli che condividono lo stesso estremo siano ordinati in modo che venga-

no prima le triple associate a intervalli che iniziano in tale estremo di quelle associate a

intervalli che terminano in esso.

Il successivo ciclo (righe 8-20) scandisce gli intervalli in ordine, per simulare la lineaimmaginar ia, e assegna i colori agli intervalli memorizzandoli nell'array co l o r e . Presol'estremo dell'intervallo corrente, non importa il valore di tale estremo ma piuttosto se èdestro o sinistro: la seconda componente della tripla fornisce quest'informazione, mentrela terza comp onen te ci indica che siamo nell'intervallo [ai, b j (riga 9).

Utilizziamo ora il fatto che quando incontriamo un estremo destro, rilasciamo ilcolore riponendolo nel secchiello (righe 10—12). Nel caso in cui incontriamo un estremosinistro, dobbiamo o prelevare uno dei colori nel secchiello (se non è vuoto, come nellerighe 13-15) oppure utilizzare un nuovo colore mai usato prima (righe 16—18).

Questo algoritmo produce una k-colorazione del grafo, dove il numero k di coloriè pari a n u m e r o C o l o r i : dimostr iamo ora che l'algoritmo utilizza il minor numeropossibile di colori, e quindi che n u m e r o C o l o r i = xo al termine dell'algoritmo. A talescopo, per ogni nodo i, indichiamo con P(t) i nodi adiacenti a i ai quali, nel momento

Page 234: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 234/373

 

in cui considera di, l'algoritmo ha già assegnato un colore. Tali nodi corrispondono a

intervalli che iniziano prima di, o con, a^ e che terminano dopo, o con, cu: pertanto ,

tutti questi nodi, incluso i, corrispondono a intervalli che si intersecano in cu, e quindi

l'insieme P(i) U {i} forma una cricca.

In particolare, se i è un nodo in corrispondenza del quale la variabile numeroColori

deve essere incrementata, questo implica che | P(i) |= numeroColori, e il grafo contie-

ne una cricca di | n u m e r o C o l o r i | +1.

Se consideriamo il valore di numeroColori al termine dell'algoritmo, abbiamo che

il grafo contiene una cricca di tale dimensione: da ciò deriva che xo ^ n u m e r o C o l o r i .

Dato che l'algoritmo ha effettuato una k-colorazione dove k = n u m e r o C o l o r i , ne

deriva che tale colorazione è ottima.

Per quanto riguarda la complessità dell'algoritmo, notiamo che il costo dominante

è quello dell'ordinamento (riga 7) in quanto il resto del codice richiede tempo O(n).

Infatti, il primo ciclo richiede n iterazioni di tempo costante mentre il secondo ciclone richiede 2n, in cui ciascuna operazione sul secchiello richiede tempo costante . Ne

risulta, in generale, una complessità totale di O( n l o gn ) tempo.

ALVIE: colorazione di grafi a intervalli

Osserva, sperimenta e verifica

IntervalGraphColoring

6.2.4 Massimo insieme indipendente in un grafo a intervalli

Il problema della colorazione dei nodi di un grafo è solo uno dei tanti problemi su grafiche, in generale, sono NP-completi, ma che divengono risolvibili in tempo polinomialenel momento in cui li restringiamo a grafi a intervalli: un altro esempio di tali problemiè quello della ricerca del massimo insieme indipendente.

Definiamo un insieme indipendente (independent set) come un sottoinsieme I r dir vertici, il cui sottografo indotto non possiede archi: IT è, quindi, complementare allacricca Kr che invece possiede tutti gli archi possibili. In generale, dato un grafo G, trovareun suo insieme indipendente di cardinalità massima è un problema NP-completo: notia-mo come tale problema possa essere visto come un problema di colorazione, consistentenell'individuare il massimo numero di nodi che possono essere correttamente coloratifacendo uso di un solo colore. In questo paragrafo, most riamo che ciò è risolvibile intempo polinomiale nel caso di grafi a intervalli.

Page 235: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 235/373

 

Il problema del massimo insieme indipendente ha diverse applicazioni in molti con-testi reali: l'esempio principe, in tal senso, è quello dell'assegnazione (scheduling) a unsingolo elaboratore (sia esso umano, meccanico o elettronico) del massimo numero dicompit i da eseguire. Ipotizzando, infatti , che i compiti non possano essere fraziona-ti e che sussista tra i compiti una relazione di compatibilità, possiamo modellare taleproblema nel modo seguente.

Definiamo un grafo G i cui nodi corrispondono ai compiti da eseguire e i cui ar-chi rappresentano la relazione di compatibilità tra di essi (ovvero, esiste un arco tra ilcompito t e il compito t' se e solo se t e t' possono essere entrambi eseguiti dall'ela-boratore): un massimo insieme indipendente di G rappresenta una soluzione ottimaleper il problema dell'assegnazione di compit i. Nel caso in cui i compiti siano specificaticome intervalli di esecuzione, attraverso un tempo di inizio e un tempo di fine, e chedue compiti siano compatibili se e solo se i loro corrispondenti intervalli non si interse-cano, il problema dello scheduling si riduce a quello della ricerca del massimo insiemeindipendente all'interno di un grafo a intervalli.

L'algoritmo polinomiale per la risoluzione di tale problema è abbastanza simile aquello visto in precedenza per il problema della colorazione. Anche in questo caso, scan-diamo gli intervalli da sinistra verso destra: ogni qualvolta incontriamo la fine di unintervallo che non ne interseca un altro precedentemente assegnato all'elaboratore, deci-diamo di assegnare tale intervallo all'elaboratore stesso. In altre parole, questo approc-cio cerca di rendere l'elaboratore nuovamente disponibile il prima possibile, preferendoassegnare a esso i compiti che terminano prima.

Il Codice 6.2 realizza questa strategia, ipotizzando per semplicità che gli intervalliabbiano gli estremi destri distinti e che quindi possano essere ordinati in modo crescentein base ai loro estremi destri (righe 3—6). Una volta eseguito l'ordinamento, l'algoritmoesamina uno dopo l'altro gli intervalli in base a tale ordine (righe 8-16) e, per ciascunodi essi, verifica (riga 10) se è il primo esaminato ( u l t i m o < 0) e, quindi , il primo a ter-minare, oppure se il suo tempo di inizio è successivo al tempo di conclusione dell'ultimointervallo precedentemente assegnato (QÌ > buitimo)- Se è così, assegna l'intervallo esa-minato e aggiorna il riferimento all'ultimo intervallo assegnato (righe 11 e 12), mentrein caso contrario decide di non assegnare l'intervallo esaminato (riga 14).

Osserviamo anzitutto che la soluzione prodotta dal Codice 6.2 è un insieme indi-pendente. In effet ti, in base alla guardia della riga 10 e al fat to che gli intervalli sonoesaminati-in ordine crescente rispetto al loro tempo di conclusione, un intervallo cheviene incluso nella soluzione non interseca nessuno di quelli precedentemente inclusi.Tale soluzione è anche ottimale, ovvero non può esistere un insieme indipendente dicardinalità maggiore.

Supponiamo per assurdo che esista un insieme indipendente M tale che |M| > |S|,dove S è l'insieme indipendente calcolato dall'algoritmo. Supponiamo, inoltre, che i due

Page 236: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 236/373

 

InsiemelndipendenteGraf olntervalli( [a[0], b[0]],..., [a[n — 1], b[n — 1]] ) :

(pre: i valori b[i] sono tutti distinti per 0 ij i SJ n — 1)

FOR  (i = 0; i < n; i = i + 1) {

estremoDestro[i] = <b[i],i>;

>OrdinaCrescente( estremoDestro );ultimo = -1;

FOR  (j = 0; j < n; j = j + 1) {

<x,i> = estremo[j];

IF ((ultimo < 0) II (a[i] > b[ultimo])) {

assegnati] = TRUE;

ultimo = i;

> ELSE {

assegnati] = FALSE;

>

>RETURN assegna;

Codic e 6. 2 Algoritmo per la ricerca del massimo insieme indipendente in un grafo a intervalli.

insiemi siano in ordine crescente rispetto al tempo di conclusione degli intervalli in essicontenuti: pertanto possiamo considerare M come un insieme di indici {mo,..., m.h}tale che b m i < b m i + 1 per 0 ^ i < h, e S come un insieme di indici {so,..., sjJ tale chebSi < bSi + 1 per 0 ^ i < k (con h > k).

Notiamo che M deve includere un intervallo T i cui tempi di inizio e di conclusionesono entrambi maggiori di b m k . Se dimostr iamo che bSk < b m k , allora abbiamo cheT viene esaminato dal Codice 6.2 successivamente all'intervallo [a s k , b s  j : in tal caso,T deve essere incluso in S in quanto il suo tempo di inizio è maggiore di b m k > bS k ,contraddicendo il fatto che S contiene k intervalli.

Per verificare che bSk ^ b m k , mostriamo per induzione che b Si ^ b m i per 0 ^ i ^ k.Chiaramente, bSQ ^ b m o , in quanto il nostro algoritmo seleziona sempre l'intervallo chetermina per primo. Per i > 0, abbiamo per ipotesi induttiva che b S i < b m i _,. Inoltre,poiché M è un insieme indipendente, deve valere bTTli_1 < a m ( ^ b m i (due intervalli inM non si possono intersecare).

Quindi l'intervallo [a m i , b m J viene esaminato dal Cod ice 6.2 successivamente al-l'intervallo [aSi , ,b S i ,] d'intervallo [a S l ,bS l ] non può avere un tempo di fine superiorea b m i , ovvero bSi ^ b m i . Abbi amo dunque dimostrato che l'insieme indipendenteprodotto dal Codice 6.2 ha la cardinalità massima possibile.

Per quanto riguarda la complessità dell'algoritmo, il costo dominante è quello del-l'ordinamento, per cui la complessità totale è O ( n l og n ) tempo.

Page 237: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 237/373

 

ALVIE : massimo insieme indipendente in grafi a intervalli

Osserva, sperimenta e verifica

IndependentSet

6.2.5 Paradigma dell'algoritmo goloso

I due algoritmi polinomiali per la risoluzione dei problemi della colorazione di nodi edel massimo insieme indipendente nel caso di grafi a intervalli, seguono uno schemamolto simile (non a caso i Codici 6.1 e 6.2 non sono molto diversi tra di loro). In effetti,una volta stabilito un ordine con cui esaminare gli intervalli, entrambi gli algoritmi de-

cidono come comportarsi sulla base di scelte che appaiono in quel momento le miglioripossibili.4

Nel caso del problema della colorazione, ogni qualvolta esaminiamo un nuovo inter-vallo, decidiamo di assegnargli, se possibile, un qualunque colore tra quelli già utilizzati:in questo caso, il criterio adottato è quello di non utilizzare nuovi colori se non è neces-sario. Nel caso del problema del massimo insieme indipendente, invece, ogni qualvoltaesaminiamo un nuovo intervallo decidiamo di includerlo nella soluzione se ciò è compa-tibile con quanto fatto fino a quel momento: in questo caso, quindi, il criterio adottatoè quello di assegnare un compito se è possibile farlo.

I due criteri sopra esposti conducono l'algoritmo di risoluzione a comportarsi inmodo goloso, nel senso che spingono l'algoritmo a operare delle scelte solamente sulla basedella situazione attuale, senza cercare di prevedere le conseguenze di tali scelte nelfuturo:

per questo motivo, diciamo che i Codici 6.1 e 6.2 si basano sul paradigma dell'algoritmogoloso (greedy algorithm). Tale paradigma (che difficilmente può essere formalizzato inmo do più preciso) risulta talvolta, ma non così spesso, vincente: il suo successo, inverità, dipende quasi sempre da proprietà strutturali del problema che non sempre sonoevidenti. Nel caso dei du e problemi su grafi a intervalli, abbiamo sf rutt ato proprietàgeometriche del problema intrinseche alla sua restrizione al caso di grafi a intervalli.

Osserviamo che, nonostante la sua semplicità, il paradigma dell'algoritmo golosonon risulta essere quasi mai una strategia facile da applicare. Una delle difficoltà princi-pali da affrontare consiste nel decidere in che ordine dobbiamo esaminare i componentidi un'istanza del problema. Nel caso del problema del massimo insieme indipenden-te in grafi a intervalli, avremmo potuto decidere di esaminare gli intervalli ordinati in

4 No n è in realtà la prim a volta che inc ont ria mo un alg orit mo che si com por ta in questo mo do: la

strategia SJF descritta nel Paragrafo 2.2, infatti, ordina i programmi da eseguire sulla CPU in base al loro

tempo di esecuzione e, quindi, li assegna a essa in base a tale ordinamento.

Page 238: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 238/373

 

base al loro tempo di inizio, piuttosto che in base al loro tempo di fine: in tal caso, èfacile verificare che l'algoritmo goloso corrispondente non calcola un insieme indipen- rdente di cardinalità massima. Supponiamo, infatti, che gli intervalli siano [1,10], [2, 5]e [6,9]: in tal caso, l'algoritmo goloso restituisce come soluzione l'insieme formato dalsolo intervallo [1, 10], mentre l'insieme di cardinalità massima è costituito dagli altri dueintervalli.

L'algoritmo goloso può essere riformulato in termini di programmazione dinamica incui, una volta ordinati in modo opportuno gli elementi di un'istanza, decomponiamo unproblema in un unico sotto-problema, definito eliminando l'ult imo elemento nell'ordinespecificato: la fase di ricombinazione consiste nel decidere in modo goloso se e in chemodo aggiungere tale elemento alla soluzione del sotto-problema.

In conclusione, il paradigma dell'algoritmo goloso non è, in generale, più sempliceda utilizzare di quello della programmazione dinamica e raramente consente di risolvere

in modo esatto un problema computazionale (anche se esso è stato applicato con un certosuccesso nel campo delle euristiche di ottimizzazione combinatoria e degli algoritmi diapprossimazione).

6.3 Grafi casuali e modelli di reti complesse

In molti contesti una struttura modellabile mediante un grafo deriva come conseguenzadi una serie di attività svolte in modo indipendente, senza alcun coordinamento comune.I risultanti grafi non sono ottenuti da processi guidati da un qualche tipo di controllo

centrale, ma piuttosto si accrescono per mezzo di processi evolutivi, in cui nuovi nodie nuovi archi sono aggiunti al grafo sulla base di informazioni e caratteristiche locali,ignorando la struttura generale del grafo stesso. Tali strutture, denominate reti comples-se, presentano l'ulteriore e non secondaria caratteristica di avere una dimensione moltoelevata, sia in termini di archi che di nodi.

Il più noto esempio di tale situazione è fornito dal World Wide Web, che può esseremodellato mediante un grafo orientato in cui i nodi rappresentano pagine e gli archirappresentano riferimenti {link) tra le pagine stesse. Tale grafo si è formato in modo"casuale", intendendo con tale termine il fatto che la creazione (o l'eliminazione) di nodi

o archi avviene al di fuori di controlli centralizzati: ogni utente decide individualmentee indipendentemente i contenuti delle proprie pagine e i riferimenti ad altre pagine.

Altri tipi di situazioni modellate mediante reti complesse compaiono in contesti mol-to diversi tra loro, di cui mostriamo ora alcuni esempi significativi. Le reti sociali sonousate per rappresentare persone, o gruppi di persone, e un qualche tipo di relazione traloro, come l'amicizia e la conoscenza, ma anche relazioni di collaborazione nella produ-zione scientifica (ad esempio Erdos number) o compresenza quali interpreti in uno stesso ¿film (ad esempio Six Degrees of Kevin Bacon, in breve ÓDKB). A tale proposito, il grafo

Page 239: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 239/373

 

non orientato di 6DKB è un esempio frequentemente citato di tali reti, i cui nodi sonogli attori cinematografici e dove due nodi sono collegati mediante un arco se e solo sei due attori corrispondenti hanno recitato in uno stesso film. Dato un attore, il giococonsiste nel trovare un cammino fino all'attore Kevin Bacon; in alternativa, dati due at-tori, bisogna trovare un cammino che li collega. Alla data del 1997, il grafo di 6DKBconteneva più di 200.000 nodi collegati da circa 13 milioni di archi (nel 2005, il numerodi nodi è diventato circa 800.000, ma per motivi di completezza faremo riferimento aidati del 1997). Un'ulteriore tipologia di reti sociali, infine, è rappresentata da grafi incui gli archi rappresentano comunicazioni di un qualche tipo, quali ad esempio scambiodi messaggi (di posta ordinaria o elettronica) o chiamate telefoniche (ad esempio quellonoto come AT&T caligraph).

Le reti di informazioni sono utilizzate per rappresentare relazioni di rimando tra do-cumenti di una qualche natura. Il grafo del Web è un esempio di rete di questo tipo, co-me anche i grafi costruiti per rappresentare l'insieme delle citazioni tra articoli di ricercascientifica che appaiono nella loro bibliografia.

Le reti tecnologiche rappresentano la struttura di reti di tipo tecnologico, quali adesempio reti di distribuzione (elettrica, telefonica, ma anche Internet) o reti di trasporto(strade, ferrovie, collegamenti aerei e marittimi).

Le reti biologiche infine modellano relazioni che sorgono in campo biologico, sia alivello di biologia molecolare e genetica che di etologia (relazioni predatore-preda) e dimedicina (reti neurali, reti vascolari).

L'obiettivo dello studio di grafi di questo tipo è quello di ottenere una caratterizza-

zione di parametri ritenuti significativi: ad esempio, cerchiamo le proprietà che caratte-rizzano la rete come il diametro del grafo corrispondente (la distanza tra i due nodi piùlontani), o la distribuzione del grado dei nodi.

Inoltre, visto che oggetto dello studio non sono in realtà i singoli grafi, ma le famigliedi grafi che modellano una stessa tipologia di relazione o sistema (ad esempio tutti i grafiche rappresentano la relazione di conoscenza tra persone, per insiemi diversi di persone),tale caratterizzazione è di tipo statistico.

In definitiva, cerchiamo di ottenere una caratterizzazione, e quindi un modello ma-tematico, della struttura generale di un qualunque grafo di grandi dimensioni che rap-

presenti una rete del tipo di quelle citate sopra. Tale caratterizzazione potrà essere uti-lizzata per comprendere le caratteristiche fondamentali del processo che ha portato allacostruzione del grafo risultante e per costruire algoritmi che generino istanze di grafistatisticamente simili a quelli considerati.

Ciò permetterà di simulare e prevedere il risultato futuro del processo alla base dellacostruzione del grafo esaminato, e quindi il comportamento futuro della rete. Usando ta-le conoscenza possiamo inoltre costruire algoritmi che operano efficientemente, almenoin senso statistico, su tali grafi, utilizzando a tal fine le caratteristiche dei grafi stessi.

Page 240: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 240/373

 

Esaminando le reti complesse finora studiate dai ricercatori, emergono alcune carat-teristiche comuni. In primo luogo, il numero di archi è limitato rispetto al loro numeromassimo, vale a dire m n ( n — 1 )/2, dove n è il numero di nodi e m è il numero diarchi. È stato cioè osservato che le reti complesse esaminate tendono a essere sparse.

In secondo luogo, le reti complesse tendono a presentare raggruppamenti di nodi oaggregazioni (cluster): intuitivamente, un insieme di nodi adiacenti a uno stesso nodotende a formare una cricca. Co me vedremo, l'abbondan za di aggregazioni in una reteviene misurata a partire da un parametro associato ai singoli nodi, denominato coeffi-

ciente di aggregazione: dato un nodo v, il relativo coefficiente di aggregazione C v è ilrapporto tra il numero di archi presenti nel sottografo indotto dai nodi adiacenti a v, eil massimo numero possibile di archi tra tali nodi, corrispondente al caso in cui tali nodiformino una cricca. Formalmente,

= ^ (6-1)d v ( d v - 1)

dove dv è il grado di v, m v è il numero di archi nel sottografo indotto dai nodi ad esso

adiacenti e dv( d v — 1 )/2 è il numero di archi nella cricca Kdv.

Il coefficiente C di aggregazione di un grafo, cui faremo riferimento nel seguito, èallora definito come la media, sull'insieme dei nodi del grafo, di C v , vale a dire C =n Y-v Cv E stato quind i osservato che, se consideriamo il coeffic iente di aggregazionedi una rete complessa, tale coefficiente tende ad assumere valori elevati.

In terzo luogo, tali reti presentano un diametro relativamente piccolo in considerazionedelle due caratteristiche precedenti: il diametro, vale a dire la distanza tra i due nodi piùlontani, tende a essere più elevato per grafi sparsi (bisogna percorrere più archi per andareda un nodo a un altro) e per grafi aventi un maggior numero di aggregazioni (gli architendono a collegare solo nodi vicini, e archi che realizzano collegamenti "lunghi" sonorari). Invece, nonostante siano sparse e contengano un numero elevato di aggregazioni,molte reti complesse con n nodi hanno un diametro significativamente inferiore al suovalore massimo n.

In quarto luogo, le reti complesse presentano una grande varietà nella distribuzione

dei gradi dei nodi: vale a dire, esse contengono un numero significativo di nodi per ognipossibile valore del grado, all'interno di un intervallo ampio di tali valori.

Descriviamo ora tre modelli classici di grafi casuali utilizzati per descrivere e generare

grafi aventi caratteristiche quanto più possibile in accordo, da un punto di vista stati-

stico, con le proprietà fondamentali delle reti complesse illustrate sopra, presentando al

contempo dei semplici algoritmi per generare tali grafi in modo efficiente.

I tre modelli verrano in particolare analizzati dal punto di vista della distribuzione

statistica del coefficiente di aggregazione, del diametro e dei gradi dei nodi.

Page 241: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 241/373

 

GeneraErdòsRényi (n, p) : {pre: 0 p 1)

FOR  (u = 0; u < n; u = u + 1) {

A[u] [u] = 0;

FOR (V = u + 1; v < n; v = v + 1) {

IF (random() < p) {

A [u] [v] = A [v] [u] = 1;> ELSE {

A[u] [v] = A[v] [u] = 0;

>>

>RETURN A;

Codice 6.3 Algoritmo per la generazione di un grafo casuale G„, p alla Erdòs-Rènyi.

6.3.1 Grafi casuali alla Erdòs-Rényi

Il modello classico di grafi non orientati costruiti in base a un processo casuale è il co-siddetto modello di Erdòs-Rényi, dal nome dei due matematici ungheresi che lo hannointrodotto alla fine degli anni '50. In tale modello, ipotizziamo di avere un insieme di nnodi e un valore prefissato di probabil ità p, con 0 ^ p ^ 1. Nel modello supponiamoinoltre che, data una qualunque coppia di nodi u e v, l'arco (u, v) esista con probabilità p,indipendentemente dalle caratteristiche strutturali del grafo, come ad esempio dalla pre-

senza di altri archi incidenti su u o v. La generazione di un grafo casuale di questo tipo,indicato con la notazione G n , p , può essere effettuata in tempo 0(n 2 ) mediante il Codi-ce 6.3 che utilizza una primitiva random() per generare un valore reale r pseudocasualeappa rtene nte all'intervallo 0 ^ r < 1, in modo unifo rme ed equiprobabile.

ALVIE: generazione di grafi casuali alla Erdòs-Rènyi

Osserva, sperimenta e verifica

ErdosRenyiGraph

La distribuzione dei gradi dei nodi in un grafo casuale alla Erdòs-Rényi è descritta

dalla probabilità pd che un nodo abbia grado d, probabilità caratterizzata dalla nota

distribuzione di Bernoulli

pd = ( n ^ 1 ) p d d - p ) n - d - 1 (6.2)

Page 242: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 242/373

 

La formula (6.2) deriva dall'osservazione che il grado di un nodo v è pari a d se esisteun sottoinsieme di d nodi, tra gli altri n — 1 nel grafo, a esso adiacenti, tale che nessunodei rimanenti n — d — 1 nodi è adiacente a v. Ricordiamo che il numero di sottoinsiemidi cardinalità d in un insieme di n — 1 elementi è dato dal coefficiente binomiale ')•Inoltre, per ciascun sottoinsieme, la probabilità che tutti i suoi d nodi siano adiacentia v è pari a p d , mentre la probabilità che nessuno degli altri n — d — 1 lo sia è data da( 1 — p ) n _ d _ 1 : da ciò deriviamo la formula (6.2). Una nota proprietà della distribuzionedi Bernoulli in (6.2) è la sua approssimabilità, per valori di n sufficientemente grandi,per mezzo della distribuzione di Poisson

pa = z - i r  (6.3)

dove z = p(n — 1) è il grado medio di un nodo.

I grafi casuali G n , p presentano ulteriori caratteristiche significative elencate di se-guito. Come possiamo vedere nella formula (6.3), la probabilità che un nodo abbiagrado d decresce esponenzialmente (ovvero, potremmo dire, precipitevolissimevolmente)al crescere di d. Possiamo mostrare tale proprietà facendo uso della seguente formula diStirling per l'approssimazione del fattoriale:

d! ss dde~dv / 27td (6.4)

Applicando tale approssimazione alla formula 6.3 otteniamo

c h e p ^ « (f)d e- zV2^d,dal che possiamo concludere che se d > ez allora p^ decresce esponenzialmente alcrescere di d.

Da questo fatto consegue che in un grafo G n , p i gradi dei nodi tendono a essereaddensati intorno al valore medio p(n— 1 ), con frazioni di nodi aventi grado maggiore ominore di tale valore che tendono rapidamente a svanire man mano che ci allontaniamoda esso (la nota curva a forma di "campana" centrata attorno al valore medio).

A proposito del coefficiente di aggregazione di un grafo di questo tipo, osserviamoche tale valore risulta il più basso possibile, a parità di numero di archi nel grafo stesso:

questo deriva dalla considerazione che in un grafo di questo tipo gli archi tendono aessere distribuiti in modo uniforme (addensamenti di archi su un sottoinsieme di nodirisultano poco probabili). Det to altrimenti, se un nodo ha z nodi adiacenti, allora ilnumero massimo di archi possibili tra i suoi nodi adiacenti è pari a z(z — l)/2 e ilnumero medio di archi presenti tra tali nodi è pari a pz(z — 1 )/2: da ciò consegue cheil coefficiente C di aggregazione del grafo è pari a p. Se not iamo però che p è in mediala frazione di archi presenti in un qualunque insieme di nodi, possiamo convincerci chenon c'è nessun particolare addensamento di archi tra nodi vicini di uno stesso nodo, equindi nessun effetto significativo di aggregazione.

Page 243: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 243/373

 

Il diametro del grafo è con alta probabilità logaritmico nel numero di nodi: talecaratteristica è in realtà dovuta al fatto che, con alta probabilità, il diametro non diffe-risce significativamente dalla distanza media tra due nodi scelti in modo casuale e chequest 'ult ima è logaritmica nel numero di nodi. Per giustificare informalmente quest'af-fermazione osserviamo che, indicando con z il grado medio, un nodo avrà all'incirca znodi adiacenti (a distanza 1), z2 nodi a distanza 2, z3 nodi a distanza 3 e, in generale,all'incirca z s nodi a distanza s. Attraverso questo processo di ramificazione è possibiledimostrare che, con alta probabilità, per la distanza media s abbiamo che z s = 0(n), dacui otteniamo che s = 0(logn/ logz).

Nonostante le caratteristiche appena enunciate siano di grande utilità in diversi con-testi, i grafi casuali di tipo Erdòs-Rényi non sembrano però adatti a rappresentare reticomplesse come quelle discusse in precedenza. Tale difformità deriva da due fattori: co-me osservato sopra, da un lato la distribuzione dei gradi in G n , p risulta troppo accentrataintorno al valore medio rispetto a quanto non avvenga in una rete complessa; dall'altronon si presenta in G n , p alcun fenomeno di aggregazione, al contrario di quanto avvienenelle reti complesse.

Un'ulteriore caratteristica significativa dei grafi casuali di tipo Erdos-Rényi riguardal'andamento della dimensione delle varie componenti connesse al crescere della proba-bilità p, il quale presenta un effetto cosiddetto di soglia: vale a dire che la dimensionedella componente connessa più grande nel grafo cambia al di sotto e al di sopra di undeterminato valore di p, denominato appunto soglia.

È interessante concludere con l'osservazione che in qualunque grafo, generato ca-sualmente come sopra o meno, esiste sempre un sottografo regolare, come conseguenzadella teoria di Ramsey (introdotta dal matematico inglese Frank Ramsey nei primi delNovecento). Ricordiamo che un insieme indipendente è un sottoinsieme I r di r vertici,il cui sottografo indotto non possiede archi e che I r è complementare alla cricca Kr cheinvece possiede tutt i gli archi possibili. Il numero di Ramsey R(k, l) è il più piccolointero tale che ogni grafo con n > R(k, l) nodi contiene K^ oppure li come sottografo.Anche se, in generale, è difficile calcolarlo, Ramsey ha dimostrato che tale numero esistesempre. In altre parole, il disordine totale è impossibile.

6.3.2 Grafi casuali con effetto di piccolo mondoA un più attento esame, le reti complesse si collocano in una situazione intermedia tra igrafi casuali di tipo Erdòs-Rényi e i grafi regolari, caratterizzati dal fatto che i nodi hannotutti lo stesso grado.

In particolare, consideriamo i grafi regolari Rni d con n nodi, ciascuno di grado d pa-ri, in cui ogni nodo u è collegato a d nodi in modo circolare (d/2 nodi che lo precedonoe altrettanti che lo succedono), come illustrato nella Figura 6.14, dove ciascun nodo u ècollegato ai nodi v = u + 1 ev = u + 2 modulo n in quanto d = 4. Il diametro della rete

Page 244: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 244/373

 

Figura 6.14 Esempio di rete Rl2,4.

nella figura è pari a 3, mentre il coefficiente di aggregazione risulta pari a 1/2, poiché

ogni nodo ha d = 4 vicini collegati da 3 archi (a fronte di 6 archi possibili). In generale,

il diametro di Rn,d è 0(n/d), mentre il coefficiente di aggregazione rimane 1/2.

Osserviamo quindi che, da un lato, le reti R n,d hanno diametro e coefficiente di

aggregazione elevati mentre, dall'altro, i grafi di tipo Erdòs-Rényi hanno diametro e

coefficiente di aggregazione limitati. In tale scenario, le reti complesse sono ibride poiché

combinano un diametro limitato con un elevato coefficiente di aggregazione pur essendosparse: questa caratteristica viene indicata con il termine piccolo mondo (small world)

ed è stata ampiamente divulgata nella letteratura e nella pubblicistica.

L'origine del concetto di piccolo mondo deriva in effetti dallo studio di reti sociali, e

in particolare della rete delle conoscenze tra persone: un famoso esperimento effettuato

dal sociologo Stanley Milgram negli anni '60, cercò di valutare la distanza media tra

una qualunque coppia di persone su un grafo di tale tipo, mediante un'operazione di

instradamento "cooperativo" di messaggi.

Nell'esperimento, una lettera doveva viaggiare da un mittente nel Nebraska a un

destinatario nel Massachusetts. La lettera doveva essere inviata dal mi tten te a un suoconoscente a scelta, in modo tale da avvicinare, a suo avviso, il messaggio al destinatario:

il conoscente in questione, e tutti gli intermediari successivi, ricevevano la richiesta di

operare nello stesso modo, fino alla consegna della lettera al destinatario effettivo.

L'esperimento appurò, sulla base di molti tentativi e contrariamente all'intuizione,

che il numero medio di intermediari per ogni messaggio ricevuto dal destinatario era in

effetti inferiore a 6: tale risultato dette luogo alla locuzione "6 gradi di separazione" ed è

anche la ragione del numero 6 nel nome del grafo 6DKB.

Page 245: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 245/373

 

Figura 6. 15 Creazione di una rete di piccolo mondo a partire dalla rete regolare nella Figura 6.14.

Un esempio di rete sociale che presenta caratteristiche di piccolo mondo è, infatti,dato proprio dal grafo di 6DKB (dati riferiti all'anno 1997, in cui il numero n di nodiera circa uguale a 225000). Questa rete, con grado medio z pari a 61, ha una distanzamedia tra due nodi pari a 3,65 e un coefficiente di aggregazione C pari a 0,79. Volendomodellare tale rete come un grafo casuale di Erdòs-Rényi, dobbiamo fissare il valore dip pari a z/(n — 1 ) ~ 0,00027: in tal caso, la distanza media è circa pari a logn/ logz ~2,99 e il coefficiente di aggregazione è uguale a p ~ 0, 000 27 . La rete 6DKB presenta

quindi caratteristiche simili a un grafo casuale per quanto riguarda la distanza mediatra nodi, per la quale abbiamo un incremento di circa il 22%, ma diverse rispetto alcoefficiente di aggregazione, che risulta superiore per un fattore di circa 3000.

La stessa situazione si presenta per il grafo orientato del World Wide Web, avendoanche quest'ultimo caratteristiche di piccolo mondo. Uno studio effettuato sul sottoin-sieme del World Wide Web composto dai nodi nel domin io . ed u ha ottenuto valori paria 4,062 per la distanza media tra due nodi (4,048 per il corrispondente grafo casuale) ea 0,156 per il coefficiente di aggregazione (0,0012 per il corrispondente casuale).

Per la loro caratteristica intermedia, le reti di piccolo mondo sono solitamente ge-nerate partendo da una rete regolare di grado d che viene poi modificata attraverso unuso limitato di casualità, come illustrato nel Codice 6.4, partendo da una rappresenta-zione mèdiante liste di adiacenza vuote, create attraverso la funzione NuovaLista, chevengono opportunamente riempite con il metodo A g gi u ng i Li s t aA di a ce n za . Il co-dice produce delle pertubazioni casuali a partire dalle reti R n,a: infatti, sostituendo lerighe 7-17 con la sola riga 16, otteniamo Rn,d- Tali righe simulano l'eliminazione di unarco (u,v) dalla rete regolare e il contemporaneo inserimento di un arco (u,w) verso unnodo w / v, determinato in modo casuale. In particolare, fissata una probabilità p ' di

Page 246: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 246/373

 

GeneraPiccoloMondoCn, p', d):

FOR  (u = 0; u < N; u = u + 1)

listaAdiacenza[u] = NuovaListaC );

FOR  (u = 0; u < n; U = u + 1)

FOR  (j = 1; j <= d/2; j = j + 1) {

v = (u+j) '/. n;i = (n-1) x randomO ;

IF (i < v) {

w = i;

> ELSE {

w = i + 1;

>IF (randomO < p') {

AggiungiListaAdiacenzaC u, w );

> ELSE {

AggiungiListaAdiacenzaC u, v );>

>

AggiungiListaAdiacenzaC x, y ):

e.chiave = y; listaAdiacenza[x].InserisciFondoC e );

e.chiave = x; listaAdiacenza[y].InserisciFondoC e );

Codi ce 6.4 Generazione di un grafo casuale con effetto di piccolo mondo ottenuta per turbandola costruzione di un grafo non orientato regolare di grado d pari.

"ridirezionamento", il codice considera l'uno dopo l'altro tutti gli archi della rete: ogniarco (u,v) considerato viene eliminato con probabilità p' e sostituito da un arco (u, w),dove w è un nodo determinato in modo casuale e uniforme tra i rimanenti n — 1 nodi(righe 7-12), come illustrato nella Figura 6.15.

ALVIE: generazione di grafi casuali con effetto di piccolo mondo

Osserva, sperimenta e verifica • •

SmallWorldGraph _

Poiché il numero di archi è m = O(dn), l'algoritmo richiede tempo lineare nelladimensione del grafo. I nuovi archi introdotti vengono a fungere da "scorciatoie" traregioni diverse del grafo e, come vedremo, sono sufficienti a limitare la distanza media

Page 247: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 247/373

 

Figur a 6.16 Andamento di L(p')/L(0) (curva continua) e C(p')/C(0) (curva tratteggiata) alcrescere della probabilità p'.

tra due qualunque nodi. Al tempo stesso, il coefficiente di aggregazione della rete nonviene modificato in modo sostanziale dai nuovi archi introdotti, se la probabilità p' èsufficientemente piccola.

Data la probabilità p', indichiamo con L(p') la distanza media tra due nodi nellarete risultante dal Codice 6.4 e con C(p') il suo coefficiente medio di aggregazione.Possiamo confrontare queste due quanti tà con quelle della rete Rn,d di partenza, che sonoindicate con L(0) e C(0) in quanto nessun arco viene cambiato: la Figura 6.16 mostra

l'andamento, in funzione della probabilità p' riportata in ascissa in scala logaritmica, deirapporti L(p')/L(0) (curva continua) e C(p')/C(0) (curva tratteggiata).

Esiste un intervallo all'interno del quale la distanza media è diminuita significativa-mente mentre il coefficiente di aggregazione è rimasto immutato: in questo intervallo larete presenta quindi l'effetto desiderato di piccolo mondo. In particolare, potremmo sce-gliere 0,01 come valore di p', ottenendo una trascurabile diminuzione del coefficientedi aggregazione a fronte di una riduzione di oltre l'80% della distanza media.

Per quanto riguarda il grado medio dei nodi nella rete così costruita, esso risultaindipendente dal valore p ' in quanto il numero di archi rimane costante al variare di tale

probabilità. Osserviamo inoltre che, al variare di p ' , passiamo da una rete in cui tut ti inodi hanno lo stesso grado, per p' = 0, a una rete simile ai grafi casuali di Erdòs-Rényi,

perp' = 1.

In ogni caso, la distribuzione del grado dei nodi risulta concentrata intorno al valor

medio, cosa che non si verifica in generale per le reti complesse esaminate. Per superarequesta limitazione, vediamo un modello alternativo che consente di rappresentare e ot-tenere grafi casuali con le medesime distribuzioni dei gradi delle reti complesse osservate"in natura", pur non assicurando di generare reti di piccolo mondo.

Page 248: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 248/373

 

6.3.3 Grafi casuali invarianti di scala

Una delle caratteristiche rilevanti nelle reti complesse è quella relativa alla distribuzionedei gradi dei nodi. Differentemente dai grafi casuali, questa distribuzione segue una leggedi potenza (power law) del tipo Pd ~ d Y in funzione del grado d, dove y > 0 è una

costante che dipende dalla rete considerata e che risulta, per i casi osservati, compresatra 2,1 e 4: per esempio, y ~ 2,3 per il grafo di ÓDKB, mentre y ~ 3 per il grafo chemodella le citazioni tra articoli di ricerca.

La Figura 6.17 riporta un diagramma a scala logaritmica su entrambi gli assi, chemostra l'andamento di una distribuzione con legge di potenza e di una con legge espo-nenziale, come quella di un grafo casuale. La distribuzione esponenziale presenta unintervallo ristretto in cui assume valori elevati seguito da un punto di caduta ( c u t - o f f )

sulle ascisse, oltre il quale il numero di nodi è praticamente nullo: in sostanza, la rete nonpresenta nodi con grado significativamente superiore al punto di caduta, che definisce,

quindi, un limite superiore sul possibile grado di un nodo.E possibile verificare che una distribuzione a legge di potenza risulta invariante di

scala, vale a dire che essa appare uguale a se stessa, indipendentemente dalla scala a cuiviene esaminata. Dic iamo che una funzione f(x) è invariante rispetto alla scala se valela proprietà f(ax) = g(a)f(x) per ogni a, dove g( ) è una funzione dipendente da f( ).L'idea di fondo è che un incremento di un fattore a nella scala (e quindi nell'unità dimisura di x) non determina variazioni di f(x), eccetto che per un fattore moltiplicativodi scala.

Anche se, in linea di principio, tale proprietà risulta soddisfatta soltanto da funzioni

a legge di potenza, del tipo f(x) = cx a , per le quali vale f(ax) = c(ax) a = (ca a ) x a ,è uso comune considerare invarianti di scala anche funzioni che all'infinito tendono acoincidere con funzioni del tipo suddetto. In generale, un grafo è detto invariante di sca-la se la distribuzione dei gradi dei suoi nodi è una funzione invariante di scala e quindi,in particolare, se segue una legge di potenza. Le reti complesse osservate presentano lacaratteristica di essere invarianti di scala, almeno in linea di principio: infatti, esse pre-sentano una distribuzione dei gradi dei nodi che segue una legge a potenza fino a un certopunto, oltre il quale il numero dei nodi decresce rapidamente, anche in considerazionedel fatto che la rete ha comunque una dimensione finita.

In una rete complessa, quindi, la probabilità che un nodo abbia grado d decresce polinomialmente al crescere di d: pertanto, in una tale rete il grado dei nodi è molto piùdifferenziato che non in un grafo casuale alla Erdòs-Rényi, dove decresce esponenzial-mente all'allontanarsi dal valore medio. In una rete complessa compaiono nodi aventigrado più elevato del grado medio, f enomeno assente nei modelli finora discussi. Seconsideriamo la struttura del grafo del World Wide Web, ad esempio, possiamo osser-vare che al suo interno compare una quantità significativa di portali, corrispondenti anodi aventi grado molto elevato. In effetti, è possibile verificare empiricamente che la

Page 249: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 249/373

 

Figura 6. 17 Distribuzione con legge a potenza (linea tratteggiata) e distribuzione esponenziale(curva continua) su scala logaritmica.

distribuzione dei gradi in ingresso nel World Wide Web rispetta una legge a potenza con y ~ 2,1, mentre la distribuzione dei gradi in uscita ne rispetta una con y ^ 2,45.

L'invarianza di scala delle reti complesse sembra derivare da due fattori. Da un lato, lereti non sono costruite inserendo archi su un insieme di nodi inizialmente privo di archi;al contrario, esse si espandono anche per mezzo di un inserimento continuo di nuovinodi. Ad esempio, il World Wide Web cresce nel tempo mediante la creazione di nuovepagine, che vengono collegate a quelle già esistenti, così come la rete di collaborazionitra attori si estende con l'aggiunta di nuovi debuttanti . Inoltre, i nuovi nodi t endonoa essere collegati ai nodi aventi grado più elevato: ad esempio, una nuova pagina delWorld Wide Web tenderà ad avere collegamenti verso i siti più noti, come molte altrepagine. La probabil ità con cui un nuovo nodo si collega ai nodi esistenti non è quindiuniforme: al contrario, i nodi aventi grado più elevato hanno maggiore probabilità diessere riferiti dalle nuove pagine secondo il principio per cui "il ricco diventa sempre piùricco". Tra i vari approcci introdotti per costruire grafi non orientati aventi distribuzionedei gradi che rispettino una legge a potenza, il più semplice procede come mostrato nelCodice 6.5, utilizzando liste di adiacenza vuote, create con NuovaLista ed estese con

Page 250: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 250/373

 

GeneraScalabile(n):

arrayArchi = {(0,1), (1,2), (2,0)};

FOR  (t = 0; t < 3; t = t + 1)

listaAdiacenza[t] = NuovaLista( );

AggiungiListaAdiacenza( 0, 1 );

AggiungiListaAdiacenza( 1, 2 );AggiungiListaAdiacenza( 2, 0 );

FOR  (t = 2; t < n-1; t = t + 1) {

i = (2 x t - 1) x randomO;

(u, w) = arrayArchi [i];

arrayArchi[2t-l] = (t+1, u);

arrayArchi[2t]= (t+1, w);

listaAdiacenza[t+l] = NuovaLista( );

AggiungiListaAdiacenza( t+1, u );

AggiungiListaAdiacenza( t+1, w );

}

Codice 6.5 Algoritmo per la generazione di un grafo casuale scalabile.

• Il procedi men to inizia all'istante t = 1, in una configurazione in cui il grafo ècomposto da una cricca K3 con 2t + 1 = 3 archi (e i cui nodi sono 0, 1 , 2 comemostrato nella righe 2—7).

• A ogni istante t ^ 2, il nodo t + 1 viene creato e due nuovi archi incidenti sudi esso vengono aggiunti al grafo. A tal fine, un arco (u,w) viene determinatoin modo casuale (e con distribuzione uniforme) tra i 2t — 1 archi già presenti nelgrafo e memorizzati in a r r a y A r c h i (righe 9- 10 ): i due nuovi archi introdo ttisono allora (t + l,u) e (t + l,w), dando luogo a un grafo con 2t + 1 archi checollegano t + 2 nodi (righe 11-15).

Nella Figura 6.18 mostriamo un esempio di costruzione di un grafo con distribuzio-ne dei gradi dei nod i secondo la legge a potenza, limitatamen te ai primi ot to nodi. Perogni passo, riportiamo i valori di t, u,w al fine di determinare quali sono gli archi che

sono stati inseriti nel passo stesso.

ALVIE : generazione di grafi casuali con invariante di scala

Osserva, sperimenta e verifica

ScaleFreeGraph

Page 251: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 251/373

 

Figura 6.18 Costruzione di grafo scalabile.

Nella costruzione precedente, il grado di un nodo aumenta, a ogni passo, con proba-bilità tanto maggiore quanto maggiore è il grado del nodo stesso: ciò deriva dal fatto chela probabilità di scegliere un arco incidente su un nodo aumenta al crescere del suo grado

(in quanto gli archi vengono scelti in modo uniforme). Pertanto, nodi "ricchi" tendonoa diventare sempre più ricchi.

Il grafo risultante rispetta una legge a potenza. Consideriamo l 'evento che un qua-lunque nodo v abbia grado d all'istante t e indichiamo con p(d, t) la sua probabilità.Tale evento si verifica:

1. se v aveva grado d — 1 all'istante t — 1 e uno degli archi incidenti su v è stato sceltotra i 2t — 1 archi del grafo, oppure

2. se v aveva grado d all' istante t — 1 ed è stato scelto uno dei 2t — 1 — d archi nonincidenti su v.

Dato che la probabilità che il nuovo arco scelto sia incidente a v è pari al rapportotra il grado di v e il numero complessivo di archi nel grafo, ne consegue che, che p(d, t)può essere espressa mediante la seguente relazione di ricorrenza:

p(d, t) = ^ - ^ p ( d - l , t - l ) + 2 t ~ ^ ~ d p ( d , t - l ) . ( 6 . 5 )

Page 252: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 252/373

 

È possibile dimostrare, a partire dall'equazione (6.5), che la probabilità che un nodoabbia grado d tende, al crescere del numero di nodi, al valore

P ( J ) = d ( d + l ' ) 2 ( d + 2) = e ' d ~ 3 > -

verificando così che il grafo ottenuto modella una rete invariante di scala.

6.4 Opus libri: motori di ricerca e classificazione

La disponibilità di grandi quantità di informazioni resa possibile, in particolare, da Inter-net e il World Wide Web, se da un lato fa sì che abbiamo accesso a una grande quantitàdi documenti di interesse, dall'altro introduce la necessità di avere informazioni sullamaggiore o minore significatività, ai propri fini, dei documenti accessibili.

Una tipica operazione effettuata nell'accesso al Web è quella di richiedere tutti i do-cumenti che soddisfano una certa interrogazione (query), generalmente definita usando itermini presenti nei documenti (per esempio, trovare tutti i documenti contenenti i ter-mini "web" e "searching"). Quest'operazione viene resa possibile dall'utilizzo di motoridi ricerca, ovvero di sistemi disponibili in linea che effettuano una raccolta e una catalo-gazione dei documenti accessibili sul Web e che consentono di interrogare velocementeil catalogo così costruito utilizzando, ad esempio, le liste invertite descritte nel Paragra-fo 5.6 (una nota regola empirica stabilisce che la risposta debba essere fornita entro 200millisecondi affinché un essere umano la percepisca come rapida).

Data però la dimensione del Web, la semplice restituzione di un elenco, che puòessere molto lungo, di tutti i documenti che soddisfano l'interrogazione non fornisceuna soluzione pratica al problema di estrarre le informazioni più rilevanti (per esempio, laricerca della parola "algoritmo" restituisce oltre due milioni di documenti mentre quelladi "web searching" ne restituisce oltre 150 milioni). Vorremmo estrarre dall'elenco di talidocumenti un sottoinsieme S con le seguenti caratteristiche:

• S contiene relativamente pochi documenti (un centinaio al massimo);

• i document i in S sono rilevanti per l'u tente che ha formulato l'interrogazione;

• molti dei documenti in S provengono da fonti autorevoli.

E quindi necessario che i documenti che soddisfano una certa interrogazione ven-gano ordinati in modo tale che i più significativi siano presentati prima degli altri: a talfine, un motore di ricerca effettua, oltre alla ricerca dei documenti, la loro classificazionein base a un valore di significatività o rango (rank) assegnato a ciascun documento, perrestituire i documenti stessi ordinati per rango decrescente.

Page 253: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 253/373

 

Data una collezione di documenti D, intendiamo definire una funzione rango :D i—> R tale che, per ogni coppia di documenti di e d2 in D, consideriamo di piùrilevante di d.2 se r an go(d i ) > ra ng o( d2 ).

Nel caso di collezioni di documenti convenzionali la funzione rango veniva tradi-

zionalmente calcolata sulla base del loro solo contenuto. Con l'avvento delle tecnologieipertestuali, in cui i documenti possono essere arricchiti con riferimenti ad altri docu-menti (come nel caso del Web in cui i documenti sono pagine collegate tra di loro),l'insieme dei collegamenti tra i documenti stessi viene utilizzato per estrarre ulteriori in-formazioni sulla loro significatività: in altre parole, la funzione di rango usa, oltre alcontenuto dei documenti, anche la struttura a grafo orientato dell'intera collezione (incui i vertici sono i documenti e gli archi orientati sono i collegamenti tra di loro).

L'intuizione, in questo caso, è che quanto più un documento è riferito da altri docu-menti, tanto più esso è significativo all'interno della collezione D: infatti, tali riferimenti

sono ritenuti essere l'espressione di una forma latente di giudizio umano.In un certo senso, la significatività di un documento viene determinata in modo

democratico, attraverso un meccanismo di votazione in cui l'introduzione di un riferi-mento dal documento di al documento d2 assume la valenza di un voto espresso da dia favore di d2- Se consideriamo quindi il grafo del Web definito sopra, l'intuizione èche una pagina è tanto più significativa quanto maggiore è il grado di ingresso del nodocorrispondente, cioè il numero di archi entranti in tale nodo.

Questo approccio alla misurazione del rango di un documento rende il risultato ditale processo particolarmente robusto rispetto a tentativi di modificarne in modo arti-

ficioso il risultato, quando una grande quantità di "elettori" ovvero di documenti sonocoinvolti. In particolare, nelle situazioni in cui il rango di un documento è calcolato apartire solo dal suo contenuto, quest'ultimo può essere manipolato dal suo produttore(ad esempio, introducendo termini appositi) al fine di aumentare in modo indebito ilrango del documento: tale attività è nota, nel caso del Web, come search spam e consiste,ad esempio, nel manipolare elementi di una pagina quali il titolo, le parole chiave e itesti associati ai riferimenti (snippet ). Al contrario, una funzione di rango basata anchesui riferimenti tra i documenti risulta di difficile manipolazione da parte di un singoloproduttore, e rende quindi il rango stesso più resistente a questo tipo di search spam.

Nel processo di votazione appena descritto (basato esclusivamente sul grado di in-gresso dei nodi), dobbiamo però tenere presente anche un altro aspetto, cioè che unriferimento proveniente da un documento che ne ha molti in uscita deve avere una rile-vanza minore rispetto a quello di un documento che ne ha pochi, perché presupponiamoche nel secondo sia stata effettuata una scelta più accurata dei documenti da riferire.

Inoltre, dobbiamo tenere conto anche della significatività dei documenti, in quantoun riferimento proveniente da un documento "autorevole" è più influente rispetto aquello proveniente da uno meno rilevante.

Page 254: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 254/373

 

Attualmente, i più importanti motori di ricerca sul Web utilizzano una propria fun-zione di rango basata sull'analisi dei collegamenti (link analysis) e, in linea di principio,operano secondo il seguente schema comune:

1. impiegano specifici programmi, detti crawler o spider, che visitano e raccolgono lepagine del web seguendo i link che le collegano;

2. analizzano il testo in ogni pagina raccolta e costruiscono un indice di ricerca deitermini che compaiono nelle pagine stesse;

3. tengono traccia dei link tra le pagine, costruendo quindi la matrice di adiacenzadel grafo del Web (o meglio, della sua porzione visitata);

4. calcolano, a partire dalla matrice di adiacenza, il rango delle pagine raccolte (o diun loro sottoinsieme) secondo modalità che possono variare da motore a motore.

Nel resto di questo paragrafo, discutiamo due approcci che hanno ispirato gli attualimetodi di realizzazione della funzione di rango: osserviamo sin d'ora che tali approcci

non sono tra di loro alternativi, ma sono piut tosto complementari. Il primo approccio,detto PageRank  e utilizzato da Google insieme ad altri metodi, calcola (e ricalcola perio-dicamente) il rango di tutte le pagine raccolte: le pagine restituite in seguito a un'in-terrogazione vengono poi mostrate all'utente in ordine decrescente di rango. Il secondoapproccio, detto HITS (  Hypertext Induced Topic Selection o selezione degli argomentiindotta dagli ipertesti) e utilizzato inizialmente da Clever dell'IBM, ha in parte ispiratosuccessivi motori di ricerca come Ask, il quale usa ExpertRank: l'approccio HITS iden-tifica prima un opportuno insieme D di pagine che, in qualche modo, sono collegateall'interrogazione e di queste solamente ne calcola il rango che viene utilizzato per deci-dere l'ordine con cui mostrare le pagine in D. Osserviamo che la collezione D (dominiodella funzione rango) è costituita da tutte le pagine raccolte dai crawler nel caso di Pa-geRank, mentre tale collezione è formata dalle sole pagine collegate all'interrogazionenel caso di HITS: in quest'ultimo caso, la stessa pagina può avere valori di rango diversia seconda dell'interrogazione. Da quanto appena esposto, è chiaro che i due approccipossono essere combinati utilizzando HITS per raffinare ulteriormente i risultati forni-ti da PageRank: osserviamo inoltre che i due approcci possono essere usati in generaleper calcolare il rango dei nodi di un qualunque grafo orientato o di un suo sottografoindotto, utilizzando i loro archi come meccanismo di votazione.

6.4.1 Significatività delle pagine con PageRank

Come osservato in precedenza, una funzione di rango basata sul sistema di votazione eda applicare al grafo del Web rispetta due principi fondamentali: il rango di una pagi-na è direttamente proporzionale al rango delle pagine che la riferiscono e inversamenteproporzionale al grado in uscita di tali pagine. Il sistema di votazione prevede che ognipagina esprima almeno una preferenza, ovvero esso interpreta l'astensione di una pagi-na come una distribuzione uniforme di preferenze a tutte le pagine del Web (per quella

Page 255: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 255/373

 

pagina tutte le pagine del Web sono ugualmente importanti ). In termini della matricedi adiacenza del Web, ciò vuol dire che le righe con tutti 0 (corrispondenti alle paginesenza link in uscita) diventano righe con tutti 1.

A queste osservazioni, dobbiamo aggiungere quella che il tipico navigatore (surfer)

non sempre si muove all'interno del Web seguendo un link, ma talvolta utilizza la barradei comandi di un browser per spostarsi direttamente su una pagina non necessariamentecollegata a quella attuale: supponiamo che questo avvenga con probabilità 1 — a con0 < a < l. 5 Per ogni pagina i, indichiamo nel seguito con E(i) l'insieme delle pagineche riferiscono i e con us c i t a ( i ) il grado in uscita di i. Il PageRank modella le suddetteosservazioni definendo matematicamente la seguente funzione rango per ogni pagina j:

Tale formula intuitivamente descrive il fatto che con probabilità oc la pagina j vieneraggiunta seguendo un link da una delle pagine che la riferiscono, mentre con probabilità1 — a essa viene raggiunta digitando direttamente il suo indirizzo nella barra dei comandi(quando PageRank è stato introdotto, a = 0,85).

Sia A la matrice di adiacenza del grafo del Web, ovvero A[i][j] = 1 se e solo se esisteun link dalla pagina i alla pagina j per 0 ^ i, j ^ n — 1, dove n indica il numero di pagine(notiamo che, da quanto osservato in precedenza, per ogni i deve esistere almeno un va-lore di j tale A[i][j] = 1): quindi , i € E(j) se e solo se A[i][j] = 1. Il calcolo della funzione

rango secondo l'equazione (6.6) è esplicitato nel Codice 6.6, che per prima cosa calcolail grado in uscita di ogni nodo e inizializza un array mono-dimensionale di n elementi,detto vettore di rango, per il quale al termine dell'esecuzione vale X[j] = rango(j) per0 ^ j ^ n — 1 (righe 3—8). Il ciclo successivo (righe 9-18) calcola il rango delle pagineusando l'equazione (6.6): poiché tale equazione è ricorsiva, non è sufficiente una suasola applicazione, ma il calcolo deve essere effettuato in maniera iterativa, in modo che1 valori calcolati a un passo divengano i valori in ingresso del passo successivo (righe 10e l i ) , fino a raggiungere il risultato finale (riga 18). In particolare, le righe 12-17 ese-guono un passo di tale calcolo iterativo: notiamo che, poiché A[i][)] = 1 se i € E(j) eA[i][j] = 0 altrimenti, il ciclo f o r più interno (righe 14—15) corrisponde al calcolo delvalore X!igE(j) usTit°a(V) P r e s e n t e nell'equazione (6.6). Quando il ciclo do-while ter-mina (ovvero, il nuovo valore coincide con quello calcolato al passo precedente), il valoredi X che viene restituito alla riga 19 soddisfa l'equazione (6.6): pertanto, il Codice 6.6,se termina, calcola il valore di PageRank.

5In realtà, non stiamo considerando in questo contesto la possibilità che il navigatore utilizzi i pulsantidi navigazione generalmente disponibili nei browser.

ran go (j ) = (1 - a ) + a Y_i e E ( j )

rango( t(6.6)

u s c i t a ( i )

Page 256: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 256/373

 

PageRank( A ):

(pre: A è una matrice binaria n x n, tale che Jo' A[i] [j] > 0 per 0 ^ i < n)

FOR  (j = 0; j < n; j = j+1) {

uscita[j] = 0;

FOR  (i = 0; i < n; i = i+1)

uscita[j] = uscita[j] + A[j][i];X[j] = 1;

>DO {

FOR  (j = 0; j < N; j = j+1)

Y[j] = X[j] ;

FOR  (j = 0; j < n; j = j+1) {

X[j] = 0;

FOR  (i = 0; i < n; i = i+1)

X[j] = X[j] + A[i] [j] x Y[i] / uscitati] ;

X[j] = (1 -a ) + a x X[j] ;}

> WHILE (X != Y);

RETURN X;

Codice 6.6 Algoritmo per il calcolo di PageRank.

A L V I E : calcolo di PageRank

Osserva, sperimenta e verifica

PageRank

Per quanto riguarda la complessità del codice, ogni iterazione del ciclo do-whilerichiede 0(n 2 ) tempo: quindi, la complessità temporale è 0(bn 2 ) dove b indica il nu-mero di iterazioni che sono eseguite. In linea di principio, b potre bbe essere un valore

infinito, in quanto i valori X e Y calcolati a ogni iterazione potrebbero oscillare senza maiconvergere allo stesso valore: inoltre, la convergenza potrebbe dipendere dai valori concui inizializziamo X (riga 7). Vedremo nel Paragrafo 6.4.3 che ciò non può accadere,facendo uso di strumenti di algebra lineare.

Una naturale interpretazione del Codice 6.6 consiste nell'immaginare ogni paginafornita inizialmente di un "credito" di significatività di default (pari a 1 nel codice):successivamente, ogni pagina decide di distribuire in parti uguali il suo credito alle pagineda essa riferite, ricevendo in cambio un contributo da ciascuna pagina che la riferisce.

Page 257: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 257/373

 

Figura 6 .19 Effetti dell'apertura verso l'esterno sul valore di PageRank.

Anche se da un Iato aprire all'esterno una pagina, includendo in essa dei link in uscita,causa una perdita di rango, dall'altro una tale apertura può incrementare il numero diriferimenti alla pagina stessa e, quindi, aumentare il flusso di rango in entrata che, inlinea di principio, potrebbe compensare e superare quello in uscita.

Consideriamo l'esempio mostrato nella parte sinistra della Figura 6.19, in cui il sitoWeb formato dalle due pagine 0 e 1 è isolato rispetto al resto del mondo (in questocaso rappresentato dalle tre pagine 2, 3 e 4): notiamo che in questa situazione il rangototale del sito formato dalle due pagine (che è pari al numero delle pagine stesse), vieneequamente distribuito tra di esse, in quanto le due pagine si riferiscono vicendevolmente.

Volendo aprire tale sito all'esterno, possiamo decidere di farlo in diversi modi. Adesempio, possiamo decidere di collegare la pagina 0 alla pagina 2 e viceversa, come mo-strato nella parte centrale della figura (ovviamente, un'apertura verso l'esterno deve esserecontraccambiata da un riferimento in entrata): in tal caso, il sito globalmente perde delrango, passando da 2 a poco più di 1. Se, però, decidiamo di collegare la pagina 0 allapagina 3 e viceversa, come mostrato nella parte destra della figura, la situazione cam-bia drasticamente: la pagina 0 arriva a un rango superiore a 2 e anche la 1 aumentaleggermente il suo rango, così che il rango totale passa da 2 a 3, 3. In altre parole, ge-stendo l'apertura verso l'esterno in modo opportuno, il rango delle proprie pagine può

migliorare significativamente.Un'altra interessante osservazione consiste nel fatto che aprire un sito verso l'esterno

può essere accompagnato da una ristrutturazione del sito stesso, in modo da diminuirela quantità di rango che viene perso a causa dell'apertura.

Consideriamo la situazione mostrata nella parte sinistra della Figura 6.20, in cui lapagina 3 potrebbe essere una tipica pagina contenente un elenco di riferimenti a pagineesterne al sito formato dalle pagine 0, 1, 2 e 3. Come possiamo vedere, il rango iniziale(pari a 4) del sito si riduce significativamente a causa della pagina 3: il rango totale, dopo

Page 258: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 258/373

 

0,42 0,8

Figura 6 .20 Effetti della ristrutturazione di un sito web sul valore di PageRank.

l'apertura verso l'esterno, è divenuto pari a circa 2 ,213 con una perdita del 45%. Inquesto caso, possiamo progettare una ristrutturazione del sito, aggiungendo delle paginedi recensione delle pagine esterne a cui la pagina 3 fa riferimento e che a loro volta fannoriferimento alla pagina 0 (che funge da home page del sito), come mostrato nella partedestra della figura: cosi facendo il rango che fuoriesce dal sito si riduce significativamentee il rango totale passa da un potenziale di 7 a un valore attuale pari a circa 5,485, conuna perdita del 22%.

Queste considerazioni mostrano che diverse strategie in grado di manipolare il valoredi PageRank sono state progettate allo scopo (generalmente concertato) di migliorare laposizione delle proprie pagine nell'ordine di apparizione e, quindi, di "monetizzare" lacreazione di riferimenti.

Queste strategie hanno avuto un certo impatto sull'affidabilità del concetto di rangodi una pagina Web. Sappiamo che il motore di ricerca Google penalizza esplicitamente lecosiddettefabbriche di link  e altre manipolazioni progettate per aumentare artificialmenteil rango di una pagina: non conosciamo, però, i meccanismi con cui Google riconoscetali fabbriche e tali manipolazioni.

Un altro (non indifferente) svantaggio di PageRank è che esso tende a favorire paginepiù vecchie: in effetti, una pagina nuova, anche se molto interessante, non avrà all'iniziomolti riferimenti in entrata, a meno che non sia parte di un sito già esistente e con uninsieme di pagine fortemente connesse tra di loro.

Per tutti questi motivi, Google non usa solo PageRank per calcolare il rango: sivocifera che usi oltre 100 fattori moltiplicativi, tra i quali PageRank è uno dei tanti (eforse nemmeno troppo importante). Google suggerisce, quasi banalmente, che il migliormodo per acquisire un alto rango è quello di creare pagine con contenuti di qualità.

Page 259: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 259/373

 

6.4.2 Significatività delle pagine con HITS

La funzione di rango realizzata da HITS è motivata dall'osservazione che le pagine si-gnificative per certi termini di ricerca non sempre contengono quei termini stessi: peresempio, né la pagina principale di www. f e r r a r i . i t né quella di www . f i a t . i t con-

tengono esplicitamente il termine "automobile". Inoltre tali pagine non si riferisconovicendevolmente in modo diretto, mentre lo fanno indirettamente attraverso qualche lo-ro pagina secondaria oppure attraverso siti specializzati per gli appassionati di automobili.Nella terminologia di HITS, le pagine w w w . f e r r a r i . i t e w w w . f i a t . i t sono delleautorità (authority) nel campo automobilistico (mentre non lo sono in quello enologicoo di letteratura latina), e un sito specializzato che contiene dei riferimenti a entrambeviene denominato concentratore di collegamenti (hub). Il fenomeno osservato è quindiquello del mutuo rafforzamento in termini di significatività, secondo i seguenti principi:

• un concentratore è tanto più significativo quanto lo sono le autorità a cui si ri-ferisce e, quindi, il peso del primo è direttamente proporzionale al peso delleultime;

• un'autorità è tanto più significativa quanto lo sono i concentratori che la riferi-scono e, quindi, il peso della prima è direttamente proporzionale al peso degliultimi.

HITS classifica implicitamente le pagine in base ai principi sopra esposti: ogni pagina èsimultaneamente un'autorità e un concentratore, chiaramente con peso molto variabile.A tal fine, le seguenti due funzioni di rango sono utilizzate per una data collezione D didocumenti:

• ran go A( j) misura il peso in D della pagina j intesa come autori tà;

• ran go C(j) misura il peso in D della pagina j intesa come concentratore.

Da quanto osservato sopra, possiamo derivare le equazioni ricorsive che definisconole suddette funzioni per la pagina j nella collezione D, indicando con E(j) C D l'insie-me delle pagine che riferiscono j e con U(j) C D quelle riferite da j, all'interno dellacollezione stessa:

rang oA(j ) = ^ ran goC( i) (6.7)iGE(j)

ran goC (j ) = rangoA( k) (6.8)k€U( j )

Analogamente al PageRank, possiamo effettuare il calcolo di tali funzioni median-te un procedimento iterativo, assegnando un valore iniziale pari a ^ a ciascuna pagi-na j e normalizzando di volta in volta il valore di rango, per mantenere l'invariante che£  j € D rangoA(j) = £  j 6 D rangoC(j) = 1.

Page 260: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 260/373

 

HITS ( A ) : {pre: A è La matrice di adiacenza di un grafoG con n nodi)FOR  (j = 0; j < n; j = j + 1)

X[j] = W[j] = 1/n;

DO {

sommaX = sommaW = 0;

FOR  (j = 0; j < N; j = j+1)

{ Y[j] = X[j] ; Z[j] = W[j] ; >

FOR  (j = 0; j < n; j = j+1) {

X[j] = W[j] = 0;

FOR  (i = 0; i < n; i = i+1)

X[j] = X[j] + A[i] [j] x Z[i] ;

FOR  (k = 0; K < N; k = k+1)

W[j] = W[j] + A[j] [k] x Y[k] ;

sommaX = sommaX + X[j];

sommaW = sommaW + W[j];

}FOR  (j = 0; j < n; j = j+1)

{ X[j] = X[j] / sommaX; W[j] = W[j] / sommaW; >

> WHILE ((X != Y) Il (W != Z));

RETURN <X, W>;

Codice 6.7 Algoritmo per il calcolo di HITS.

Per semplificare la notazione, il Codice 6.7 suppone che il sottoinsieme delle pagine

del grafo del Web che compongono la collezione D siano state nuovamente numerate

da 0 a n — 1 e che A sia la matrice di adiacenza del sottografo del Web indotto dalle

pagine in D: quind i, i € E(j) se e solo se A[i][j] = 1 e k G U( j) se e solo se A[j][k] = 1.

Il Codice 6.7 inizializza due vettori di rango di n elementi (righe 2 e 3) tali che, altermine dell'esecuzione, vale X[j] = rangoA(j) e W[j] = rangoC(j) per 0 ^ j ^ n — 1. Ilciclo successivo (righe 4-19) calcola le due funzioni di rango usando le equazioni (6.7)e (6.8): poiché tali equazioni sono ricorsive, i valori calcolati a un passo diventano i

valori in ingresso del passo successivo (righe 6 e 7), fino a raggiungere il risu ltato finale(riga 19).

In particolare, le righe 8- 18 eseguono un passo di tale calcolo iterativo: not ia-mo che il pr imo ciclo f o r interno (righe 10 e 11) corr isponde al calcolo del valoreZi 6 E( j ) ran goC(i ) (poiché A[i][j] = 1 se i e E(j) e A[i][j] = 0 altrimenti) e che il se-condo ciclo f o r (righe 12 e 13) corr isponde al calcolo di ^L k € U (  j) rangoA(k) (poichéA[j][k] = 1 se k e E(j) e A[j][k] = 0 alt rimenti) .

Le rimanenti righe del ciclo do-while calcolano i valori sommaX e sommaW necessaria normalizzare i valori così calcolati (righe 17 e 18): quan do il ciclo d o - w h i l e termina

Page 261: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 261/373

 

(ovvero, i nuovi valori coincidono con quelli calcolati al passo precedente), i valori diX e W che vengono restituiti alla riga 20 soddisfano necessariamente l'equazioni (6.7)e (6.8). Pertanto, se termina, il Codice 6.7 calcola correttamente le funzioni di rangodefinite per HITS.

A L V I E : calcolo di HITS

Osserva, sperimenta e verifica

HITS

Analogamente a PageRank, ogni iterazione del ciclo do-while richiede 0(n 2 ) tem-po: quindi, la complessità temporale è 0(bn 2 ) dove b indica il numero di iterazioni che

sono eseguite (vedremo nel Paragrafo 6.4.3 che b è un valore finito, ovvero il Codice 6.7termina).

A questo punto, è interessante discutere come viene costruita la collezione D didocumenti su cui applicare HITS, mostrando anche come esso può integrarsi con altrisistemi di rango.

Ipotizziamo di eseguire un'interrogazione utilizzando un motore di ricerca con lapropria funzione di rango (come, ad esempio, PageRank), ottenendo così un elenco dirisultati: prendiamo i primi t in ordine di rango formando un insieme S di partenza(t = 200 nella proposta originale di HITS). Per ottenere D, estendiamo S con le pagine

che appartengono al vicinato di quelle in S: HI TS aggiunge a S tut te le pagine in U(j ) per  j G S e un opportuno sottoinsieme delle pagine in E(j) per j S S (quest'ultimo potrebbeessere molto vasto). Altre scelte sono possibili: per esempio, possiamo aggiungere anchele pagine con i riferimenti in uscita di secondo livello, ovvero U(k) per k e U(j) e j € S.In tal modo, l'insieme S delle prime t pagine fornite da un motore di ricerca è esteso eraffinato con l'algoritmo di HITS.

Il metodo HITS, come quello PageRank, presenta alcuni svantaggi che in qualchemodo limitano la sua affidabilità come implementazione del concetto di rango di unapagina Web. Anzitutto, poiché il valore di HITS dipende dall'interrogazione, la costru-

zione dell'insieme D e il calcolo descritto nel Codice 6.7 devono essere eseguiti ognivolta al momento dell'interrogazione (con ovvie conseguenze dal punto di vista delleprestazióni).

Inoltre, HITS è soggetto a meccanismi di search spam al pari di PageRank. Dalpunto di vista del produttore di una pagina Web, è infatti facile aggiungere link in uscitae quindi aumentare in tal modo il punteggio di concentratore della pagina stessa: poichéi due punteggi di HITS sono tra di loro dipendenti, questo può portare a un aumentodel punteggio di autorità della pagina. Inoltre, cambiamenti locali alla struttura dei

Page 262: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 262/373

 

collegamenti possono risultare in punteggi drasticamente diversi, in quanto la collezioneD è piccola in confronto all'intero Web.

Infine, HITS presenta un cosiddetto problema di "deriva del soggetto" (topic drift),in quanto è possibile che costruendo l'insieme D includiamo una pagina non precisamen-

te focalizzata sull'argomento dell'interrogazione ma con un alto punteggio di autorità: ilrischio è che questa pagina e quelle a essa vicine possano dominare la lista ordinata cheviene restituita all'utente, spostando così l'attenzione su documenti non proprio inerentiall'interrogazione.

6.4.3 Convergenza del calcolo iterativo di PageRank e HITS

Per dimostrare la convergenza del Codice 6.6 facciamo uso di un famoso risultato dialgebra lineare, detto teorema di Perron-Frobenius. Ciò è dovuto al fatto che, comemostriamo in questo paragrafo, possiamo interpretare il codice come un procedimento

iterativo per il calcolo della soluzione di un sistema di equazioni lineari: il teorema diPerron-Frobenius ci consente di dimostrare che tale soluzione esiste ed è unica e cheviene calcolata da tale procedimento iterativo.

Indichiamo con X , k ' il valore del vettore di rango X al termine della k-esima itera-zione del ciclo do-wh i le del Codice 6.6, per k ^ 1, e con X(0 ' il suo valore iniziale(ovvero, formato da tutti 1). In base alle istruzioni eseguite dal Codice 6.6 all'interno delciclo, possiamo riformulare l'equazione (6.6) come

X | k | [j] = (1 - a) + a V ( A [ Ì ] p ] n X < k -"[i]) (6.9)V u s c i t a t Ji=0 v x

per 0 ^ j < n e k ) 1. Osserviamo che, per ogni k ^ 0, ^"JQ 1 X(k) [j] = n. Ciò èvero per k = 0. Supponendo che lo sia per ogni h. < k e appl icando l'equazione (6.9),abbiamo che

"tV'ra - +  j =0 j=0 i=0v '

- < 1 - « ) " " Z : ^ - r a E s Si=0 j=0

n— 1= ( 1 - a)n + <x ^ X , k - 1 )[i] = ( 1 — a)n + an = n

i=0

dove la terza uguaglianza è dovuta al fatto che u s c i t a t i ] = ^.^Jo A[i][)] per ogni 0 ^

i < n, mentre la quarta uguaglianza segue dall'ipotesi induttiva che ^{^Q ' X ( k _ 1 ) [i] = TI.

Page 263: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 263/373

 

Mostriamo ora che l'insieme delle equazioni (6.9), una per ogni valore di j, costitui-sce un sistema di equazioni lineari, che può essere espresso mediante moltiplicazione dimatrici. A tal fine, definiamo la matrice M di dimensione n x n, tale che

imr-ir-i 1 _ a ,M[)][t] = + ot-r i u sc i t a t i ]

per 0 ^ j,i < n. Possiamo verificare che

X , k ' = M x X ' k ~ "

in quanto X (k 'tj] = n P e r ° g n ' k ^ 0. Quindi, X (k ) = M k X ( 0 ) dove ricordiamo

che M k è il risultato della moltiplicazione di M per se stessa k volte: pertanto, il Codi-ce 6.6 converge se esiste un k tale che M k = M k _ 1 . Il teorema di Perron-Frobenius ci

permette di affermare che ciò è asintoticamente vero in quanto gli elementi di M sonotutt i positivi: per ogni e > 0, esiste k e ^ 1 tale che |Mk[j][i] — Mk-1[j][t]| < e per0 ^ i, j < n . Per questo motivo, la condizione di terminazione del Codice 6.6 deveessere espressa in funzione di un grado di precisione e che deve essere fornito in ingressoinsieme alla matrice A. Inoltre, lo stesso teorema garantisce che la soluzione calcolatanon dipende dalla scelta di X' 0 ' .

Il teorema di Perron-Frobenius potrebbe essere applicato anche al Codice 6.7 modi-ficando in modo opportuno la matrice A in ingresso. Tuttavia, altri risultati di algebralineare consentono di affermare che il Codice 6.7 converge sempre (specificando la con-

dizione di terminazione in base a un grado di precisione), anche se la soluzione calcolatapuò dipendere dai valori assegnati inizialmente a X e W.

RIEPILOGO

  In questo capitolo abbiamo esaminato le caratteristiche principali dei grafi, fornendo lerelative definizioni e mostrando come utilizzarli per modellare una quantità di situazionie problemi reali. Abbiamo mostrato come rappresentare i grafi e come risolvere, medianteil paradigma dell'algoritmo goloso, il problema della colorazione e del massimo insiemeindipendente nel caso di grafi a intervalli. Abbiamo quindi discusso le reti complesse e come

generarle probabilisticamente e abbiamo, infine, studiato il problema della classificazionedei documenti nei motori di ricerca utilizzando la struttura a grafo del Web.

ESERCIZI

1. Discutete gli algoritmi per inserire o cancellare un nodo o un arco in un grafoin relazione alle due rappresentazioni dei grafi discusse (rappresentazione di grafidinamici).

Page 264: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 264/373

 

2. Il grafo a torneo è un grafo orientato G in cui per ogni coppia di vertici x e y esisteun solo arco che li collega, (x,y) oppure (y ,x), ma non entrambi. L'interpretazio-ne è che nella partita del torneo tra x e y uno dei due ha vinto. Mostrate che ungrafo a torneo ammette sempre un cammino Hamiltoniano.

3. Descrivete un algoritmo lineare per stabilire se un grafo è bipar tito , tentando diusare il colore opposto del vertice corrente quando scoprite un nuovo vertice.

4. Mostrate che le seguenti due varianti del paradigma dell'algoritmo goloso noncalcolano un insieme indipendente di cardinalità massima in un grafo a intervalli:

(a) una volta ordinati gli intervalli in ordine non decrescente rispetto alla lorolunghezza, l'algoritmo esamina gli intervalli uno dopo l'altro e li include nellasoluzione se ciò è possibile;

(b) l'algoritmo seleziona l'intervallo con il minor numero di intersezioni, includetale intervallo nella soluzione, elimina tutti gli intervalli che lo intersecano e, se visono ancora intervalli, ripete tale procedimento.

5. Mostrate come ridurre la complessità del Codice 6.3 per la costruzione di un grafoalla Erdòs-Rény, da 0(n 2 ) tempo a O(np) tempo, quindi con complessità lineare.

6. Mostrate che X | k ) = M x X ( k _ 1 ) , dove X ( h ) per h. ^ 0 e M sono definite nel

Paragrafo 6.4.3.

Page 265: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 265/373

 

Capitolo 7

Pile e code

SOMMARIO  In questo capitolo, definiamo e analizziamo due strutture di dati comunemente utilizzatein contesti informatici (e non solo) per la gestione di sequenze lineari dinamiche, ovvero le

 pile e lecode. Per ciascuna di esse, descriviamo due diversi possibili modi di implementarle

e forniamo poi alcuni esempi significativi di applicazione nella gestione della notazione  polacca poslfissa, nella visita di grafi, nel loro ordinamento topologico e nel calcolo dellecomponenti connesse.

DIFFICOLTÀ

1 CFU

7.1 Pile

Una pila è una collezione di elementi in cui le operazioni disponibili, come l'estrazionedi un elemento, sono ristrette unicamente a quello più recentemente inserito. Questapolitica di accesso, detta LIFO (  Last In First Out), comporta che l'ordine con cui glielementi sono estratti dalla pila è opposto rispetto all'ordine dei relativi inserimenti e,ad esempio, riflette quanto avviene per la pila di vassoi, in cui il vassoio che possiamoprendere è sempre quello in cima alla pila che è anche l'ultimo a essere stato riposto.

L'insieme delle operazioni caratteristiche di una pila è composto da tre operazioni,due delle quali inseriscono ed estraggono rispettivamente l'elemento in cima alla pila,

mentre la terza restituisce tale elemento senza estrarlo.In particolare, la prima operazione prende il nome di Push e inserisce un nuovoelemento in cima alla pila; la seconda è detta Pop ed estrae l'elemento in cima alla pilarestituendo l'informazione in esso contenuta; la terza è detta Top e restituisce l'informa-zione contenuta nell 'elemento in cima alla pila senza estrarlo. In alcune applicazioni èutile avere anche l'operazione Empty che verifica se la pila è vuota o meno.

Page 266: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 266/373

 

Come vedremo, ogni operazione invocata su di una pila può essere eseguita in tempocostante, indipendentemente dal numero di elementi contenuti nella pila stessa: che ciòsia possibile può essere verificato immediatamente considerando il caso della pila di vas-soi, in quanto riporre o prendere un vassoio richiede lo stesso tempo, indipendentementeda quanti siano i vassoi sovrapposti.

In effetti, se gli elementi nella pila sono mantenuti ordinati secondo l'istante di in-serimento, tutte e tre le operazioni agiscono su un'estremità (la cima della pila) dellasequenza. Basta quindi avere la possibilità di accedere direttamente a tale estremità pereffet tuare le operazioni in tempo ind ipendente dalla dimensione della pila: in questoparagrafo proponiamo due specifiche implementazioni della struttura di dati pila, checonsentono effettivamente di fare ciò.

7.1.1 Implementazione di una pila mediante un array

Una pila può essere implementata utilizzando un array. In particolare, gli elementi dellapila sono memorizzati in un array di dimensione iniziale pari a una costante predefinita.

Successivamente, la dimensione dell'array viene raddoppiata o dimezzata per garan-tire che sia proporzionale al numero di elementi effettivamente contenuti nella pila: l'a-nalisi del metodo di r idimensionamento di un array (discussa nel Paragrafo 2.1.3) mostrache occorre un tempo costante ammortizzato per operazione.

Gli elementi della pila sono memorizzati in sequenza nell'array a partire dalla loca-zione iniziale, inserendoli man mano nella prima locazione disponibile: ciò comporta

che la "cima" della pila corrisponde all'ultimo elemento di tale sequenza. Basterà quinditenere traccia dell'indice della locazione che contiene l'ultimo elemento della sequen-za per implementare le operazioni Push, Pop, Top e Empty in modo che richiedanotempo costante ammortizzato, come mostrato nel Codice 7.1, in cui ipotizziamo che lapila sia rappresentata per mezzo di un array p i l a A r r a y di dimensione variabile (gestitomediante le funzioni Ve r if ic aR ad do pp i o e Ve ri f icaDimezzam ento).

La cima della pila corrisponde all'elemento dell'array il cui indice è memorizzato nel-la variabile cimaPila, inizialmente posta uguale a — 1. Facendo uso di tale informazionele operazioni di accesso alla pila sono molto semplici da realizzare. Infatti, l'elemento in

cima alla pila sarà sempre p i l a A r r a y [ c i m a P i l a ] .L'operazione Push incrementa cimaPila, dopo avere verificato che l'array non sia

pieno (nel qual caso la sua dimensione andrà raddoppiata). L'operazione Pop richiede diverificare se la pila non è vuota invocando la funzione Empty e, in tal caso, di decremen-tare il valore di cimaPila, verificando che l'array non sia poco popolato (nel qual casola sua dimensione andrà dimezzata).

Osserviamo come, nel caso di un'operazione Pop, il contenuto dell'elemento dell'ar-ray che si trova nella posizione specificata da c i m a P i l a , non debba essere necessaria-

Page 267: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 267/373

 

Push( x ):Verif icaRaddoppio( ) ;cimaPila = cimaPila + 1;pilaArray[ c imaPila ] = x;

Pop( ):IF (!Empty( )) {x = pilaA rrayE cima Pil a ] ;cim aPi la = cim aPi la - 1;VerificaDimezzamento( );RETURN X;

>

Top( ):IF (!Empty( )) RETURN pi laArray t c imaPi la ] ;

Empty( ):RETURN ( c i m a P i l a = = -1) ;

Codice 7.1 Implementazione di una pila mediante un array: le funzioni VerificaRaddoppio eVerificaDimezzamento seguono l'approccio del Paragrafo 2.1.3.

mente azzerato, in quanto nel momento in cui faremo di nuovo accesso a tale elemento,il suo contenuto sarà stato modificato dalla corrispondente operazione Push.

A L V I E : implementazione di una pila mediante un array

i n»ì •rr.TTn-rrnMOsserva, sperimenta e verifica

StackArray

7.1.2 Implementazione di una pila mediante una lista

Una pila può essere implementata anche utilizzando una lista i cui elementi sono man-tenuti ordinati in base al loro tempo di inserimento decrescente. In tal modo, la "cima"della pila corrisponde all'inizio della lista, e le operazioni agiscono tutte sull'elementoiniziale della lista stessa. Nel Codice 7.2, il riferimento all'elemento in cima alla pilaè memorizzato nella variabile cimaPila e ciascun elemento contiene oltre all'informa-zione un riferimento all'elemento successivo (c i m a P i l a è n u l i nel caso in cui la pila

Page 268: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 268/373

 

Push( x ):u = NuovoNodo( ) ;u.dato = x;u.succ = cimaPila ;cimaPila = u;

Pop( ):IF (! Empty( ) ) {

x = cima Pil a .da to ;c imaPila = c imaPila .succ;RETURN X;

>

Top( ):IF (!Empty( )) RETURN c imaPi la .da to ;

Empty( ):RETURN (c imaPila == nuli) ;

Codice 7.2 Implementazione della pila mediante una lista.

sia vuota). Facendo uso di tali riferimenti, le operazioni di accesso alla pila sono quindialtret tanto semplici da realizzare di quelle esaminate nel paragrafo precedente. La mo-dalità di allocazione di un nodo nella lista (riga 2 in Push) dipende dal linguaggio di

programmazione adottato.

A L V I E : implementazione di una pila mediante una lista

J h f e ^ Osserva, sperimenta e verifica^UK/ StackList

7.2 Opus libri: Postscript e notazione postfissa

Il Postscript è un linguaggio di programmazione per la grafica che viene eseguito da uninterprete che utilizza la pila e la notazione postfissa o polacca inversa definita di seguito.Se un'operazione in Postscript ha k argomenti, questi ultimi si trovano nelle k posizioniin cima alla pila, ovvero l'esecuzione di k operazioni Pop fornisce gli argomenti all'ope-razione in Postscript, il cui risultato viene posto sulla pila tramite un'operazione Push.

Page 269: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 269/373

 

L'ampia diffusione del linguaggio Postscript lo rende uno degli standard tipografici prin-cipalmente adottati, insieme alla sua evoluzione PDF (Portable Document Format), perla stampa professionale (inclusa quella del presente libro). Il principio del suo funziona-mento basato sulla pila è intuitivo e possiamo illustrarlo usando le espressioni aritmetichecome esempio.

È uso comune in matematica scrivere l'operatore tra gli operandi, come in A + B,piuttosto che dopo gli operandi, come in AB+ (nel seguito, supponiamo che gli operatorisiano tutt i binari). La prima forma si chiama notazione infissa mentre la seconda sichiama postfissa o polacca inversa dalla nazionalità del matematico Lukasiewicz che nestudiò le proprietà.

La notazione postfissa ha alcuni vantaggi rispetto a quella infissa. Anzitutto, le espres-sioni scritte in notazione postfissa non hanno bisogno di parentesi (l'ordine degli ope-randi viene preservato rispetto all'infissa). In secondo luogo, non è necessario specificare

una priorità, talvolta arbitraria, degli operatori (ad esempio, il fatto che A + B x C siaequivalente a A + (B x C) è dovuto al fatto che la moltiplicazione ha, in base a unadefinizione arbitraria, priorità superiore alla somma).

Infine, tali espressioni si prestano a essere valutate semplicemente, da sinistra a de-stra, mediante l'uso di una pila applicando le seguenti regole in base al simbolo letto,supponendo di avere operazioni binarie:

• operando: viene eseguita la Push di tale operando;

• operatore: vengono eseguite due Pop, l'operatore viene applicato ai due operandiprelevati dalla pila (nel giusto ordine) e viene eseguita la Push del risultato.

A L V I E : valutazione di un'espressione postfissa mediante una pila

Osserva, sperimenta e verifica

P o s t f i x E v a l u a t i o n . _

Oltre che per la valutazione di espressioni algebriche in notazione polacca inversa, il

tipo di dati pila può essere usato anche per convertire un'espressione algebrica in formainfìssa in un'espressione algebrica equivalente in forma postfissa. Concettualmente, pos-siamo costruire l'albero che rappresenta l'espressione infissa, visitandolo in ordine posti-cipato per ottenere l'espressione postfissa, ma possiamo evitare questo doppio passaggiousando direttamente una pila.

Supponiamo per semplicità che le parentesi siano comunque esplicitate, per cui un'e-spressione è data da una coppia di parentesi al cui interno ci sono due espressioni (ricor-sivamente definite) separate da un operatore. A questo punto la trasformazione avviene

Page 270: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 270/373

 

simbolo corrente dell'espressione infissa

cima della pila $ + oppure — x oppure /  ( )$ 4 1 1 1

4- oppure — 2 2 1 1 2x oppure /  2 2 2 1 2

( 1 1 1 3

Figura 7.1 Tabella per la conversione di un'espressione infissa in una postfissa.

leggendo l'espressione infissa da sinistra a destra e applicando le seguenti regole in base

al simbolo letto:

• parentesi aperta: viene ignorata;

• operando: viene appeso dirett amente in fond o all'espressione postfissa in costru-zione senza passare per la pila;

• opera tore: viene eseguita la Pu s h di tale operatore ;

• parentesi chiusa: viene eseguita la Po p per riprendere l'opera tore che viene appeso

in fondo all'espressione postfissa in costruzione.

A L V I E : conversione di un'espressione infissa con parentesi esplicite in una postfissa

Osserva, sperimenta e verificaF u l l l n f i x P o s t f i x

In realtà, non abbiamo bisogno di imporre le parentesi nell'espressione infissa quan-do no n sono necessarie. Per verificare tale affermazione, ipotizziamo che l'espressioneinfissa sia composta dei seguenti simboli: variabili e costanti (ovvero, lettere alfanumeri-che), operatori binari (ovvero, +, —, x, /) e parentesi (ovvero, ( e ) ). Supponiamo inoltreche l'espressione infissa sia sintatticamente corretta (ad esempio, non sia A + xB) e sia

terminata dal simbolo speciale $. L'esecuzione delle azioni inizia ponendo una copia delsimbolo $ nella pila vuota, e ha termine quando l'espressione infissa diviene vuota.

Durante la conversione, se il simbolo corrente nell'espressione infissa è un operando(una variabile o una costante), esso viene appeso direttamente in fondo all'espressionepostfissa in costruzione senza passare per la pila. Alt riment i, il simbolo corrente è unoperatore, una parentesi oppure il simbolo $, e le regole per elaborare tale simbolo so-no rappresentate succintamente nella tabella in Figura 7.1, dove il numero contenutoall'incrocio di una riga e di una colonna rappresenta una delle seguenti azioni, da intra-

Page 271: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 271/373

 

prendere quando il simbolo di riga è sulla cima della pila e il simbolo di colonna è quelloattualmente letto nell'espressione infissa:

1. viene eseguita la Push del simbolo corrente dell'espressione infissa;2. viene eseguita una Po p e l'operatore così ot tenu to viene appeso in fond o all'espres-

sione postfissa in costruzione;3. il simbolo corrente viene ignorato nell'espressione infissa e viene eseguita una Po p(ignorando il simbolo restituito);

4. la conversione ha avuto termine , il simbolo corrente viene cancellato dall'espres-sione infissa e viene eseguita una Pop (ignorando il simbolo restituito).

Usando la tabella in Figura 7.1, possiamo trasformare anche espressioni che hannodelle parentesi implicitamente definite dall'associatività a sinistra e dalla precedenza deglioperatori, come nel caso di 6 + (5 — 4) x ( 1 + 2 + 3). Il costo computazionale dell'al-goritmo di conversione e di valutazione è O( n) tempo per un'espressione di n simboli,

ipotizzando che il costo di valutazione di un singolo operatore sia costante e quindi nondipenda dalla lunghezza dell'espressione.

A L V I E : conversione di un'espressione infissa in una postfissa

Osserva, sperimenta e verifica

I n f i x P o s t f i x

7.3 Code

Analogamente alla pila, la coda è una collezione di elementi in cui le operazioni di-sponibili sono defin ite dalla seguente politica di accesso: men tre nella pila l'accesso èconsentito solo all'ultimo elemento inserito, nella coda estraiamo il primo elemento, in"testa" alla coda, essendo presente da più tempo, mentre inseriamo un nuovo elemen-to in fondo alla coda, perchéè più recente. Ciò corrisponde a quanto avviene in moltesituazioni quotidiane come, ad esempio, nel pagare un pedaggio autostradale, nel fareacquisti in un negozio e, in generale, nel ricevere una serie di eventi o richieste da ela-borare. Una politica del tipo suddetto viene detta FIFO (First In First Out) in quanto ilprimo elemento a essere inserito nella coda è anche il primo a essere estratto.

Le operazioni principali definite su una coda permettono di inserire un nuovo ele-men to nella coda e di estrarre un elemento dalla coda stessa in tempo costante: Enqueu einserisce un nuovo elemento in fondo alla coda, Dequeue estrae l'elemento dalla testadella coda e restituisce l'informazione in esso contenuta e First restituisce l'informa-zione con tenuta nell'e lemento in testa alla coda, senza estrarre tale elemento . Infine,

Page 272: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 272/373

 

l'operazione Empty verifica se la coda è vuota. Mentre nella pila c'è un unico puntodi accesso su cui le operazioni vanno a incidere (quello corrispondente alla "cima" dellapila), nel caso della coda ne esistono due, ovvero le estremità della coda stessa, in quantoF i r s t e Dequeue vanno a operare sulla "testa" della coda, mentre En qu eue incide sul"fondo" della coda.

7.3.1 Implementazione di una coda mediante un array

L'implementazione di una coda mediante un array consiste nel memorizzare gli elementidella coda in un array di dimensione variabile. Gli elementi della coda sono memorizzatiin sequenza nell'array a partire dalla locazione associata all'inizio della coda, inserendoliman mano nella prima locazione disponibile: ciò comporta che la fine della coda cor-risponde all 'ultimo elemento di tale sequenza. Basterà quindi tenere traccia dell'indice(indicato con testaCoda) della locazione che contiene il primo elemento della sequen-

za e di quello (indicato con f ondoCoda) della locazione in cui poter inserire il prossimoelemento, per implementare le operazioni F i r s t , En queue e Deq ueu e in tempo co-stante ammortizzato. Tuttavia, per poter sfruttare al meglio lo spazio a disposizione enon dover spostare gli elementi ogni volta che un oggetto viene estratto dalla coda, lagestione dei due indici testaCoda e f ondoCoda avviene in modo "circolare" rispettoalle locazioni disponibili nell'array. In altre parole, se uno di questi due indici superala fine dell'array, allora esso viene azzerato facendo, quindi, in modo tale che indichi laprima locazione dell'array stesso.

Nel Codice 7.3, utilizziamo tre interi cardCoda, testaCoda e fondoCoda chesono stati inizializzati rispettivamente con i valori 0, 0 e — 1 : inizialmente la coda è vuota(quindi, contiene cardCoda = 0 elementi e testaCoda vale 0) e il prossimo elemen-to potrà essere inserito nella prima locazione dell'array (quindi, fondoCoda vale — 1perché viene prima incrementato). Il metodo Empty si limita a verificare se il valoredi ca rd Co da è uguale a 0. L'operazione di incremento di t e s t a C o d a e fon do Co daè circolare, per cui adoperiamo il modulo della divisione intera a tal fine (riga 4 inEnqueue e riga 5 in Dequeue, nelle quali supponiamo che la lunghezza dell'arraysia memorizzata nella variabile l ung hezz aA r r ay ) . Inoltre, osserviamo che, quandoarrayCoda deve essere raddoppiato o dimezzato, le funzioni Verif icaRaddoppioe VerificaDimezzamento ricollocano ordinatamente gli elementi della coda nelleprime celle dell'array e pongono te s t a C o d a a 0 e fo nd oC od a a ca rd Co da — 1.

7.3.2 Implementazione di una coda mediante una lista

L'implementazione più naturale di una coda mediante una struttura con riferimenti con-siste in una sequenza di nodi concatenati e ordinati in modo crescente secondo l'istantedi inserimento. In tal modo, il primo nodo della sequenza corrisponde alla "testa" della

Page 273: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 273/373

 

Enqueue( x ):

VerificaRaddoppio( );

cardCoda = cardCoda + 1 ;

fondoCoda = (fondoCoda + 1) "/, lunghezzaArray ;

codaArrayt fondoCoda ] = x;

Dequeue( ):

IF (!Empty( )) {

cardCoda = cardCoda - 1;

x = codaArrayt testaCoda ];

testaCoda = (testaCoda + 1) '/, lunghezzaArray;

VerificaDimezzamento( );

RETURN X;

>

First( ) :

IF (!Empty( )) RETURN codaArrayt testaCoda ];

Empty( ):

RETURN (cardCoda == 0);

Cod ice 7.3 Implementazione della coda mediante un array: le funzioni Verif icaRaddoppio eVerif icaDimezzamento seguono l'approccio del Paragrafo 2.1.3 e aggiornano anchei valori di testaCoda e di fondoCoda.

coda ed è il nodo da estrarre nel caso di una Dequeue, mentre l'ultimo nodo corrisponde

al "fondo" della coda ed è il nodo a cui concatenare un nuovo nodo, inserito mediante

Enqueue. Lasciamo al lettore il compito di definire tale implementazione sulla falsariga

di quanto fatto per la pila nel Codice 7.2 e per le liste doppie nel Paragrafo 5.2.

7.4 Opus libri: Web crawler e visite di grafi

Abbiamo già visto che uno degli esempi più noti di grafo orientato G = (V, E) è fornito

dal World Wide Web in cui l'insieme dei vertici in V è costituito dalle pagine Web el'insieme degli archi orientat i in E sono i collegamenti tra le pagine. No n abb iamo,però, ancora detto come tali collegamenti sono rappresentati all'interno delle pagine ecome i crawler di un motore di ricerca (Paragrafo 6.4) possano recuperarli e utilizzarlinel processo di raccolta delle pagine Web.

Ogni pagina Web è identificata da un indirizzo URL ( Uniform Resource Locator).

Per esempio, / / w w w . w 3 . o r g / A d d r e s s i n g / A d d r e s s i n g . h t m l è l'URL della pagi-na Web in cui è descritta la specifica tecnica delle URL: la par te racchiusa tra / / e

Page 274: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 274/373

 

il primo / successivo (www.w3.org) è un riferimento simbolico a un indirizzo di uncalcolatore ospite (host ) connesso a Internet . Tale indirizzo è una sequenza di 32 bitse viene utilizzato il protocollo IPv4 (oppure di 128 bit nel caso di IPv6). Infine, laparte rimanente dell'URL, ovvero / A d d r e s s i n g / A d d r e s s i n g . h t m l , indica un per-corso interno al file system dell'host, che identifica il file corrispondente alla pagina Web.Un collegamento da una pagina a un'altra è specificato, all'interno della prima, utiliz-zando la seguente sintassi definita nel linguaggio HTML ( HyperText Markup Langua-ge), che permette di associare, tra l'altro, un testo a ogni link con la seguente sintassi< a h r e f = " h t t p : URL">testo</a>.

Queste informazioni sono utilizzate dai crawler per attraversare il grafo del web inmaniera sistematica ed efficiente. Infatti, vista la dimensione del grafo del Web, è im-proponibile generare tutti gli indirizzi a 32 o 128 bit dei possibili host di siti Web, peraccedere alle loro pagine. I crawler effettuano invece la visita del grafo del Web partendoda un insieme S di pagine selezionate: in pratica, S viene formato con gli indirizzi di-sponibili in alcune collezioni, come Open Directory Project, che contengono un insiemedi pagine Web raccolte e classificate da editori "umani". Quando un crawler scopre unanuova pagina, la lista dei link in uscita da essa permette di estendere la visita a ulterioripagine (in realtà la situazione è più complessa per la presenza di pagine dinamiche e diformati diversi e, inoltre, per altre problematiche come la ripartizione del carico di lavorotra i vari crawler).

Possiamo quindi modellare il comportamento dei crawler come una visita di tutti inodi e gli archi di un grafo raggiungibili da un nodo di partenza, ipotizzando che i verticisiano identificati con numeri compresi tra 0 e n — 1 (quindi V = {0,1,... ,n — 1]) e ilnumero di archi sia indicato con m = |E|. Osserviamo che gli algoritmi di visita discussinei seguito Utilizzino le pile e le code discusse finora e funzionano sia per grafi orientatiche per grafi non orientati.

7.4.1 Visita in ampiezza di un grafo

Abbiamo già incontrato la visita in ampiezza nel caso degli alberi: tale tipo di visita, chein tal caso prende il nome di BFS {Breadth-First Search), può essere applicato anche a grafi(di cui gli alberi sono un caso particolare), tenendo presente l'esigenza di evitare che la

presenza di cicli possa portare a esaminare ripetutamente gli stessi cammini. Nell'esporrela visita in ampiezza su un grafo, faremo inizialmente l'ipotesi che l'insieme dei nodi siaconosciuto: successivamente considereremo il caso più generale in cui tale insieme nonsia preventivamente noto.

Al fine di esaminare ogni arco un numero limitato di volte (in particolare 2 o 1 indipendenza del fatto che il grafo sia orientato o meno) nel corso della visita, usiamo unarray booleano di appoggio raggiunto, tale che raggiunto [u] vale T R U E se e solose il nodo u è stato scoperto nel corso della visita effettuata fino a ora. Il resto dello

Page 275: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 275/373

 

BreadthFirs tSearchC s ) :FOR  (u = 0 ; u < N; u = U + 1)

r a g g i u n t o [ u ] = FALSE;

Q.Enqueue( s );  WHILE (!Q.Empty( )) {

u = Q.Dequeue( ) ;IF (!raggiunto [u] ) {

r a g g i u n t o [ u ] = TRUE;

FOR (X = l i s t aA d i ac en za [u ] . i n i z io ; x != nu l i ; x= x. succ) {v = x .da to ;Q.Enqueue( v );

>>

Codice 7.4 Visita in ampiezza di un grafo con n vertici a parti re dal vertice s, utilizzando unacoda Q inizialmente vuota e un array raggiunto per marcare i vertici visitati.

schema segue quello visto per gli alberi, in cui la lista di adiacenza del vertice correntefornisce, come al solito, i rifer iment i ai suoi vicini. Il Codice 7.4 riporta lo schemadi visita a partire da un vertice prescelto s, dove l i s t a A d i a c e n z a [ u ] . i n i z i o indicail riferimento all'inizio della lista di adiacenza per il vertice u . Do po aver inizializzator a g g i u n t o e la coda Q (righe 2-4), inizia il ciclo di visita. Il vertice u in testa alla coda

viene estratto (riga 6) e, se non è stato ancora raggiunto, viene marcato come tale e la sualista di adiacenza viene scandita a partire dal primo elemento (righe 7-13). Poiché ognivertice viene inserito nella coda soltanto se non ancora marcato, e quindi una sola volta,ciascuna delle liste di adiacenza esaminate viene anch'essa considerata una sola volta: inconseguenza di ciò, il costo totale della visita è dato dalla somma delle lunghezze delleliste di adiacenza esaminate, ovvero al più dalla somma dei gradi di tutti i vertici delgrafo, ottenendo un totale di 0(n + m) tempo e O(n) celle di memoria aggiuntive.

Un esempio di visita in ampiezza è descritto nella Figura 7.2, dove l'ordine dei verticiin ciascuna lista di adiacenza determina l'ordine di visita dei vertici stessi nel grafo: sup-poniamo che i vertici in ciascuna lista di adiacenza siano mantenuti in ordine crescente,se non specificato altrimenti. L'ordine con cui i vertici vengono raggiunti dalla visita delgrafo illustrato nella parte sinistra della Figura 7.2 è dato da 0 , 1 , 2 , 3 , 5 , 4 , 6 , 7 (l'ordi-ne del loro inserimento nella coda). In particolare, dal vertice 0 raggiungiamo i vertici1,2,3 e 5, dal vertice 3 raggiungiamo 4 e 6 e dal vertice 5 raggiungiamo 7 (mentre irimanenti vertici non permettono di raggiungerne altri).

L'esempio illustra alcune proprietà interessanti della visita. Gli archi che conduconoa vertici ancora non visitati, permettendone la scoperta, formano un albero detto albero

Page 276: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 276/373

 

destra, annotato con gli archi all'indietro tratteggiati e la profondità dei nodi.

BFS, la cui struttura dipende dall'ordine di visita (parte destra della Figura 7.2). Perpoter costruire tale albero, modifichiamo lo schema di visita illustrato nel Codice 7.4:invece di usare una coda Q in cui i vertici sono inseriti ed estratti una sola volta, usiamo Qcome coda in cui gli archi sono inseriti ed estratti una sola volta. Il Codice 7.5 riportatale modifica della visita in ampiezza: dopo aver estratto l'arco (u', u) dalla coda (riga 6),scandiamo la lista di adiacenza di u solo se quest'ultimo non è stato scoperto (riga 7). Lavisita richiede 0(n + m) tempo, in quanto ogni arco è inserito ed estratto una sola volta

e le liste di adiacenza sono scandite solo quando i corrispondenti vertici sono visitati laprima volta.

Utilizzando il Codice 7.5, non è difficile individuare gli archi dell'albero BFS: bastamemorizzare l'arco (u',u) quando il nodo u viene marcato come raggiunto nella riga 8e, inoltre, u ' diventa il padre di u nell'albero BFS. In generale, gli archi individuati in talmodo formano un sottografo aciclico e, quando gli archi di tale sottografo sono inciden-ti a tutti i vertici, l'albero BFS ottenuto è un albero di ricoprimento (spanning tree) delgrafo, vale a dire un albero i cui nodi coincidono con quelli del grafo. Cambiando l'ordi-ne relativo dei vertici all'interno delle liste di adiacenza, possiamo ottenere alberi diversi.

Notiamo infine che tali alberi, avendo grado variabile, possono essere rappresentati comealberi ordinali (Paragrafo 4.4).

L'albero BFS è utile per rappresentare i cammini minimi dal vertice di partenza sverso tutti gli altri vertici: tale proprietà è vera in quanto gli archi non sono pesati (al-trimenti non è detto che valga, e vedremo successivamente come gestire il caso in cuigli archi sono pesati). Per verificare tale proprietà, basta osservare che l'algoritmo visitaprima i vertici a distanza 1 (ovvero i vertici adiacenti a s), poi quelli a distanza 2 e cosìvia, come un semplice ragionamento per induzione può stabilire. In altre parole, la prò-

Page 277: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 277/373

 

BreadthFirstSearch( s ) :

FOR  (u = 0; u < n; u = u + 1)

raggiunto[u] = FALSE;

Q.Enqueue( (null, s) );

 WHILE (!Q.Empty( )) {

(u', u) = Q.Dequeue( );IF (!raggiunto[u]) {

raggiunto[u] = TRUE;

FOR (x = listaAdiacenza[u].inizio ; x != nuli; x=x.succ) {

v = x.dato;

Q.Enqueue( (u, v) );

}>

Codice 7 .5 Visita in ampiezza di un grafo in cui la coda Q contiene archi anziché vertici.

fondità p di un vertice v nell'albero BFS indica che la sua distanza minima da s nel grafoè proprio p; inoltre, i nodi irraggiungibili da s non vengono inclusi nell'albero BFS, epossiamo considerare che risultino a distanza infinita da s.

Per verificare quanto affermato sopra ragioniamo in modo induttivo rispetto alladistanza dei nodi da s: il caso base del l'induzione è banalme nte verificato in qua nt o

l'unico nodo a profondità 0 è s che evidentemente ha distanza 0 da sé stesso.Per mostrare il passo indutt ivo, supponiamo che per ogni nodo u a distanza p ' < p

da s la profondità di u. sia pari a p ' e ipotizziamo che esista, per assurdo, un nodo v adistanza 6 da s la cui profond ità nell'albero BFS sia p / 6, e quindi tale che il ca mminominimo da s a v sia di lunghezza 6. Consideriamo allora sia il vertice v'  (a distanza 6—1da s) che precede v in tale cammino, che il padre u di v a profondità p — 1 nell'alberoBFS. Evidentemente, non può essere p < 6 in quanto, in tal caso, il cammino da s a vche attraversa u avrebbe lunghezza minore di 6, il che contraddice l'ipotesi che 6 sia ladistanza tra s e v. Al tempo stesso, se 6 < p l 'algoritmo di visita avrebbe raggiunto v

da v' e quindi v avrebbe profondità 6 (infatti v' sarebbe stato visitato prima di u).La proprietà appena discussa ha due conseguenze rilevanti.

1. Gli archi del grafo che non sono nell'albero BFS sono chiamat i all'indietro (back):

possono collegare solo due vertici alla stessa profondità nell'albero BFS oppure a

profondità consecutive p e p + 1.

2. Il diametro del grafo può essere calcolato come la massima tra le altezze degli alberiBFS radicati nei diversi vertici del grafo (in generale, ne possono esistere n diversi) .Il tempo richiesto per calcolare il diametro è 0(n(n + m)).

Page 278: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 278/373

 

BreadthFirs tSearchExploreC s ) :Q.Enqueue( s );

 WHILE ( ! Q . E m p t y ( ) ) {

u = Q.Dequeue( ) ;IF ( !D.Appar t iene(u)) {

D . I n s e r i s c i ( u ) ;FOR (X = l i s t aA di ac en za [u ] . in iz io ; x != nu l i ; x=x .succ) {

v = x.dato;Q.Enqueue( v );

>>

Co di ce 7.6 Esplorazione mediante visita in ampiezza di un grafo, utilizzando una coda Q e undizionario D per memorizzare i vertici visitati.

Un'altra applicazione interessante è che la visita BFS ci permette di stabilire se ungrafo non orientato G è connesso. Infatti, G è connesso se e solo se raggiuntoci.] valeT R U E per ogni 0 ^ u. ^ n — 1, ovvero tutt i i vertici sono stati raggiunti e quindi abbiamoche l'albero BFS è un albero di ricoprimento. Il costo computazionale è lo stesso dellavisita BFS, quindi 0(n + m) tempo e O(n) celle di memoria.

Nel caso in cui l'insieme dei nodi del grafo non sia preventivamente noto, e quindi

il grafo debba essere esplorato per determinarne la struttura, non è possibile evidente-mente utilizzare un array per distinguere i nodi raggiunti da quelli non ancora trovati.Per ottenere tale funzionalità è necessario fare uso di una struttura di dati che consen-ta di rappresentare un insieme, nello specifico l'insieme dei vertici raggiunti, dando lapossibilità di aggiungere nuovi elementi all'insieme e di verificare l'appartenenza di unelemento all'insieme stesso.

Tali funzionalità sono offerte da un dizionario (Capitolo 5), che quindi impieghiamonel Codice 7.6, che è una semplice riscrittura del Codice 7.4. Nel codice in questione,il dizionario D, inizialmente vuoto, viene in effetti utilizzato in sostituzione dell'array

r a g g i u n t o per rappresentare, a ogni istante, l'insieme dei nodi già raggiunti dalla visita.Dal punto di vista del costo computazionale, la sostituzione dell'array con un di-

zionario fa si che tale costo dipenda dal costo delle operazioni definite sul dizionario,dipendente a sua volta dall'implementazione adottata per tale struttura di dati.

In particolare, esaminando il Codice 7.6 risulta che l'operazione I n s e r i s c i è ese-guita O(n) volte, mentre l'operazione Appartiene è invocata O(m) volte: ad esempio,utilizzando una tabella hash, per la quale le due operazioni richiedono tempo medio0(1), il costo complessivo della visita èO(n + m) nel caso medio. Utilizzando invece un

Page 279: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 279/373

 

DepthFirstSearchC s ):FOR  (u = 0; u < n; u = u + 1)

r a g g i u n t o [ u ] = FALSE;

P.PushC s );  WHILE (! P . E m p t y ( ) ) {

u = P.PopC ) ;IF (! raggiunto[u]) {r a g g i u n t o [ u ] = TRUE;

FOR (x = l i s t aA di ac en za [u ] . f i ne ; x != nu l i ; x = x .p r ed) {v = x .da to ;P.PushC v );

>>

Cod ice 7.7 Visita in profondità di un grafo utilizzando una pila P di archi, inizialmente vuota.Ciascuna lista di adiacenza viene scandita all'indietro.

albero di ricerca bilanciato, i costi delle due operazioni sono O(logn) nel caso peggiore,e quindi il costo conseguente dell'algoritmo è 0((n + m.) logn).

A L V I E : visita in ampiezza di un grafo

Osserva, sperimenta e verifica M  ™ •

B r e a d t h F i r s t S e a r c h —

7.4.2 Visita in profondità di un grafo

Se nello schema di visita illustrato nei Codici 7.4 e 7.5 sost ituiamo la coda Q con una

pila P, otteniamo un altro algoritmo di visita di grafi, noto come algoritmo di visita inprofondità, o DFS {Depth-First Search), realizzato dai Codici 7.7 e 7.8. Dato che in unapila un insieme di elementi viene estratto in ordine opposto a quello di inserimento,volendo estrarre dalla pila gli archi incidenti a un nodo u nello stesso ordine con cui liincontriamo scandendo la lista di adiacenza di u, dobbiamo inserire nella pila tali archiin ordine inverso rispetto a quello della lista. Analogamente alla visita in ampiezza, anchenella visita in profondità viene costruito un albero, detto albero DFS, i cui archi vengonoindividuati in corrispondenza alla scoperta di nuovi vertici.

Page 280: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 280/373

 

DepthFirstSearchC s ):

FOR  (u = 0; u < n; u = u + 1)

raggiunto[u] = FALSE;

P.Push( (nuli, s) );

 WHILE (!P.Empty( )) {

(u\ u) = P.Pop( );IF (!raggiunto[u]) {

raggiunto[u] = TRUE;

FOR (x = listaAdiacenza[u].fine; x != nuli; x = x.pred) {v = x.dato;

P.Push( (u, v) );

>>

Codice 7. 8 Visita in profondità di un grafo in cui la pila P contiene archi anziché vert ici.

La visita in profondità, per la natura stessa della politica LIFO che adotta la pila, sipresta in modo naturale a un'implementazione ricorsiva, riportata nel Codice 7.9. Taleimplementazione è ampiamente usata in varie applicazioni discusse in seguito, in alter-nativa a quella iterativa; notate che in questo caso il fatto che l'utilizzo della ricorsionenon richieda una gestione esplicita di una pila fa si che non sia più necessario effettuare

una scansione al contrario delle liste di adiacenza.Unitamente alla visita ricorsiva, il codice mostra la funzione Scansione, che esami-

na tutti i vertici del grafo alla ricerca di quelli non ancora scoperti, invocando la ricorsio-ne su ciascun nodo s di questo tipo, utilizzandolo come vertice di partenza di una nuovavisita. Osserviamo che l'esame di tutti i vertici del grafo in effetti non richiede necessa-riamente l'adozione di una visita in profondità per ogni nodo non ancora raggiunto: inlinea di principio, anzi, potremmo utilizzare tipi di visita diversi.

Il costo computazionale delle visite in profondità discusse sopra è analogo a quello

della visita in ampiezza, ovvero 0 ( n + m) tempo sia per grafi orienta ti che per grafi non

orientat i. Il nu me ro di celle di memoria richieste per la visita iterativa è O( m) mentreper quella ricorsiva è 0(n).

Un esempio di visita di un grafo orientato, a part ire dal vertice s = 0, è ripor-tato nella Figura 7.3, dove sono mostrati sia l'albero BFS che l'albero DFS risultan-ti. Durante la visita in profondità, in particolare, i vertici vengono scoperti nell'ordine0,1,2,4,5,3 (quest'ultimo a partire dal vertice 1). Una caratteristica importante dellavisita D e p t h F i r s t S e a r c h R i c o r s i v a è che la pila implici tamente mantentuta dallachiamata ricorsiva su un vertice u contiene i nodi, in ordine inverso, lungo il cammino n

Page 281: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 281/373

 

Scansione( G ):FOR  (s = 0; s < n; s = s + 1)

r a g g i u n t o [ s ] = FALSE;

FOR (S = 0; s < n; s = s + 1) {IF ( ¡ r a gg i un t o [ s ] ) Dep thF i r s tSea rchR icor s iva ( s ) ;

>DepthFi r s tSea rchR icor s iva ( u ) :

r a g g i u n t o [ u ] = TRUE;

FOR (x = l i s t aA di ac en za [u ] . in iz io ; x != nu l i ; x = x . su cc) {v = x.dato;IF ( ¡ r a g g i u n t o [ v ] ) D e p t h F i r s t S e a r c h R i c o r s i v a ( v ) ;

>

Cod ice 7 .9 Visita in profondità di un grafo utilizzando la ricorsione.

dell'albero DFS che va dalla radice s al nodo u. In altre parole, esaminando il contenutodella pila implicita per ogni vertice scoperto, otteniamo la visita anticipata dell'alberoDFS. Per esempio, quando la chiamata di D e p t h F i r s t S e a r c h R i c o r s i v a esaminail vertice u = 3 nella Figura 7.3, la pila implicita contiene i vertici corrispondenti alcammino 7t = 0,1,3 nell'albero DFS (il vertice u = 3 è in cima alla pila).

Per quanto riguarda invece gli archi non appartenenti all'albero DFS, questi, nel caso

di grafi orientati, possono essere classificati ulteriormente. In particolare, un arco (u, v)non appartenente all'albero DFS, può essere catalogato come segue:

« all'indietro (back)-, se v è antenato di u nell'albero DFS;

• in avanti ( forward ): se v è discendente di u nell'albero DFS (nipote, pronipote ecosì via, ma non figlio perché altrimenti l'arco apparterrebbe all'albero);

• trasversale (cross): se v e u non sono uno antenato dell'altro.

Nei grafi non orientati possono esserci solo archi all'indietro, che sono gli unici a con-durre a vertici già visitati durante la visita in profondità.

Dall'esempio nella Figura 7.3 emerge anche la differenza tra le due visite. Nella visita

in ampiezza, i vertici sono esaminati in ordine crescente di distanza dal nodo di partenzas, per cui la visita risulta adatta in problemi che richiedono la conoscenza della distanza edei cammini minimi (non pesati): successivamente, vedremo come, in effetti, un limitatoadattamento della visita in ampiezza consenta di individuare i cammini minimi anche ingrafi con pesi sugli archi.

Nella visita in profondità, l'algoritmo raggiunge rapidamente vertici lontani dal ver-tice di partenza s, e quindi la visita è adatta per problemi collegati alla percorribilità, allaconnessione e alla ciclicità dei cammini.

Page 282: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 282/373

 

 A  © © ©© © ©

avanti© V

F  ?

( T ) ; avanti

'trasversale' 

indietro \ ©

trasversale

X D - ' "

Figura 7.3 Un grafo orientato, il relativo albero BFS a partire dal vertice s, dove s = 0, e l'alberoDFS a partire da s con la classificazione degli archi in avanti, all'indietro e trasversali.

Anche per la visita in profondità, l'esplorazione di un grafo di cui non sono preven-tivamente noti i vertici richiede la sostituzione dell'array r a g g i u n t o con un dizionario:valgono rispetto a ciò le considerazioni effettuate per la visita in ampiezza nel Paragra-fo 7.4.1. Nel caso specifico del grafo del Web, possiamo sost ituire la coda o la pila conuna coda che estrae gli elementi in base a un loro valore di rilevanza (il rank  delle pa-

gine Web), garantendo in questo modo la priorità di caricamento, durante la visita, allepagine classificate come più interessanti.

A L V I E : visita in profondità di un grafo

Osserva, sperimenta e verifica

D e p t h F i r s t S e a r c h

7.5 Applicazioni delle visite di grafi

7.5.1 Grafi diretti aciclici e ordinamento topologico

La visita in profondità trova applicazione, tra l'altro, nell'identificazione dei cicli in un

grafo, che in tal caso viene detto ciclico, mentre è aciclico se non contiene cicli: vale in-

fatti la proprietà che un grafo G è ciclico se e solo se contiene almeno un arco all'indietro

Page 283: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 283/373

 

(definito per le visite BFS e DFS). Infatti, consideriamo un grafo con un arco all'indie-tro (u, v) che, ricordiamo, non appartiene all'albero di ricoprimento (BFS o DFS) di G:esiste un cammino da v a u nell'albero in quanto, per definizione di arco all'indietro, v èantenato di u e da ciò deriva che, estendendo questo cammino con (u,v), ritorniamo inv, ottenendo quindi un ciclo. Viceversa, consideriamo un grafo contenente un ciclo: lavisita costruisce un albero DFS che non può contenere tutti gli archi del ciclo, in quantoun albero è aciclico, e quindi almeno un arco è all'indietro.

L'argomentazione suddetta vale per grafi orientati e non; infatti, nel caso di grafiorientati, se un ciclo contiene un arco in avanti (u,v) allora possiamo sostituire tale arcocon il cammino da u a v nell'albero e ottenere comunque un ciclo (ricordiamo che, nelcaso di grafi orientati, gli archi nell'albero sono diretti dai padri ai figli). Quindi, se ilgrafo è ciclico, esiste necessariamente un ciclo che non include archi in avanti. Infine, unarco trasversale (u,v) non può appartenere a un ciclo. Se così fosse, nell'albero il nodo usarebbe antenato di v o viceversa, pervenendo a una contraddizione.

L'algoritmo per verificare se un grafo è aciclico richiede 0(n + m) tempo: per otte-nerlo, è infatti sufficiente modificare la visita in profondità (aggiungendo un ramo ELSEal controllo nella riga 7 del Codice 7.8 o nella riga 5 del Codice 7.9) in modo che, seessa trova che un vertice è già stato scoperto, determina che l'arco (u, v) è all'indietro equindi che il grafo contiene un ciclo.

Un grafo orientato aciciclo è chiamato DAG {Directed Acyclic Graph) e viene utiliz-zato in quei contesti in cui esiste una dipendenza tra oggetti espressa da una relazioned'ordine: per esempio gli esami propedeutici in un corso di studi, la relazione di eredita-

rietà tra classi nella programmazione a oggetti, la relazione tra ingressi e uscite delle portein un circuito logico, oppure l'ordine di valutazione delle formule in un foglio elettro-nico. In generale, supponiamo di avere decomposto un'attività complessa in un insiemedi attività elementari e di avere individuato le relativa dipendenze. Un esempio concretopotrebbe essere la costruzione di un'automobile: volendo eseguire sequenzialmente le at-tività elementari (per esempio, in una catena di montaggio), bisogna soddisfare il vincoloche, se l'attività B dipende dall'attività A, allora A va eseguita prima di B.

Usando i DAG per modellare tale situazione, poniamo i vertici in corrispondenzabiunivoca con le attività elementari, e introduciamo un arco (A,B) per indicare che

l'attività A va eseguita prima dell'attività B. Un'esecuzione in sequenza delle attività chesoddisfi i vincoli di precedenza tra esse, corrisponde a un ordinamento topologico delDAG costruito su tali attività, come illustrato nella Figura 7.4.

Dato un DAG G = (V, E), un ordinamento topologico di G è una numerazio-ne r| : Vi -» {0 ,1 ,. .. , n — 1} dei suoi vertici tale che per ogni arco (u ,v ) e E valer|(u) < r|(v). In altre parole, se disponiamo i vertici lungo una linea orizzontale in ba-se alla loro numerazione T|, in ordine crescente, otteniamo che gli archi risultano tuttiorientati da sinistra verso destra. L'ordinamento topologico può anche essere visto come

Page 284: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 284/373

 

© ®

© © © ( è " " © " ®

ti 0 1 2 3 4 5

®Figura 7 .4 Un esempio di DAG e di suo ordinamento topologico.

un ordinamento totale compatibile con l'ordinamento parziale rappresentato dal DAG.Osserviamo che i grafi ciclici non hanno un ordinamento topologico.

Concettualmente, l'ordinamento topologico di un DAG G può essere trovato comesegue: prendiamo un vertice z avente grado di uscita nullo, ovvero tale che la sua listadi adiacenza è vuota (tale vertice deve necessariamente esistere, altrimenti G sarebbeciclico). Assegniamo il valore r|(z) = n — 1 a tale vertice, rimuovendolo quindi da Ginsieme a tutti i suoi archi entranti, ottenendo così un grafo residuo G ' che sarà ancoraun DAG. Identifichiamo ora un vertice z' di grado di uscita nullo in G', a cui assegniamoq(z') = n — 2, rimuovendolo come descritto sopra.

Iterando questo procedimento, otteniamo alla fine un grafo residuo con un solovertice s, a cui assegniamo numerazione ri(s) = 0. Ogn i arco (u,v) è evidentemente

orientato da sinistra a destra nell'ordinamento indotto da rj, semplicemente perché v vie-ne rimosso prima di u per cui q(v) > T|(u). Da tale procedimento deduciamo, inoltre,che non è detto che esista un unico ordinamento topologico per un dato DAG, in quan-to, in generale, in un dato istante possono esistere più nodi aventi grado di uscita nullo,e quindi più possibilità (tutte corrette) di scelta del nodo da rimuovere.

L'algoritmo per trovare un ordinamento topologico, in realtà, può essere realizzatoin modo semplice, come mostrato nel Codice 7.10. Esso si basa sulla visita ricorsiva inprofondità discussa precedentemente nel Codice 7.9: tale visita viene estesa realizzandola funzione TI(u) mediante un array e t à [ u] e un contatore globale, inizializzato a n — 1(riga 4 dell'ordinamento topologico).

Dopo che le visite lungo gli archi uscenti da u sono terminate, il contacore viene asse-gnato a e t a [ u ] e decrementato di uno (righe 7 - 8 della visita ricorsiva). Intuitivamente,tutti i vertici raggiungibili da u in uscita sono stati esaminati e hanno ricevuto una nume-razione strettamente maggiore del valore corrente del contatore, per cui assegnando talevalore a e t a [ u ] garantiamo che valga et a[ u] < et a[ u] per ogni arco uscente (u, v). Pergarantire di assegnare una numerazione a tutti i vertici, scandiamo l'insieme dei verticistessi, invocando la visita ricorsiva su quelli non ancora raggiunti dalle visite precedenti.

Page 285: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 285/373

 

Ordinamento-topologico( ):

FOR (s = 0; s < n; s = s + 1)raggiunto[s] = FALSE;

contatore = n - 1 ;

FOR (S = 0; s < n; s = s + 1) {

IF (!raggiunto[s]) DepthFirstSearchRicorsivaOrdinaC s );>

DepthFirstSearchRicorsivaOrdinaC u ):

raggiunto[u] = TRUE;FOR (X = listaAdiacenza[u].inizio; x != nuli; x = x.succ) {

v = x.dato;

IF (!raggiunto[v]) DepthFirstSearchRicorsivaOrdina( v );>eta[u] = contatore;

contatore = contatore - 1;

Codice 7.10 Ordinamento topologico di un grafo diretto aciclico. Per ogni vertice, l'array etàindica l'ordine inverso di terminazione della visita in profondità ricorsiva.

Il costo computazionale rimane 0 ( n + m) tempo e lo spazio occupato è O( n) celle di

memoria, richieste dalla pila implicitamente gestita dalla ricorsione.

A L V I E : ordinamento topologico di un grafo

Osserva, sperimenta e verificaTopologicalSort

7.5.2 Componenti (fortemente) connesse

Le visite di grafi sono utili anche per individuare le componenti connesse di un grafo nonorientato. Riconsiderando la scansione effettuata nel Cod ice 7.9 da questo pu nt o di vista,notiamo che tutti i vertici sono connessi se e solo se, al termine della visita, tutti i verticirisultano raggiunti. Ne deriva che, in tal caso, tutti i valori dell'array r a g g i u n t o sonoTRUE dopo la terminazione di S c a n s i o n e . Se, al contrario, qualche vertice non risultaraggiunto, esso verrà esaminato successivamente nel corso della visita ricorsiva invocatasu un qualche nodo s / 0: ogni qualvolta abbiamo che r a g g i u n t o [ s ] vale FALSE, vieneindividuata una nuova com ponen te connessa, che include il no do s. L'individuazione

Page 286: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 286/373

 

d

a

1Figura 7.5 Un esempio di grafo orientato con le sue componenti fortemente connesse.

delle componenti connesse in un grafo orientato richiede pertanto 0(n + m) tempo eO(n) celle di memoria.

Nel caso di un grafo orientato, come il grafo del Web, le componenti fortemente

connesse sono collegate alla nozione di navigatore casuale (Paragrafo 6.4): osserviamoperò che, se il grafo ha più di una componente fortemente connessa, il navigatore casualerischia di non poter più raggiungere tutte le pagine perché resta intrappolato in unadelle componenti. Tale eventualità viene scongiurata introducendo la possibilità per ilnavigatore di scegliere la prossima pagina in modo casuale.

Per individuare le componenti fortemente connesse in un grafo orientato G = (V, E)applichiamo a G una visita in profondità ottenendo, come vedremo, una partizione del-l'insieme dei vertici V in sottoinsiemi Vo, Vi,..., V s^i massimali e disgiunti, e tale checiascun sottoinsieme Vi soddisfa la proprietà che due qualunque vertici u,v G V; sono

collegati da un cammino orientato sia da u a v che da v a u (mentre questa proprietà nonvale se u G Vi e v e Vj per i ^ j).

Un semplice esempio di grafo composto da una singola componente fortementeconnessa è dato da un ciclo orientato di vertici, oppure da un grafo contenente un cicloEuleriano (che, ricordiamo, attraversa tutti gli archi una e una sola volta).

Un esempio di grafo composto da più componenti è invece mostrato nella Figu-ra 7.5, dove le componenti (massimali) sono racchiuse in cerchi per scopo illustrativo.Le componenti possono essere anche viste come macro-vertici, collegati da archi multipli.

Page 287: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 287/373

 

S = T0

U

Figura 7.6 Passo generico dell'algoritmo per l'individuazione delle componenti fortemente con-nesse: quelle complete sono indicate in grigio chiaro mentre quelle parziali, attraver-sate dal cammino n dal vertice s al vertice u, sono rappresentate in neretto (quelleisolate non sono mostrate). I vertici nelle componenti parziali sono di tre tipi: quellilungo 7i (in neretto), quelli con la visita completata (in grigio scuro) e quelli ancora dascoprire (in girgio chiaro). I rappresentanti sono indicati con r 0 =

Un'importante proprietà è che, non considerando la molteplicità di tali archi, i macro-vertici formano un DAG: se così non fosse, infatti, un ciclo di macro-vertici formerebbeuna componente fortemente connessa più grande, in quanto due vertici arbitrari all'in-terno di due macro-vertici distinti sarebbero comunque collegati da cammini in entram-be le direzioni, ma questo non è possibile per la massimalità delle componenti . Il DAGottenuto in questo modo a partire dall'esempio mostrato nella Figura 7.5 è rappresentatonella Figura 7.4.

La strutturazione di un grafo orientato in un DAG di macro-vertici (corrispondentia componenti fortemente connesse) è fondamentale per la comprensione dell'algoritmo

che stiamo per discutere.Un'altra utile osservazione riguarda la visita ricorsiva in profondità mostrata nel Co-

dice 7.9. Ricordiamo che la visita mantiene implicitamente, e in ordine inverso, il cam-mino ti nell'albero DFS dal nodo di partenza s al vertice u attualmente considerato nellavisita: i vertici lungo 7t sono quelli in cui la visita ricorsiva è iniziata ma non ancoraterminata. I rimanenti vertici ricadono in due tipologie, ovvero quelli la cui visita è statagià completata (quindi la chiamata ricorsiva per loro è terminata) oppure quelli che nonsono stati ancora raggiunti.

Page 288: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 288/373

 

Per individuare le componenti fortemente connesse, applichiamo un algoritmo ba-sato sulla visita ricorsiva in profondità, che si avvale anche di due ulteriori pile di vertici.Consideriamo un istante qualunque nell'esecuzione dell'algoritmo, e sia u il vertice at-tualmente raggiunto dalla visita. Adottiamo la seguente terminologia per classificare lecomponenti fortemente connesse del grafo in base allo stato dei vertici in esse contenutirispetto alla visita in corso, oltre che al cammino implicito n dal vertice di partenza s alnodo u:

• una componente è completa se la visita in tutti i suoi vertici è stata completata (talivertici vengono detti completi e la visita ricorsiva in essi è terminata);

• una componente è parziale se contiene alcuni vertici del cammino 71 (oltre a questipuò contenere gli altri due tipi di vertici, ossia quelli la cui visita è stata già com-pletata e quelli che non sono stati ancora raggiunti): sono chiamati parziali tutti i

vertici della componente tranne quelli non ancora raggiunti;

• una componente è ignota altrimenti, ovvero contiene esclusivamente vertici chenon sono stati ancora raggiunti.

L'algoritmo identifica anche dei rappresentanti durante la visita. Precisamente, un ver-tice parziale u è un rappresentante della propria componente (parziale) se è il primovertice della componente a essere visitato. Quando la componente diventa completa, ilrappresentante diventa completo come il resto dei vertici in essa e perde il suo status di

rappresentante. Di conseguenza, i rappresentanti sono nel cammino n e separano unacomponente parziale dalla successiva.

Se numeriamo tutti i vertici nell'ordine di scoperta da parte della visita (usando unarray df sNumero a tal fine), i rappresentanti compaiono in ordine crescente di numera-zione lungo il cammino 7t, come mostrato nella Figura 7.6. Durante la visita, l'algoritmomantiene due pile:

• p a r z i a l i : contiene tutti i vertici parziali inseriti in ordine di visita;• r a p p r e s e n t a n t i : contiene i vertici rappresentanti inseriti in ordine di visita.

In generale, presi i vertici contenuti nel cammino 7t, osserviamo che i vertici parziali ne

sono un sovrainsieme mentre i rappresentanti ne sono un sottoinsieme. Inoltre, i verticiappartenenti a una stessa componente parziale occupano posizioni contigue nella pilap a r z i a l i e il primo di tali vertici a essere impilato è il loro rappresentante, che vieneanche inserito in r a p p r e s e n t a n t i a tale scopo.

Queste sono le proprietà che il Codice 7.11 intende mantenere e utilizzare per l'in-dividuazione delle component i fortememente connesse. Inizialmente, nessun vertice èancora esaminato (e quindi neanche completo) e le pile sono vuote. Inoltre, usiamo l'ar-ray df sNumero sia per numerare i vertici in ordine di scoperta (attraverso c o n t a t o r e )

Page 289: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 289/373

 

ComponentiFortementeConnesse( ):FOR  (s = 0; s < n; s = s + 1) {

dfsNumero[s] = -1;

completo[s] = FALSE;

>contatore = 0;

FOR  (s = 0; s < n; s = s + 1) {

IF (dfsNumero [s] == -1) DepthFirstSearchRicorsivaEstesa( s );

>

DepthFirstSearchRicorsivaEstesa( u ):

df sNumero[u] = contatore;

contatore = contatore + 1;

parziali.Push( u );

rappresentanti.Push( u );

FOR (X = listaAdiacenza[u].inizio; x != nuli; x = x.succ) {

v = x.dato;

IF (dfsNumero [v] == -1) {

DepthFirstSearchRicorsivaEstesa( v );

> ELSE IF (!completo[v]) {

 WHILE (dfsNumero[rappresentanti.Top()] > dfsNumero[v])

rappresentanti .PopO ;

>>IF (u == rappresentanti.TopO) {

PRINT 'Nuova componente fortemente connessa:'

DO {

PRINT z = parziali .PopO ;

completo[z] = TRUE;

> WHILE (z != u);

rappresentanti .PopO ;

>

Codic e 7.11 Stampa delle componenti fortemente connesse in un grafo orientato.

Page 290: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 290/373

 

s = r0 s = r0

Figura 7.7 Situazione trattata dalle righe 10-12 di De pt hF ir st Se ar ch Ri co rs iv aE st es a (u) ,prima (a sinistra nella figura) e dopo (a destra) la loro esecuzione.

sia per stabilire se un vertice è stato visitato o meno: inizializzando gli elementi di talearray al valore — 1, il successivo assegnamento di un valore maggiore oppure uguale a 0 (aseguito della visita) ci permette di stabilire se il corrispondente vertice sia stato raggiunto

o meno. Osserviamo che, a differenza della visita in profondità, non occorre utilizzareesplicitamente l'array raggiunto.

Le righe 2-3 assegnano la numerazione di visita a u, e le successive righe 4-5 inseri-scono u in cima a entrambe le pile (in quanto potrebbe iniziare una nuova componenteparziale che prima era ignota). A questo punto, esaminiamo la lista dei vertici adiacentidi u e invochiamo ricorsivamente la visita su quei vertici non ancora raggiunti (righe 8-9). Al contrario delle visite discusse in precedenza, se un vertice v adiacente a u è statoraggiunto precedentemente (righe 10-12), occorre verificare se l'arco (u, v) contribuiscealla creazione di un ciclo. Questo è possibile solo se v non è completo (riga 10): altri-

menti (u, v) è un arco del DAG di macro-vertici di cui abbiamo discusso prima e nonpuò creare un ciclo.

Ipotizzando quindi che v non sia completo, siamo nella situazione mostrata nellaFigura 7.7, dove l'arco (u,v) chiude un ciclo di componenti parziali, che devono essereunite in una singola componente parziale. Poiché i rispettivi vertici parziali occupanoposizioni contigue nella pila p a r z i a l i , è sufficiente rimuovere i soli rappresentantidi tali componenti dalla pila r a p p r e s e n t a n t i : ne sopravvive solo uno, ovvero quellocon numerazione di visita minore, che diventa il rappresentante della nuova componente

Page 291: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 291/373

 

s = r0 s = T0

• • "

Figura 7.8 Situazione trattata dalle righe 17-20 di De pt hF ir st Se ar ch Ri co rs iv aE st es a( u) ,prima (a sinistra nella figura) e dopo (a destra) la loro esecuzione.

parziale così creata implicitamente (righe 10-12, dove la numerazione di v è usata pereliminare i rappresentanti che non sopravvivono).

Terminata la scansione della lista di adiacenza di u, e le relative chiamate ricorsive,abbiamo che u diventa completo perché non possiamo scoprire ulteriori vertici tramite

esso. Se u è anche rappresentante della propria componente (riga 15), vuol dire che ue tutti i vertici parziali che si trovano sopra di esso in p a r z i a l i formano una nuovacomponente completa, come illustrato nella Figura 7.8. E sufficiente, quindi, estrarreciascuno di tali vertici dalla pila p a r z i a l i , marcarlo come co mple t o (righe 18—19) edeliminare u dalla pila r a p p r e s e n t a n t i (riga 21): notiamo che il corpo del ciclo allerighe 17-20 viene eseguito prima della guardia che, se non verificata, fa uscire dal ciclo.Inoltre, ogni vertice viene inserito ed estratto una sola volta al più in ciascuna pila, percui il costo asintotico rimane quello della visita ricorsiva, ovvero 0 ( n + m) tempo e O(n)celle di memoria.

A L V I E : componenti (fortemente) connesse di un grafo

Osserva, sperimenta e verifica

Conne etedComponent

Page 292: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 292/373

 

RIEPILOGO

 In questo capitolo abbiamo esaminato le pile e le code, mostrando come applicarle alla valu-tazione di notazioni polacche e a diversi problemi su grafi: visite in ampiezza e profondità,verifica della ciclicità o meno di un grafo, ordinamento topologico di un grafo aciclico,calcolo delle componenti (fortemente) connesse.

ESERCIZI

1. Scrivete il codice per realizzare la conversione e l'interprete per le espressionipolacche. Usate un spazio bianco per separare variabili e costanti.

2. Scrivete il codice per implementare una coda mediante una lista, secondo quantodescritto nel Paragrafo 7.3.2.

3. Modificate la tabella di conversione da un'espressione infissa a una postfissa (Fi-gura 7.1) in modo da riconoscere quando l'espressione infissa fornita in ingresso èsintatticamente incorretta (ad esempio, A + xB).

4. Modificate il codice per costruire l'albero BFS come albero ordinale rappresentatocon memorizzazione binarizzata.

5. Usate la visita DFS per mostrare che un grafo non orientato con grado min imo dammette un cammino di lunghezza maggiore di d.

6. Ricordiamo che il grafo a torneo è un grafo orientato G in cui per ogni coppiadi vertici x e y esiste un solo arco che li collega, (x,y) oppure (y,x), ma nonentrambi. L'interpretazione è che nella parti ta del torneo tra x e y uno dei dueha vinto. Prendete il DAG risultante dalle componenti fortemente connesse (chesono i macro-vertici) e mostrate che l'ordinamento topologico del DAG induceuna classifica dei partecipanti al torneo.

Page 293: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 293/373

 

Capitolo 8

Code con priorità

SOMMARIO  In questo capitolo illustriamo e analizziamo la struttura di dati denominata codacon prio-rità, che gestisce sequenze di inserimenti ed estrazioni di elementi da un insieme, in presenzadi pesi associati a tali elementi, pesi che ne determinano l'ordine di estrazione dall'insiemestesso. Di tale struttura di dati presentiamo l'implementazione più diffusa, lo heap, mo-strandone poi l'utilizzo in contesti informatici diversi, quali l'ordinamento, la ricerca dicammini minimi su grafi pesati e l'individuazione di alberi ricoprenti di peso minimo(sempre su grafi pesati).

DIFFICOLTÀ

1,5 CFU

8.1 Code con prioritàLa struttura di dati coda con priorità memorizza una collezione di elementi in cui aogni elemento è associato un valore, detto peso, appartenente a un insieme totalmenteordinato (solitamente l'insieme degli interi positivi). La coda con priorità può essere vistacome un'estensione della coda (Paragrafo 7.3) e, infatti, le operazioni disponibili sono lestesse della coda: Empty, Enqu eue, F i r s t e Dequ eue. L'unica e sostanziale differenzarispetto a tale tipo di dati è che le ultime due operazioni devono restituire (ed estrarre,nel caso della Dequeue) l'elemento di peso minimo nella coda con priorità. 1

Ai fini della discussione, ipotizziamo che ciascun elemento e memorizzato nella codacon priorità contenga due campi, ossia un campo e.peso per indicarne il peso e uncampo e. dato per indicare i dati a cui associare quel peso.

'Nel seguito, considereremo il caso in cui la coda con priorità restituisce sempre il minimo, ma le stesseconsiderazioni possono essere applicate mutatis mutandis al caso in cui dobbiamo restituire il massimo.

Page 294: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 294/373

 

La necessità di estrarre gli elementi dalla coda in funzione del loro peso, ne rendel'implementazione più complessa rispetto a quella della semplice coda. Per convincerci

di ciò consideriamo la semplice implementazione di una coda con priorità mediante unalista dei suoi elementi.

Nel caso in cui decidiamo di implementare in tempo costante l'operazione Enqueue,inserendo i nuovi elementi in corrispondenza a un estremo della lista, ne deriva chela lista non è ordinata: per l'implementazione di De queu e e di F i r s t è necessarioindividuare l'elemento di peso minimo all'interno della lista e, quindi, tale operazionerichiede tempo O(n), dove n è la lunghezza della lista. Ad esempio, facendo riferimentoalla parte alta della Figura 8.1, l'inserimento nella lista di un nuovo elemento avvieneponendolo subito prima del primo (con peso pari a 17), mentre la Dequeue richiede la

scansione dell'intera lista, per determinare che l'elemento minimo è quello con peso paria 3 e per poi estrarlo.

Se decidiamo, invece, di mantenere gli elementi della lista ordinati rispetto al loropeso (ad esempio, in ordine non decrescente), ne deriva che De qu eue e F i r s t richie-dono 0(1) tempo, in quanto l'elemento di peso minimo è sempre il primo della lista. Altempo stesso, però, in corrispondenza a ogni Enqueue dobbiamo utilizzare tempo 0(n)per inserire il nuovo elemento nella giusta posizione della lista, corrispondente all'or-dinamento degli elementi nella lista stessa. Ad esempio, facendo riferimento alla partebassa della Figura 8.1, osserviamo che l'operazione Dequeue richiede soltanto di elimi-

nare il primo elemento della lista (ovvero, quello con pes^ pari a 3). L'esecuzione dellaEnqueue, ad esempio di un elemento con peso pari a 16, richiede però la scansione diuna parte della lista per determinare che l'elemento va inserito tra gli elementi con pesipari a 15 e 17: nel caso peggiore, la scansione avviene sull'intera lista.

Facendo uso di soluzioni più sofisticate è possibile implementare una coda con prio-rità in modo più efficiente, in tempo O(logn), attraverso un bilanciamento del costo diesecuzione delle operazioni Dequeue e Enqueue, ottenuto per mezzo di un'organizza-zione dell'insieme degli elementi in cui questi siano ordinati solo in parte.

Page 295: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 295/373

 

Figura 8 .2 Heaptree (a sinistra) e albero che non soddisfa la proprietà di heap (a destra).

8.2 Heap

Uno heap è una struttura di dati che, attraverso una rappresentazione solo parzialmenteordinata dei suoi elementi, permette di ottenere un tempo logaritmico per le operazioniEnqueue e Dequeue e un tempo costante per le operazioni Empty e F i r s t .

Lo heap presenta la caratteristica di essere un albero binario completo a sinistra, dicui possiamo quindi dare una rappresentazione implicita (Paragrafo 4.3.1). Almeno ini-zialmente, comunque, descriveremo la sua struttura facendo riferimento a un'organizza-zione esplicita ad albero, detta heaptree, degli elementi stessi: mostreremo poi come taleorganizzazione possa essere riportata in termini della rappresentazione implicita. Un o

heaptree è un albero H che soddisfa la seguente proprietà di heap (minimo):

1. il peso dell 'elemento contenuto nella radice di H è minore o uguale di quelli con-

tenuti nei figli della radice; più precisamente, se r è la radice di H e Vo, Vi,..., v^-j

sono i suoi figli, allora r.peso ^ vt.peso, per 0 ^ i < k.;

2. l'albero radicato in Vi è uno heaptree per 0 ^ i < k (gli alberi vuoti sono heaptree).

L'albero nella parte sinistra della Figura 8.2 è uno heaptree, a differenza di quello

nella parte destra in cui, ad esempio, il nodo con peso 18 è sopra al nodo con peso 14.

Come corollario immediato, abbiamo che la radice di uno heaptree contiene l'e-lemento di peso mi ni mo dell'insieme. Di conseguenza, l'effettuazione dell'operazione

F i r s t nel caso di una coda con priorità implemen tata con uno heaptree richiede 0 (1 )

tempo.

Uno heap è uno heaptree con i vincoli aggiuntivi di essere binario e completo a si-

nistra: ovvero, se h. è la sua altezza, i nodi di profondità minore di h. formano un albero

completamen te bilanciato e quelli di profond ità h. sono tut ti accumulati a sinistra. L'al-

bero nella parte sinistra della Figura 8.3 è uno heap, mentre quello nella parte destra,

Page 296: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 296/373

 

Figura 8.3 Heap (a sinistra) e heaptree che non e uno heap (a destra).

pur essendo uno heaptree, non è uno heap in quanto non è completo a sinistra. NelParagrafo 4.3.1 abbiamo visto che un albero completo a sinistra con n nodi ha altez-za pari a h. = O( logn ): ciò ci consente di effettuare in tempo O(logn) le operazioniEnqueue e Dequeue, di cui forniamo uno schema algoritmico generale mostrando poicome realizzare tali algoritmi nel contesto specifico della rappresentazione implicita.

L'effettuazione dell'operazione Enqueue di un elemento e in uno heap H prevede,ad alto livello, l'esecuzione dei seguenti passi.

1. Inseriamo un nuovo nodo v contenente e come foglia di H in modo da mantenere

H completo a sinistra.

2. Iterativamente, confront iamo il peso di e con quello dell'elemento f contenutonel padre di v e, se e .p eso < f .peso, i due nodi vengono scambiati: l'iterazionetermina quando v diventa la radice oppure quando e. pe so ^ f .pe so (notiamoche in questo modo manteniamo la proprietà di uno heaptree).

Come possiamo vedere, l'operazione Enqueue opera inserendo un elemento nellasola posizione che consente di preservare la completezza a sinistra dello heap, facendopoi risalire l'elemento nello heap, di figlio in padre, fino a trovare una posizione che

soddisfi la proprietà di uno heaptree.L'operazione En qu eu e implementata su uno heap richiede tempo O(logn ): a tal

fine, ci basta osservare che il numero di passi effettuati è proporzionale al numero dielementi con i quali e viene confrontato e che tale numero è al massimo pari all'altezzah. = O(logn) dello heap (in quanto e viene confrontato con al più un elemento per ognilivello dello heap).

Passiamo a considerare ora l'operazione Dequeue, che può essere effettuata, sempread alto livello, su uno heap H nel modo seguente.

Page 297: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 297/373

 

1. Estraiamo la radice di H in modo da restituire l'elemento in essa contenuto allafine dell'operazione.

2. Rimuoviamo l'ultima foglia di H (quella più a destra nell 'ul timo livello), per inse-rirla come radice v (al posto di quella estratta), al fine di mantenere H completo a

sinistra.

3. Iterativamente, conf ron tiamo il peso dell'elemento in v con quelli degli elementinei suoi figli e, se il minimo fra i tre pesi non è quello di v, il nodo v vienescambiato con il figlio contenente l'elemento di peso minimo: l'iterazione terminase v diventa una foglia o se contiene un elemento il cui peso è minore di quellidegli elementi contenuti nei suoi figli.

Al contrario dell'operazione Enqueue, l'operazione Dequeue opera facendo scende-

re un elemento, impropriamente posto come radice, all'interno dello heap, fino a soddi-sfare la proprietà di uno heaptree. Applicando le stesse considerazioni effettuate nel casodell'operazione En qu eu e, possiamo verificare che l'operazione Dequeue implementatasu uno h^ap richiede anch'essa tempo O(logn).

Notiamo che invertendo l'ordine dei confronti effettuati possiamo gestire uno heapH che soddisfa la proprietà di massimo nei suoi nodi (anziché di minimo), in cui ilmassimo è nella radice.

8.2.1 Implementazione di uno heap implicito

Come dichiarato sopra, uno heap è un albero completo a sinistra e, per quanto osservatonel Paragrafo 4.3.1, gode dell'interessante proprietà di poter essere rappresentato in mo-do implicito per mezzo di un array, che indichiamo con heapArray, senza fare uso diriferimenti espliciti tra i nodi. Ricordiamo che i nodi dello heap H corr ispondono aglielementi di heapArray come segue:

1. la radice di H corrisponde all'elemento heapArray[0];

2. se un nodo v di H corrisponde all'elemento heapArray[i], allora il figlio sini-stro corrisponde all'elemento heapArray[2i + 1], il figlio destro corrisponde ahe ap Ar ra y[ 2i + 2] e il padre corrisponde a he ap Ar ra y[ (i — 1 )/2].

Come effetto di questa rappresentazione, se H ha n nodi, i corrispondenti elementisono memorizzati in ordine di ampiezza nelle prime n posizioni di heapArray ed èpossibile attraversare l'albero da padre a figlio o viceversa, in tempo costante.

Dato che gli elementi dello heap occupano la parte iniziale di heapAr ray , nell'im-plementazione è necessario prevedere anche una variabile intera heapSize, che indichi

Page 298: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 298/373

 

Empty( ):

RETURN heapSize == 0;

First( ):

IF (!Empty( )) RETURN heapArray[0];

Enqueue( e ):

VerificaRaddoppio( );

heapArray[heapSize] = e;

heapSize = heapSize + 1 ;

RiorganizzaHeapC heapSize - 1 );

Dequeue( ):

IF (!Empty( )) {

minimo = heapArray[0];

heapArray[0] = heapArray[heapSize - 1];

heapSize = heapSize - 1;RiorganizzaHeapC 0 );

VerificaDimezzamento( );

RETURN minimo;

}

Codice 8.1 Implementazione di uno heap mediante un array: le funzioni Verif icaRaddoppio eVerificaDimezzamento seguono l'approccio del Paragrafo 2.1.3.

il numero di elementi attualmente presenti nello heap: in altre parole, heapSize èl'indice della prima posizione libera di heapArray.

Consideriamo ora l'implementazione, riportata nel Codice 8.1, delle quattro ope-

razioni forn ite da una coda con priorità. Possiamo osservare, per prima cosa, che la

funzio ne Empty restituisce il valore t r u e se e solo se h e a p S i z e è uguale a 0 mentre,

se lo heap non è vuoto, la funzi one F i r s t restituisce il primo elemento di h e a p A r r a y

che, per la rappresentazione implicita sopra illustrata, corrisponde alla radice dello heap.

L'operazione Enqueue verifica se l'array debba essere raddoppiato (riga 2): succes-

sivamente, inserisce il nuovo elemento nella prima posizione libera dell'array e, quindi,come foglia dello heap per mantenerne la completezza a sinistra (riga 3).

Do po aver aggiornato il valore di h e a p S i z e (riga 4), per mantenere la proprietà di

uno heaptree viene invocata la funzio ne R i o r g a n i z z a H e a p (riga 5), di cui posponiamo

la discussione.

Se lo heap contiene almeno un elemento, l'operazione Dequeue determina quello

con il minimo peso, ovvero quello che si trova nella posizione iniziale dell'array (riga 3):

quindi, per mantenere la completezza a sinistra, copia nella prima posizione l'elemento

Page 299: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 299/373

 

RiorganizzaHeap ( i ): {pre: heapArray è uno heap tranne che nella posizione i) WHILE (i>0 kk  heapArray[i].peso < heapArray[Padre(i)].peso) {

Scambiai i, Padre( i ) );

i = Padre( i );

> WHILE (Sinistro(i) < heapSize kk  i != MinimoPadreFigii(i)) {

figlio = MinimoPadreFigli( i );

Scambia( i, figlio );

i = figlio;

>

MinimoPadreFigli ( i ) : {pre: il nodo in posizione i ha almeno un figlio)j = k = Sinistro(i);

IF (k+1 < heapSize) k = k+1;IF (heapArray[k].peso < heapArray[j].peso) j = k;

IF (heapArray[i].peso < heapArraytj].peso) j = i;RETURN J;

Pa dr e( 1 ): Scambiai i, j ):RETURN (i-l)/2; tmp=heapArray[i];

Sinistro( i ): heapArray[i]=heapArray[j];

RETURN 2 x i + 1; heapArray[j]=tmp;

Cod ice 8.2 Riorganizzazione di uno heap per mantenere la proprietà di uno heaptree.

che si trova nella posizione finale e che corrisponde all'ultima foglia dello heap, e aggiornail valore di h e a p S i z e (righe 4 e 5).

Infine, per mantenere la proprietà di uno heaptree, invoca RiorganizzaHeap everifica se l'array debba essere dimezzato (righe 6 e 7).

La funzione RiorganizzaHeap, riportata nel Codice 8.2, ripristina la proprietàdi uno heaptree in un albero completo a sinistra e rappresentato in modo implicito,con l'ipotesi che l'elemento e = heapArray[i] sia eventualmente l'unico a violare laproprietà di heaptree.

Il primo ciclo viene eseguito quando e deve risalire lo heap perché e.peso è minoredel peso di suo padre: dobbiamo quindi scambiare e con il padre e iterare (righe 2-5).

Il secondo ciclo viene eseguito quando e deve scendere perché e.peso è maggioredel peso di almeno uno dei suoi figli: dobbiamo quindi scambiare e con il figlio aventepeso minimo, in modo da far risalire tale figlio e preservare la proprietà di uno heaptree(righe 6—10). Tale eventualità è segnalata nella riga 6 dal fatto che M i n i m o P a d r e F i g l inon restituisce i come posizione dell'elemento di peso minimo tra e e i suoi figli (que-

Page 300: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 300/373

 

sto vuol dire che un figlio ha peso strettamente minore poiché, a parità di peso, vienerestituito i).

Il codice relativo a M i n i m o P a d r e F i g l i tiene conto dei vari casi al conto rno che sipossono presentare (per esempio, che e abbia un solo figlio, il quale deve essere sinistro edeve essere l'ultima foglia).

Notia mo che, per ogni posizione i nello heap e ogni elemento e = he ap Ar ra y[ i] ,non pu ò mai accadere che vengano eseguiti entrambi i cicli di R i o r g a n i z z a H e a p : inaltre parole, e sale con il primo ciclo o scende con il secondo, oppure è già al posto giustoe, quindi, nessuno dei cicli viene eseguito.

Ne deriva che il costo di RiorganizzaHeap è proporzionale all'altezza dello heape, quindi, richiede O(logn) tempo.

A L V I E : operazioni su uno heap implicito

La complessità di O(logn) tempo delle operazioni sulla coda con priorità heap so-no determinate dal costo logaritmico di RiorganizzaHeap e dal costo ammortizzatocostante per operazione di Ve ri f ic aR ad do pp io e V er if icaDi mez zam en to: come

sempre, nel caso in cui utilizziamo un array per rappresentare un insieme di elementivarianti nel tempo, prevediamo che la sua dimensione possa variare quando necessario,e in particolare possa essere raddoppiata o dimezzata secondo quanto già discusso nelParagrafo 2.1.3 (se l'array ha dimensione prefissata, i costi finali sono al caso pessimo).

8.2.2 Insolito caso di DecreaseKey

Un'operazione molto utile negli heap è quella denominata DecreaseKey, che permettedi diminuire il peso di un elemento memorizzato nello heap. Tuttavia, localizzare taleelemento all'interno di heapArray richiede O(n) tempo senza una struttura di dati au-

siliare. A tal fine, ipotizziamo che, presi gli n elementi in heapArray, i loro campi datosiano distinti e formino una permutazione degli interi (0,1,... ,n — 1} (tale situazioneoccorre nel Paragrafo 8.3.2 dove utilizziamo DecreaseKey).

Possiamo quindi introdurre un array posizioneHeapArray di n interi che rap-presentano le posizioni occupate in heapArray dagli elementi memorizzati nello heap,ossia vale po s iz io ne Hea pA rra y[ he ap Arr ay[ i ] . da to ] = i per 0 ^ i < he ap Si ze .L'adozione di questo array richiede ulteriori O(n) celle di memorie e quindi Io heap ri-sultante non è più implicito (e non è conosciuta una semplice soluzione per renderlo

Osserva, sperimenta e verifica

HeapArray

CD

Page 301: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 301/373

 

DecreaseKeyC dato, peso ):

i = posizioneHeapArray[dato];

heapArray[i].peso = peso;

RiorganizzaHeap( i );

Enqueue( e ):VerificaRaddoppioC );

heapArray[heapSize] = e;

posizioneHeapArray[ e.dato ] = heapSize;

heapSize = heapSize + 1 ;

RiorganizzaHeapC heapSize - 1 );

Scambiai i, j ):

tmp=heapArray[i];

heapArray[i]=heapArray[j];

heapArray[j]=tmp;

posizioneHeapArray[ heapArray[i].dato ] = i;

posizioneHeapArray[ heapArray[j].dato ] = j;

Codice 8.3 Riorganizzazione di uno heap per l'operazione DecreaseKey, che richiede l'introdu-zione dell'array posizioneHeapArray, della riga 4 in Enqueue e delle righe 5 e 6 inScambia.

implicito), ma comunque tale heap rimane una struttura di dati semplice ed efficiente,

in quanto fornisce l'operazione DecreaseKey in tempo O(logn).Per mantenere le informazioni aggiornate in posizioneHeapArray, occorre mo-

dificare le operazioni Enq ue ue e Sca mb ia come riportato nel Codice 8.3. In par-

ticolare, registriamo la posizione di ogni nuovo elemento messo in coda (riga 4 in

Enqueue) e scambiamo le posizioni analogamente a quanto succede per heapArray

(righe 5 e 6 in Sc am bi a) : notiamo che la complessità di tali operazioni non cambia

asintoticamente. Infine, l'operazione De c r e a s e Ke y accede all'elemento nella posizio-

ne indicata da p o s i z i o n e H e a p A r r a y , diminuisce il peso di tale elemento e applica

RiorganizzaHeap per preservare la proprietà di heap, in costo O(logn) utilizzando

l'analisi discussa in precedenza.

8.2.3 Costruzione di heap e ordinamento

Proviamo ora a considerare il costo di costruzione di uno heap da un insieme dato din elementi, inizialmente posti in heapArray stesso: la soluzione immediata in tal casocomporta l'esecuzione di n operazioni Enqueue su uno heap inizialmente vuoto, comemostrato nel Codice 8.4. No tiamo che tale algoritmo di costruzione opera in loco e,

Page 302: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 302/373

 

CreaHeapO :

heapSize = 0;

FOR  (i = 0; i < n; i = i+1) {

Enqueue( heapArray[i] );

>

Codice 8.4 Costruzione di uno heap mediante Enqueue.

inoltre, quando Enqueue deve inserire l'elemento heapArray[i], le precedenti posizioniheapArray[0 , i — 1] formano uno heap con heapSize = i, per i > 0. In conseguenzadi ciò, al termine dell'ultima iterazione, l'intero array rappresenta uno heap. Quale saràil costo complessivo della costruzione? Da to il costo logaritmico di Enq ue ue , avremo

che il costo di costruzione sarà O( n l og n ) nel caso peggiore.Tale costo non è migliorabile asintoticamente a causa dell'operazione Enqueue, da-to che il numero di confronti effettuati da essa su uno heap di k elementi è almeno paria logk nel caso peggiore. In tal caso, il Codice 8.4 totalizza un numero di confron-ti limitato infe rior mente da logk = l°gn!> s u c u ' possiamo applicare la doppiadisuguaglianza dell'approssimazione di Stirling, in base alla quale abbiamo che

V^n7tn ne"n+TiiiTT < n ! < \ / 2 n n n n e ~ n + ^ (8.1)

Di conseguenza, il numero di confronti eseguiti da n operazioni di Enqueue nel casopeggiore sarà almeno pari a

log(n!) > ^ log2n7i + nlogn — ^ n - y ^ + l " ) l o 8 e = n ( n l o g n )

Il motivo intuitivo di tale fenomeno è che, poiché tutti gli elementi salgono lungo loheap, eventualmente fino alla radice, e poiché il numero di elementi su un livello cresce(in particolare raddoppia) a ogni livello, ne deriva che, eseguendo il Codice 8.4, moltielementi percorrono, nel caso peggiore, un cammino "lungo" fino alla radice.

Sarebbe più conveniente, se la costruzione dello heap potesse avvenire spostando glielementi verso il basso, non verso la radice ma verso le foglie: in questo caso, infatti,la maggior parte degli elementi, che tende già a trovarsi vicino alle foglie, percorrereb-be un cammino "breve". Mos tria mo come tale intuiz ione conduca a un algoritmo dicostruzione dello heap che richiede soltanto O(n ) tempo.

Il Codice 8.5 opera in tal modo, utilizzando RiorganizzaHeap, in una versio-ne ristretta al solo secondo ciclo (righe 6—10), indicata con R i o r g a n i z z a H e a p F i g l iperché procede esclusivamente verso i figli. Le iterazioni del Codice 8.5 scandiscono

Page 303: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 303/373

 

CreaHeapMigliorato( ):

heapSize = n;

FOR  (i = N-1; i >=0; i = i - 1) {

RiorganizzaHeapFigli(i);

>

Codice 8.5 Costruzione di uno heap mediante R io rg an iz za He ap Fi gl i.

heapArray all'indietro e, in effetti, la prima metà circa di tali iterazioni sono inutiliperché non modificano tale array in quanto sono invocate sulle foglie dello heap.

Le rimanenti operano sui nodi interni e, per ciascun nodo heapArray[i], tale nodoe i suoi discendenti formano uno heap tranne che nella posizione i, per cui vengonoriposizionati da R i o r g a n i z z a H e a p F i g l i : a ogni istante, la parte finale dell'array rap-presenta un insieme di heap che, man mano, vengono uniti insieme fino a ottenere ununico heap nell'intero array. Notiamo che anche questo algoritmo di costruzione delloheap è in loco.

A L V I E : costruzione di uno heap

J h f e j ^ Osserva, sperimenta e verifica ® ® o ©

^ ¡ I p ^ MakeHeap

Proviamo ora a convincerci che, in effetti, l'array risultante dall'applicazione delCodice 8.5 rappresenta un o heap. A tal fine, not iam o che al ter mine dell'i terazione idell'algoritmo gli elementi in heapArray[i, n — 1] verificano la proprietà di heaptree,ossia he ap Ar ra y[ (j — l) /2 ] < he ap Ar ra y[ j] per 2i + 1 < j < TI: in conseguen-za di ciò, per le posizioni i dei nodi interni (dove i < (n — 2)/2), possiamo vedere cheheapArray[i , n— 1] codifica un insieme di i+1 heap, ciascuno radicato in una posizionedistinta di heapArray[i, 2i].

Supponiamo induttivamente che la proprietà suddetta sia verificata per l'iterazio-n e i + 1 e che, quindi, le posizioni delle radici siano quelle nell'intervallo [i+ l,2(i+ 1)]:durante l'iterazione i, RiorganizzaHeap considera l'elemento heapArray[i] e i dueheap aventi radici in heapArray[2i + 1] e heapArray[2i + 2], al fine di ottenere ununico heap con radice in heapArray [i].

Quindi, le radici nelle posizioni 2i + 1 e 2i + 2 sono assorbite nel nuovo heap chesoddisfa la proprietà di heaptree, e la posizione i diventa una nuova radice, trasformandocosì l'intervallo delle radici da [Ì+ 1,2(i+ 1)] a [i, 2i]. Al termine dell'algoritmo, quando

Page 304: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 304/373

 

HeapSort( a ) : {pre: la lunghezza di a èn)CreaHeapMigliore( );

FOR (heapSize = n-1; heapSize > 0; heapSize = heapSize - 1) {

Scambiai heapSize, 0 );

RiorganizzaHeap( 0 );

>

Codice 8.6 Ordinamento mediante heap di un array a.

i = 0, la proprietà di heaptree è verificata a partire dalla radice heapArray[0] e l'intero

array forma uno heap.

Mostriamo ora che l'intera riorganizzazione dell'albero richiede un numero di con-

fronti lineare nel numero di elementi. A tal fine, osserviamo che il numero di confronticomplessivamente eseguiti è, nel caso peggiore, proporzionale alla somma delle altezze di

tutti i sottoalberi.

Dato l'albero completo a sinistra corrispondente allo heap di altezza h., consideriamo

il suo completamento nell'ultimo livello di foglie in modo da ottenere concettualmente

un albero completo di altezza h (se non lo è già): possiamo constatare che il Codice 8.5

non può impiegare minor tempo al caso pessimo su quest'ultimo albero.

Precisamente, esso effettua n/2 iterazioni sulle foglie, n/4 iterazioni sui padri del-

le foglie, n/8 iterazioni sui nonni e così via: in generale, effettua n/2' + 1 iterazioni sui

nodi di altezza pari a j, per 0 ^ j ^ h., e ciascuna iterazione richiede temp o propor-zionale all'altezza stessa j. Ne deriva che il costo totale dell'algoritmo di costruzione è

pari x t i /2 ' + 1), che è limitata superiormente da 2Ij>i(j x n/2') = O(n), dove

La costruzione dello heap può essere impiegata per ordinare un array di elementi. In-fatti, l'utilizzo di una coda con priorità fornisce un semplice algoritmo di ordinamento:per ordinare un array di n elementi avendo a disposizione una coda con priorità PQ è suf-ficiente dapprima inserire gli elementi in PQ, effe ttuando n operazioni Enqu eu e, e quin-di estrarli uno dopo l'altro, mediante n operazioni Dequeue. In tal modo, l'ordinamento

richiede tempo O(nlogn).Tuttavia, questo costo può essere ridotto, anche se non in termini asintotici, usando

uno heap con la proprietà di heaptree massimo (invece che minimo) e applicando ilmetodo di costruzione dello heap illustrato nel Codice 8.5 che, come visto, richiedetempo O(n) per creare lo heap a partire dall'array: ciò fornisce un costo complessivo di0 ( n + n l o g n ) per l'algoritmo, che comu nqu e asintoticamente è ancora O( nl og n) . Ilvantaggio dell'utilizzo di uno heap è che l'o rdi namento viene effettuato in loco. Co mepossiamo infatti vedere nel Codice 8.6, l'algoritmo cosiddetto di Heapsort opera, dato

Page 305: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 305/373

 

heapArray da ordinare, prima riposizionando gli elementi di heapArray in modo taleda costruire uno heap, e poi eliminando iterativamente il massimo elemento nello heap(in posizione 0), la cui dimensione è decrementata di 1, costruendo al tempo stesso lasequenza ordinata degli elementi di heapArray a partire dal fondo.

A L V I E : ordinamento mediante heap

Osserva, speri men ta e veri fica © ® ® © ° ©

HeapSort

In particolare, all'iterazione i, l'elemento massimo estratto dallo heap (che a quelpunto ha dimensione i ed è rappresentato dagli elementi in heapArray[0, i — 1]) viene

inserito nella posizione i di heapAr ray . AI termine di tale iterazione, abbiamo che glielementi in heapArray[0, i— 1] formano uno heap e sono tutti di peso minore o ugualea quelli ordinati in heapArray[i, n — 1]: quando i = 0, risultano tutti in ordine.

8.3 Opus libri: routing su Internet e cammini minimi

Le reti di computer, di cui Internet è il più importante esempio, forniscono canali dicomunicazione tra i singoli nodi, che rendono possibile lo scambio di informazioni tra inodi stessi, e con esso l'interazione e la cooperazione tra computer situati a grande distan-

za l'uno dall'altro. Il Web e la posta elettronica sono, in questo senso, due applicazioni digrandissima diffusione che si avvalgono proprio di questa possibilità di comunicazionetra computer diversi.

L'interazione di computer attraverso Internet avviene mediante scambio di infor-mazioni suddivise in pacchetti: anche se la quantità d' informazione da passare è moltoelevata, come ad esempio nel caso di trasmissione di documenti video, tale informazio-ne viene suddivisa in pacchetti di dimensione fissa, che sono poi inviati in sequenza dalcomputer mittente al destinatario. Dato che i computer mittente e destinatario non sonodirettamente collegati, tale trasmissione non coinvolge soltanto loro, ma anche un ulte-

riore insieme di nodi della rete, che vengono attraversati dai pacchetti nel loro percorsoverso la destinazione.

Il protocollo IP (Internet Protocol), che è il responsabile del recapito dei pacchetti aldestinatario, opera secondo un meccanismo di packet switching, in accordo al quale ognipacchetto viene trasmesso in modo indipendente da tutti gli altri nella sequenza. In que-sto senso, lo stesso pacchetto può attraversare percorsi diversi in dipendenza di mutatecondizioni della rete, quali ad esempio sopravvenuti malfunzionamenti o sovraccarico ditraffico in determinati nodi.

Page 306: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 306/373

 

In quest'ambito, è necessario che nella rete siano presenti alcuni meccanismi checonsentano, a ogni nodo a cui è pervenuto un pacchetto diretto a qualche altro nodo, diindividuare una "direzione" verso cui indirizzare il pacchetto per avvicinarlo al relativodestinatario: nello specifico, se un nodo ha d altri nodi a esso collegati direttamente, ilnodo invia il pacchet to a quello adiacente più vicino al destinatario. Ques to problema,noto come instradamento (routing) dei messaggi viene risolto su Internet nel modo se-guente:2 nella rete è presente un'infrastruttura composta da una quantità di computer(nodi) specializzati per svolgere la funzione di instradamento; ognuno di tali nodi, dettirouter , è collegato direttamente a un insieme di altri, oltre che a numerosi nodi "sem-plici" che fanno riferimento a esso e che svolgono il ruolo di mittenti e destinatari finalidelle comunicazioni.

Ogni router mantiene in memoria una struttura di dati, che di solito è una tabella,detta tabella di routing, rappresentata in una forma compressa per limitarne la dimen-sione, che permette di associare a ogni nodo sulla rete, identificato in modo univoco dalcorrispondente indirizzo IP, a 32 o 128 bit (a seconda che sia utilizzato IPv4 o IPv6),uno dei router a esso direttamente collegati.

L'instradamento di un pacchetto p da un nodo u a un nodo v di Internet viene alloraeseguito nel modo seguente: p contiene, oltre all'informazione da inviare a v (il carico delpacchetto), un'intestazione (header ) che contiene informazioni utili per il suo recapito; lapiù importante di tali informazioni è l'indirizzo IP di v. Il mittente u invia p al propriorouter di riferimento ri, il quale, esaminando lo header di p, determina se v sia un nodocon cui ha un collegamento diretto: se è cosi, t] recapita il pacchetto al destinatario,mentre, in caso contrario, determina, esaminando la propria tabella di routing, qualesia il router t2 a esso collegato cui passare il pacchetto . Questa medesima operazioneviene svolta da T2, e così via, fino a quando non viene raggiunto il router r t direttamenteconnesso a v, che trasmette il pacchetto al destinatario.

Il percorso seguito da un pacchetto è quindi determinato dalle tabelle di routing deirouter nella rete, e in particolare dai router attraversati dal pacchetto stesso. Per renderepiù efficiente la trasmissione del messaggio, e in generale, l'utilizzo complessivo dellarete, tali tabelle devono instradare il messaggio lungo il percorso più efficiente (o, in altritermini, meno costoso) che collega u a v. Le caratteristiche di un collegamento diretto

tra due router (come la velocità di trasmissione), così come la quantità di traffico (adesempio, pacchetti per secondo) che viaggia su di esso, consentono, a ogni istante, diassegnare un costo alla trasmissione di un pacchetto sul collegamento.

Un'assegnazione di costi a tutti i collegamenti tra router consente di modellare l'in-frastruttura dei router come un grafo orientato pesato sugli archi, i cui nodi rappresen-tano i router, gli archi i collegamenti diretti tra router e il peso associato a un arco il

2A1 fine di rendere più agevole la comprensione degli aspetti rilevanti per le finalità di questo libro,stiamo volutamente semplificando l'esposizione dei meccanismi di routing su Internet.

Page 307: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 307/373

 

costo di trasmissione di un pacchetto sul relativo collegamento. In tal modo, l'obiettivodi effettuare il routing di un pacchetto nel modo più efficiente si riduce nel cercare, datoil grafo pesato che modella la rete, il cammino di costo minimo dal router associato a ua quello associato a v.

I router utilizzano dei protocolli appositi per raccogliere le informazioni sui costi ditutti i collegamenti, e per costruire quindi le tabelle di routing che instradano i messaggilungo i percosi più efficienti. Tali protocolli rientrano in due tipologie: link state edistance vector.

I protocolli link state, come ad esempio OSPF (Open Shortest Path First), operano inmodo tale che tutti i router, scambiandosi opportuni messaggi, acquisiscono l'intero statodella rete, e quindi tutto il grafo pesato che modella la rete stessa. A questo punto ognirouter Ti applica su tale grafo un algoritmo di ricerca, come l'algoritmo di Dijkstra cheesamineremo di seguito, per determinare l'insieme dei cammini minimi da r a qualunque

altro router: se ti , t j , . . . , rs è il cammino minimo individuato da r^ che passa attraversoil router Tj a lui vicino, la tabella di routing di r^ associa il router Tj a tutti gli indirizziverso r s .

I protocolli distance vector, come ad esempio RIP (  Routing Information Protocol)ef-fettuano la determinazione dei cammini minimi senza scambiare l'intero grafo tra i rou-ter. Essi invece applicano un diverso algoritmo per la ricerca dei cammini minimi da unnodo a tutti gli altri, l'algoritmo di Bellman-Ford, che esamineremo anch'esso nel segui-to: tale algoritmo, come vedremo, ha la peculiarità di operare mediante aggiornamenticontinui operati in corrispondenza agli archi del grafo e, per tale caratteristica, si presta

a un'elaborazione "collettiva" dei cammini minimi da parte di tutti i router nella rete.

8.3.1 Problema della ricerca di cammini minimi su grafi

La ricerca del cammino più corto {shortest path) tra due nodi in un grafo rappresentaun'operazione fondamentale su questo tipo di struttura, con importanti applicazioni.In generale, come osservato sopra, la conoscenza dei cammini minimi tra i nodi è unimportante elemento in tutti i metodi di routing su rete, vale a dire in tutti i metodiche, dati una origine e una destinazione di un messaggio, determinano il percorso più

conveniente da seguire per il messaggio stesso. Tra questi, particolare importanza rivestel'instradamento di pacchetti su Internet, come visto sopra, ma anche altre applicazionidi larga diffusione, quali ad esempio la ricerca del miglior percorso stradale verso unadestinazione effettuata da un navigatore satellitare o da sistemi disponibili su Internet elargamente utilizzati quali MapQuest, GoogleMaps, YahooMaps o MSNMaps operanosulla base di algoritmi per la ricerca di cammini minimi.

Come già visto nel Paragrafo 7.4, in un grafo non pesato il cammino minimo tra duenodi u e v può essere trovato mediante una visita in ampiezza a partire da u, utilizzando

Page 308: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 308/373

 

20

Fi gu ra 8 .4 Esempio di grafo orientato con pesi sugli archi.

una coda in cui memorizzare i nodi man mano che vengono raggiunti e da cui estrarli,secondo una politica FIFO, per procedere nella visita.

Consideriamo ora il caso generale in cui il grafo G = (V, E) sia pesato con valorireali sugli archi attraverso una funzione W : E >—» IR: come già notato nel Paragrafo 6.1,un cammino v0,vi,v2,... .v^ ha associato un peso (che interpretiamo come lunghezza)pari alla somma W ( V Q , V ] ) + W ( V I , V 2 ) + ... + WFVK - i.VIJ dei pesi degli archi che locompongono. Nel seguito, la lunghezza sarà intesa come peso totale del cammino.

Dati due nodi u, v e V, esistono in generale più cammini che collegano u a v,

ognuno con una propria lunghezza: ricordiamo che la distanza pesata 6(u, v) da u av è definita come la lunghezza del cammino di peso minore da u a v. Not iamo che,se il grafo è non orientato, vale 6(u, v) = 6(v, u) , in quanto a ogni cammino da u av corrisponde un cammino (il medesimo percorso al contrario) da v a u della stessalunghezza: ciò non è vero, in generale, se il grafo è orientato. Per il grafo nella Figura 8.4,possiamo osservare ad esempio che 6(vi, v<j) = 35, corrispondente al cammino orientatovi ,v3,v7 ,v6 , mentre 6(vg,vi) = 57, corrispondente al cammino orientato v (;,v2,v5,v1.In effetti, se il grafo non è fortemente connesso può avvenire che per due nodi u,v ladistanza ó(u, v) sia finita mentre 6(v, u) è infinita: questo è il caso ad esempio dei nodi

vii e v\2 nella Figura 8.4, per i quali 6( vn ,v i2) = 12 ment re Ó(vi2,vn) = +00, inquanto non è possibile raggiungere vn da V12.

Il problema che consideriamo è quello che, dato un grafo pesato G = (V, E,W)(orientato o meno) con funzione di peso W : E i-> R, richiede di individuare i camminidi lunghezza minima tra i nodi del grafo stesso. Tale problema assume caratteristichediverse, in termini di miglior modo di risolverlo, in dipendenza del numero di camminiminimi che ci interessa individuare nel grafo: in particolare, se siamo interessati a indi-viduare gli n(n — 1 ) cammini minimi tra tutte le coppie di nodi (alipairs shortestpath),

Page 309: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 309/373

 

avremo che i metodi più efficienti di soluzione possono essere diversi rispetto al caso in

cui ci interessano soltanto gli n — 1 cammini minimi da un nodo a tutti gli altri (single

source shortest path).

Come vedremo, la complessità di risoluzione di questo problema dipende, tra l'altro,

dalle caratteristiche della funzione W: in particolare, considereremo dapprima il caso incui W : E i—» R + , in cui cioè i pesi degli archi sono positivi. Sotto questa ipotesi intro-durremo il classico algoritmo di Dijkstra (definito per il caso single source, ma utilizzabileanche per quello alipairs), e vedremo che questo algoritmo è una riproposizione deglialgoritmi di visita discussi nel Paragrafo 7.4, in cui viene utilizzata però una coda conpriorità come struttura di dati, al posto della coda e della pila.

Nel caso generale in cui i pesi degli archi possono essere anche nulli o negativi,non possiamo usare l'algoritmo di Dijkstra. Vedremo allora come risolvere il problemadiversamente, per quanto riguarda sia la ricerca ali pairs che quella single source, anche se

meno efficientemente del caso in cui i pesi sono positivi.

8.3.2 Cammini minimi in grafi con pesi positivi

Consideriamo inizialmente il problema di tipo single source-. in tal caso, dato un grafoG = (V, E, W) con W : E >—> R + e dato un nodo s € V, vogliamo derivare la distanzaó(s ,v) da s a v, per ogni nod o v € V. Se ad esempio consider iamo ancora il grafonella Figura 8.4, allora per s = vj vogliamo ottenere l'insieme di coppie (vj, 0), (v2, 57),( v 3 , l l ) , ( V 4 , 9 ) , ( V 5 ,20) , (V 6,35), (V 7,18), (v8 ,14), (v9 ,+oo), (v, 0 ,+oo), (v n , + o o ) ,

(vi2, +oo), associando a ogni nodo la relativa distanza da vj.Inoltre, vogliamo ottenere anche, per ogni nodo, il cammino minimo stesso: a tal

fine, ci è sufficiente ottenere, per ogni nodo v, l'indicazione del nodo u che precede v nelcammino minimo da s a v stesso.

Ciò e sufficiente in quanto vale la proprietà che se il cammino minimo da s a v è da- ,to dalla sequenza di nodi s, uo, Ui,..., u T ,u, v, allora la sequenza s, u<),uj,..., u^, u èil cammino min imo da s a u: se infat ti così non fosse ed esistesse un altro camminos , w o , w i , . . . , w t , u più corto, allora anche il cammino s, wo, wi Wt ,u , v avreb-be lunghezza inferiore a s,uo,u.i,... ,u r , u , v, cont radd icendo l'ipotesi fatta che tale

cammino sia minimo.Come vedremo ora, questo problema può essere risolto mediante un algoritmo di

visita del grafo che fa uso di una coda con priorità per determinare l'ordine di visita dei

nodi e che viene indicato come algoritmo di Dijkstra.

Gli elementi nella coda con priorità sono coppie (v, p), con v € V e p £ IR +, ordinaterispetto ai rispettivi pesi p: come vedremo, l'algoritmo mantiene l'invariante che, perogni coppia (v, p) nella coda con priori tà, abbiamo p ^ ó(s,v) e, nel mo me nto in cui(v,p) viene estratta dalla coda con priorità , abb iamo p = 6( s,v) .

Page 310: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 310/373

 

Dijkstra( s ):

FOR  (u = 0; u < n; u = u + 1) {

pred[u] = -1;

dist[u] = +oo;>pred[s] = s;dist [s] = 0;

FOR  (u = 0; u < n; u = u + 1) {

elemento.peso = dist[u];

elemento.dato = u;

PQ.Enqueue( elemento );

} WHILE (!PQ.Empty( )) {

e = PQ.Dequeue( );

v = e.dato;

FOR (x = listaAdiacenza[v].inizio; x != nuli; x = x.succ) -tu = x.dato;

IF (dist[u] > dist[v] + x.peso) {

dist[u] = dist[v] + x.peso;

pred[u] = v;

PQ.DecreaseKeyC u, dist[u] );

>>

>

Codice 8.7 Algoritmo di Dijkstra per la ricerca dei cammini minimi single source, dove la variabileelemento indica un nuovo elemento allocato.

In particolare, a ogni istante il peso p associato al nodo v nella coda con priorità

indica la lunghezza del cammino più corto trovato finora nel grafo: tale peso viene ag-

giornato ogni qual volta viene individuato un cammino più breve da s a v di quello

considerato finora.

L'algoritmo, mostrato nel Codice 8.7, determina la distanza di ogni nodo in V da s,

oltre al corri spondente cammi no min imo, utilizzando due array: d i s t associa a ogni no-do v la lunghezza del più breve camm ino da s a v individuato finora e p r e d rappresenta

il nodo che precede v in tale cammino.

Per effetto dell 'esecuzione del ciclo iniziale (righe 2—12), per ogni nod o v e V - {s},

dist[v] viene posto pari a +00 in quanto al momento non conosciamo alcun cammino

da s a v; inoltre viene posto dist[s] pari a 0 in quanto s dista 0 da se stesso.

I valori pred[v] vengono posti, per tutti i nodi eccetto s, pari a —1, valore utilizzato

per codificare il fatto che non è noto alcun cammino da s a v, mentre per s adottiamo

Page 311: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 311/373

 

la convenzione che pre d[s ] = s. Tutt i i nodi vengono inoltre inseriti nella coda conpriorità, con associati i rispettivi pesi.

Nel ciclo successivo (righe 13—24), l'algoritmo procede a estrarre dalla coda conpriorità il nodo v con peso minimo (riga 14): come vedremo sotto, il peso associato alnodo è pari alla distanza ó(s,v).

In corrispondenza a questa estrazione, vengono esaminati tutti gli archi uscenti dav (righe 16-23): per ogni arco x = (v, u) cons iderato, ci chiediamo se il camminos,...,v,u, di lunghezza 6(s,v) + W(v,u) = dist[v] + x.peso, è più corto della di-stanza del cammino minimo da s a u trovato fino a ora, dove tale distanza è memorizzatain d i s t [u] .

In tal caso (o nel caso in cui s,... ,v,u sia il primo cammino trovato da s a u) ilpeso associato a u viene decrementato al valore 6(s, v) + W( v, u) (riga 21) e v diventa ilpredecessore di u nel cammino minimo.

A L V I E : algoritmo di Djikstra

Osserva, sperimenta e verifica

Dijkstra

Il decremento del peso di un elemento in una coda con priorità viene eseguito me-diante l'operazione DecreaseKey descritta nel Paragrafo 8.2.2. Possiamo notare che il

peso associato a un nodo v e V — {s} non è mai inferiore alla distanza 6(s, v), in quantoall'inizio tale peso è pari a +00 e ogni volta che viene aggiornato viene posto pari allalunghezza di un qualche cammino esistente da s a v, di lunghezza quindi non inferiorea ó(s,v) . Per qua nto riguarda s, il suo peso è pos to pari alla distanza da se stesso e maiaggiornato, in quanto s è necessariamente il primo nodo estratto dalla coda con priorità.Inoltre, per la struttura dell'algoritmo, la sequenza dei pesi associati nel tempo a ogninodo v è monotona decrescente.

Ciò che è necessario mostrare, per convincerci che l'algoritmo restituisce le distanzeda s a tutti i nodi, è che il peso dist[v] di un nodo v al momento della sua estrazione

dalla coda con prior ità è pari a 6(s, v). A tal fine, da to un intero i > 0 e un n od o v e V,indichiamo con Si l'insieme dei primi i nodi estratti dalla coda e con 6t(s, v) la lunghezzadel cammino minimo da s a v passante per soli nodi in Si.

Mostriamo ora che, dopo che l'algoritmo ha considerato i primi i nodi estratti, seun nodo v è ancora nella coda il peso a esso associato è pari alla lunghezza del camminominimo da s a v passante per i soli nodi in Si, e quindi che dist[v] = 6i(s, v).

Questo è vero all'inizio dell'algoritmo quando, dopo la prima iterazione nella corsodella quale è stato estratto s, per ogni nodo v adiacente a s abbiamo dist[v] = W(s, v).

Page 312: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 312/373

 

Figura 8.5 Dimostrazione di correttezza dell'algoritmo di Dijkstra.

Dato che W(s,v) è anche la lunghezza dell'unico cammino che collega s a v passandoper soli nodi in Si = {s}, e quindi abbiamo in effetti dist[v] = ói(s,v).

Ragionando per induzione, facciamo ora l'ipotesi che la proprietà sia vera dopo k— 1nodi estratti dalla coda con priorità, e mostriamo che rimane vera anche dopo k estra-zioni. Supponiamo che u sia il k-esimo nodo estratto: per l'ipotesi induttiva, il peso di unella coda al momento dell'estrazione è pari alla lunghezza del più corto cammino da sa u passante per i soli nodi in S^-i, e quindi dist[u] = ók_i (s,u).

Consideriamo ora un qualunque nodo v adiacente a u e non ancora estratto, e con-sideriamo il cammino min imo da s a v passante per i soli nodi in Sk = Sk_i U {u}.Possiamo avere due possibilità:

1. tale cammino non passa per u, nel qual caso corrisponde al cammino minimopassante soltanto per nodi in S^-i, e quindi óijs.v) = ó^-i (s,v);

2. tale cammino passa anche per u, nel qual caso corrisponde al cammino minimoda s a u seguito dall'arco (u, v), e ó^fs, v) = ó^-i (s,u) + W(u, v).

In entrambi i casi possiamo verificare come l'algoritmo del Codice 8.7 operi in mododa assegnare a dist[v] il valore &k(s,v), e quindi da mantenere vera la proprietà chestiamo mostrando.

Mostriamo ora che, se v g V è l'i-esimo nodo estratto, il cammino minimo da sa v passa necessariamente per i soli nodi in Si. Infatt i, supponiamo per assurdo chetale cammino passi per alcuni nodi che si trovano ancora nella coda al momento in cuiv viene estratto, e indichiamo con w il primo di tali nodi che compare nel camminominimo (Figura 8.5).

Page 313: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 313/373

 

Dato che w precede v in tale cammino minimo, avremmo che 6(s,w) < ó(s ,v) ;inoltre, dato che stiamo ipotizzando che il cammino minimo non passi tutto per nodiin Si, abbiamo che ó(s,v) < ói (s ,v); infine, dato che v viene estratto dalla coda conpriorità prima di w, abbiamo che 6i(s,v) < 6t(s,w). Ma ciò porta a una contraddizione,

se consideriamo che, dato che w è per ipotesi il primo nodo non appartenente a Stnel cammino minimo, abbiamo anche 6(s,w) = ói(s,w): infatti, otteniamo allora che6(s, w) < 6(s,v) < ói( s,v) < ói (s ,w) = 6 (s, w), il che è impossibile.

Qual è il costo computazionale dell'algoritmo del Codice 8.7? Dato che l'algoritmosi limita a gestire in modo opportuno una coda con priorità, il suo costo dipenderàdal costo delle operazioni eseguite su tale struttura. Ind ichiamo allora con t c il costodi costruzione della coda, con t e il costo di estrazione del minimo con l'operazioneDequeue e con td il costo dell'operazione DecreaseKey.

Come possiamo osservare, su un grafo di n nodi e m archi, l'algoritmo esegue n

estrazioni del minimo e al più m DecreaseKey, da cui consegue che il suo tempo diesecuzione è 0 ( t c + n t e + m t d ) . Se utilizziamo l'implementazione della coda con prioritàmediante uno heap, descritta nel Paragrafo 8.2, abbiamo che t c = O(n), t e = O(logn) etd = O(logn), per cui l'algoritmo ha costo complessivo pari a 0((n + m) logn) tempo,che risulta O(mlog n) se supponiamo che il grafo sia connesso, e quindi m = n ( n ) .

Possiamo ottenere un miglioramento del costo dell'algoritmo utilizzando la sempliceimplementazione a lista della coda con priorità: infatt i, se tale implementazione vieneeffettuata per mezzo di una lista non ordinata, avremo che t c = O(n), t e = O(n) etd = 0(1), in quanto il decremento del peso di un nodo non comporta alcuna riorga-

nizzazione della struttura. In questo caso, il costo dell'algoritmo risulta 0(n 2 + m), cheè migliore del caso precedente per grafi densi, in cui in particolare il numero di archi èm = Q(n 2  / logn) .

In generale, possiamo pensare di ridurre il costo complessivo dell'algoritmo, almenosu grafi non sparsi, diminuendo il costo dell'operazione DecreaseKey a fronte di unaumento del costo dell'operazione Dequeue.

Un modo, più bilanciato, di ottenere ciò è quello di utilizzare heap non binari,ma di grado d > 2: in questo caso, la profondità dello heap, e quindi il costo della f 

DecreaseKey diviene logd

n, mentre la Dequeue richiede tempo 0(dlogd

n) in quan-to deve esaminare, a ogni livello, tut ti i d figli del nodo attuale. Da ciò deriva che iltempo di esecuzione dell'algoritmo diviene 0(ndlog d n + mlog d n), che è minimo, alvariare di d, quando nd = m, e quindi quando d = m / n . Per tale valore di d ottieniamoallora un costo dell'algoritmo pari a O(mlog m / n n), che risulta 0(m) se m = B(n 2 ) eO(n log n) se m = 0 (n ).

Osserviamo infine che esiste un'implementazione della coda con priorità, lo heap diFibonacci, che consente di effettuare le operazioni Enqueue, Dequeue e DecreaseKeycon un costo ammortizzato rispettivamente pari a 0(1), O(logn) e 0(1). Ciò, in con-

Page 314: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 314/373

 

seguenza anche di quanto visto nel Paragrafo 3.4.1, comporta che ogni sequenza di poperazioni Enqueue, di q ^ p operazioni Dequeue e di r operazioni DecreaseKey haun costo complessivo di 0( p -I- r + q lo gn) t emp o. L'utilizzo di questa implementazio-ne permette allora di eseguire l'algoritmo del Codice 8.7 in tempo 0(nlogn + m), cherappresenta il miglior costo possibile nel caso di grafi con m = O ( n l o g n ) .

La ricerca dei cammini minimi tra tutte le coppie di nodi (vale a dire nel caso ali

 pairs) può essere effettuata applicando n volte, una per ogni possibile nodo sorgente, l'al-goritmo di Dijkstra illustrato sopra: ciò fornisce un metodo di soluzione di tale problemaavente costo O(nlogn + nm).

8.3.3 Cammini minimi in grafi pesati generali

L'algoritmo di Dijkstra non è applicabile al problema dei cammini minimi quando gli

archi del grafo possono avere pesi negativi o nulli, perché un arco potrebbe diminuire lalunghezza del cammino di un nodo già estratto dalla coda con priorità. Per trattare la ver-

sione generale del problema dobbiamo comunque ipotizzare che nel grafo, anche in pre-

senza di archi a peso negativo, non esistano cicli aventi peso complessivo negativo (vale a

dire che è negativa la somma dei pesi degli archi corrispondenti): se v^, vi 2 , . . . , v¿k, v i (

fosse un tale ciclo, avente peso —D, la distanza tra due qualunque di tali nodi sarebbe

—oo. Notiamo infatti che attraversare il ciclo comporta che la distanza complessivamente

percorsa viene decrementata di D, e quindi, dato che esso può essere percorso un numero

arbitrariamente grande di volte, la distanza può divenire arbitrariamente grande in valore

assoluto e negativa.Notiamo inoltre che ciò avviene non solo per quanto riguarda la distanza tra due

nodi del ciclo, ma in realtà per tutte le coppie di nodi nella stessa componente connessache include il ciclo stesso e quindi, se il grafo è connesso per tutte le coppie di nodi, lacui distanza risulta quindi pari a —oo.

Il metodo più diffuso per la ricerca single source dei cammini minimi è il cosiddet-to algoritmo di Bellman-Ford, che opera come most rato nel Codice 8.8. L'algoritmoha una strut tura molt o semplice e simile a quello di Dijkst ra. Co me possiamo vedere,non necessita di strutture di dati particolari: anche in questo caso per ogni nodo v vie-

ne mantenuta in dist[v] la lunghezza del cammino più breve da s a v finora trovatodall'algoritmo e in pred[v] il nodo che precede immediatamente v lungo tale cammino.Facciamo l'ipotesi che il valore - 1 per pre d[v ] stia a rappresentare il fat to che non viene

rappresentato alcun cammino da s a v; inoltre, poniamo anche predts] = s.Dopo l'inizializzazione (righe 2-7) come nell'algoritmo di Dijkstra, l'algoritmo ope-

ra per n = |V| iterazioni la scansione di tutti gli archi di G (righe 8-17): per ogni arco(u,v) esaminato, verifichiamo se i valori dist[u] e dist[v] soddisfano la condizioned i s t [ u ] > d i s t [ v ] 4- W(u, v) (riga 12). Se è così, questo vuol dire che esiste un cam-

Page 315: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 315/373

 

Bellman-FordC s ) :

FOR  (u = 0; u < N; u = u + 1) {

pred[u] = -1;

dist[u] = +00;

>pred[s] = s;dist [s] = 0;

FOR  (i = 0; i < N; i = i + 1)

FOR  (v = 0; v < N; v = v + 1) {

FOR (X = listaAdiacenza[v].inizio; x != nuli; x=x.succ) {

u = x.dato;

IF (dist[u] > dist[v] + x.peso) {

dist[u] = dist[v] + x.peso;

pred[u] = v;

>

>>

Codice 8.8 Algoritmo di Bellman-Ford per la ricerca dei cammini minimi single source.

mino s,... ,v,u più breve del miglior cammino da s a u trovato finora: le informazionidist[u] e dist[v] vengono allora aggiornate per rappresentare questa nuova situazione.

A L V I E : algoritmo di Bellman-Ford

C5> • -JOD CH

Osserva, sperimenta e verifica :BellmanFord vi».

Per mostrare che l'algoritmo opera correttamente anche con pesi negativi e nulli equindi, al momento della sua terminazione, abbiamo dist[v] = 6(s, v) per ogni nodo v,

ragioniamo per induzione: verifichiamo che, all'inizio dell'iterazione i nel ciclo più ester-no (righe 8—17), abbiamo che dist[v] = ó(s, v) per ogni nodo v per il quale il camminominimo da s a v è composto da al più i archi.

Questa proprietà è vera per i = 0, in quanto s è il solo nodo tale che il camminominimo da s a s è composto da 0 archi, e in effetti dist[s] = 0 = ó(s, s).

Per il passo induttivo, ipotizziamo ora che la proprietà sia vera all'inizio dell'itera-zione i: se consideriamo un qualunque nodo v tale che il cammino minimo s,... ,u,vcomprende i + 1 archi, ne consegue che per il nodo u il cammino minimo s,..., u com-

Page 316: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 316/373

 

prende i primi i archi del cammino precedente, e quind i abbiamo d i s t [ u ] = ó(s,u) .Ma allora, nel corso dell'iterazione i, abbiamo che dist[v] è aggiornato in modo chedist[v] = dist[u] -I-W(u,v) = ó(s,v) + W(u,v), che è uguale a 6(s,v) per l'ipotesi cheil cammino s,... ,u, v sia minimo. Quindi la proprietà risulta soddisfatta quando inizial'iterazione i + 1.

Se osserviamo che, dato che non esistono per ipotesi cicli di lunghezza negativa in G,un cammino minimo comprende al più n — 1 archi, possiamo concludere che quandoi = n tutti i cammini minimi sono stati individuati.

Per quanto riguarda il costo di esecuzione, possiamo notare che l'algoritmo esegueO(n) iterazioni, in ognuna delle quali esamina ogni arco due volte, e quindi esegue O(m)operazioni: ne consegue che il tempo di esecuzione è O(nm). Abbiamo quindi che la piùvasta applicabilità (anche a grafi con pesi negativi, se non abbiamo cicli negativi) rispettoall'algoritmo di Dijkstra viene pagata da una minore efficienza.

Osserviamo infine che, per quanto detto, se l'algoritmo effettuasse più di n itera-zioni, l'ul tima di esse non vedrebbe alcun aggiornamento dei valori nell'array d i s t , inquanto i cammini minimi sono stati già tutt i trovati. Invece, se il grafo avesse cicli dilunghezza negativa, ci sarebbero valori in dist[v] che continuerebbero a decrementarsiindefinitamente.

Da questa osservazione deriva che l'algoritmo di Bellman-Ford può essere utilizzatoanche per verificare se un grafo G presenta cicli di lunghezza negativa. A tal fine, se nè il numero di nodi, basta eseguire n + 1 iterazioni e verificare se nel corso dell'ultimavengono modificati valori nell'array d i s t : se così è, possiamo concludere che il grafo

presenta in effetti dei cicli negativi.Se consideriamo ora il problema della ricerca dei cammini minimi tra tutti i nodi,

possiamo dire che una semplice soluzione è data, come nel caso dei grafi a pesi posi-tivi, dall'iterazione su tutti i nodi dell'algoritmo per la ricerca single source: nel nostrocaso, questo risulterebbe in un tempo di esecuzione 0(n 2 m ) . Possiamo tuttavia otte-nere di meglio utilizzando il paradigma della programmazione dinamica, introdotto nelParagrafo 2.7.

Per seguire questo approccio, utilizziamo la rappresentazione dei grafi pesati median-te la matrice di adiacenza A a cui è associata quella dei pesi P, tale che P[u, v] = W(u, v)

se e solo se A[u,v] = 1 (vale a dire (u,v) £ E), dove richiediamo che P[u, u] = 0 per0 ^ u, v < n: gli altri valori di P sono considerati pari a +00 in quanto non esiste un arcodi collegamento. Dato un cammino da u a v, definiamo come nodo interno del camminoun nodo w, diverso sia da u che da v, che compare nel cammino stesso. Per ogni k < n,indichiamo con V^ l'insieme dei primi k nodi di V, abbiamo cioè per definizione cheV k = { 0 , l , . . . , k - 1 } .

Possiamo allora considerare, per ogni coppia di nodi u e v e per ogni k, il camminominimo ^ ( u , v) da u a v passante soltanto per nodi in V^ (eccetto per quanto riguarda

Page 317: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 317/373

 

u e v stessi): se k = n tale cammino è il cammino minimo 7t(u,v) da u a v nell'interografo. Inoltre, dato che Vo = 0, il cammino mi nimo 7to(u,v) da u a v in Vo è datodall'arco (u, v), se tale arco esiste. Se indichiamo con 6|c(u, v) la lunghezza del cammino7tk(u, v), avremo allora che 6o(u,v) = W(u ,v) se (u ,v ) G E, ment re óo(u,v) = +00

altrimenti.Se soffermiamo la nostra attenzione sul cammino minim o Tt^fu.v) per 0 < k ^ n,possiamo notare che possono verificarsi due eventualità:

1. il nodo k— 1 non appare in ^ ( u , v), ma allora ^ ( u , v) = 7tic_ 1 (u, v) (il camminominimo è lo stesso che nel caso in cui utilizzavamo soltanto i nodi {0,1 k — 2}come nodi interni);

2. al contrario, il nod o k—1 appare in ^ ( u , v), ma allora, dato che tale cammino nonpuò contenere cicli in quanto ogni ciclo (che non può avere lunghezza negativa)

lo renderebbe più lungo, 7tk(u, v) può essere suddiviso in due parti: un primocammino da u a k — 1 passante soltanto per nodi in Vk_i e un secondo camminoda k — 1 a v passante anch'esso per soli nodi in V k_ 1.

Possiamo quindi derivare la seguente regola di programmazione dinamica per calco-lare la lunghezza ó^f u, v) del cammino min imo da u a v:

{S i ì = i p t u , v l se k = 0k | U ' V J S m i n { 6 k _ I ( u , v ) , 6 l c _ 1 (u,k- 1) + S k _ , (k - l, v) } se k > 1

Seguendo l'approccio della programmazione dinamica, abbiamo quindi un meccani-smo di decomposizione del problema in sottoproblemi più semplici, con una soluzionedefinita per i sottoproblemi elementari, corrispondenti al caso k = 0. A questo punto,l'algoritmo risultante, detto algoritmo di Floyd-Warshall, presenta l'usuale struttura diun algoritmo di programmazione dinamica. In particolare, esso opera (almeno concet-tualmente) su una coppia di tabelle tridimensionali d i s t e p r e d , ciascuna di n "strati",n righe e n colonne: per il cammino 7tk(u, v), l'algoritmo utilizza queste tabelle per me-morizzare 6k(u, v) in dist[k, u, v] secondo la relazione (8.2) e il predecessore di v in talecammino in pred[k, u, v].

In realtà, un più attento esame dell'equazione (8.2) mostra che possiamo ignorarel'indice k — 1: infatti 6 k -i (u, k — 1) = 6k(u, k — 1) e ó ^ i i k — 1, v) = ók(k — 1, v) inquanto il nodo di arrivo (nel primo caso) e di partenza (nel secondo caso) è k — 1, il qualenon può essere un nodo interno in tali cammini quando passiamo da V k_i a Vy. Taleosservazione ci permette di utilizzare le tabelle bidimensionali n x n per rappresentared i s t e p r e d , come possiamo vedere nel Codice 8.9.

L'algoritmo, dopo una prima fase di inizializzazione degli elementi dist[u, v] (ri-ghe 2- 5) , passa a derivare iterat ivamente tut ti i cammini minimi in al crescere di k

Page 318: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 318/373

 

Floyd-Warshall( ):

FOR  (u = 0; u < n; u = u + 1)

FOR  (v = 0; v < n; v = v + 1) {

dist[u,v] = P[u,v];

>FOR  (k = 1; k <= n; k = k + 1)FOR (u=0; u < n ; u = u + 1) {

FOR ( v= 0; v < n ; v = v + l ) {

IF (dist[u,v] > dist[u,k-l] + dist[k-l,v]) {

dist[u,v] = dist[u,k-l] + dist[k-l,v];

pred[u,v] = pred[k-l,v];

>>

>

Codice 8.9 Algoritmo di Floyd-Warshall per la ricerca dei cammini minimi alipairs.

(righe 6—14). A tal fine, utilizza la relazione di programmazione dinamica riporta ta

in (8.2) per inferire, alla riga 9, quale sia il cammino minimo 7tk(u,v) e quindi quali

valori memorizzare per rappresentare tale cammino e la relativa lunghezza (righe 10—12).

Il costo dell'algoritmo è determinato dai tre cicli nidificati, da cui deriva un tempo

di computazione 0(n 3 ); per quanto riguarda lo spazio utilizzato, il Codice 8.9 fa uso,

come detto sopra, di due array di n x n elementi, totalizzando 0(n2

) spazio.

A L V I E : algoritmo di Floyd-Warshall

Osserva, sperimenta e verifica

FloydWarshall

8.4 Opus libri: data mining e minimi alberi ricoprenti

In applicazioni di data mining è necessario operare su insiemi di dati di grandi dimensio-

ni, ad esempio di carattere sperimentale, per individuare delle regolarità nei dati trattati

o similitudini tra loro sottoinsiemi: il tutto nel tentativo di derivare un'ipotesi di legge,

un qualche tipo di conoscenza induttivo della realtà soggiacente ai dati. Tipici casi di tali

applicazioni sono ad esempio l'analisi di una rete sociale con l'obiettivo di individuare

Page 319: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 319/373

 

le comunità al suo interno, o il partizionamento di una collezione di documenti su uninsieme di tematiche.

In questo contesto, un metodo applicato in modo estensivo è l'analisi di cluster(cluster analysis), vale a dire il partizionamento di dati osservati in sottoinsiemi, detti

cluster, in modo tale che i dati in ciascun cluster condividano qualche proprietà comune,non posseduta da dati esterni al cluster.

In genere, l'individuazione dei cluster viene effettuata in termini di prossimità deidati rispetto a una qualche metrica definita su di essi che ne misura la distanza: a talfine, i dati possono essere proiettati, eventualmente per mezzo di una qualche funzionepredefinita, su uno spazio k-dimensionale, utilizzando la normale distanza euclidea comemetrica di riferimento.

Sono stati definiti numerosi metodi per l'individuazione di una partizione in cluster:la scelta del più efficace da utilizzare rispetto a un determinato insieme di dati è spesso

molto ardua. La maggior parte dei metodi richiede inoltre l'assegnazione di valori a unaserie di parametri, o addirittura la predeterminazione del numero di cluster da ottenere.

In tale contesto, una tecnica piuttosto semplice, diffusa ed efficace in molte situa-zioni, derivata dalla teoria dei grafi, è basata sull'uso di alberi minimi di ricoprimento.Il problema del minimo albero di ricoprimento o minimo albero ricoprente (minimumspanning tree) è uno dei problemi su grafi più semplici da definire e più studiati . Ricordia-mo che, dato un grafo non orientato e connesso G = (V, E), un albero di ricoprimentodi G è un albero T i cui archi sono anche archi del grafo e collegano tutti i nodi in V(Paragrafo 7.4), ossia un albero T = (V, E' ), dove E' C E. Un tale albero è minimo

se la somma dei pesi nei suoi archi è la minima tra quelle di tutti i possibili alberi diricoprimento (notiamo come nel caso in cui G non sia connesso non esiste alcun albe-ro che lo ricopre: possiamo però definire una foresta di ricoprimento di G, vale a direun insieme di alberi, ciascuno ricoprente la propria componente connessa). Nel seguitofaremo sempre l'ipotesi che G sia connesso.

Nel data mining basato sull'analisi dei cluster, i dati sono rappresentati da punti inuno spazio k-dimensionale. Un esempio è nell'analisi del genoma, in cui è possibilemonitorare simultaneamente k parametri numerici per decine di migliaia di geni (chereagiscono a cambiamenti imposti al loro ambiente) attraverso dei dispositivi specialichiamati microarray, i quali forniscono un insieme di punti k-dimensionali in uscita. Intal caso, il clustering basato sull'albero minimo di ricoprimento opera a partire dal grafoG = (V, E, W) in cui V è l'insieme dei punti, E è l'insieme di tut te le possibili coppie dipunti (quindi G è un grafo completo di tutti gli archi) e W è la distanza euclidea tra ipunti: in tale contesto l'albero minimo di ricoprimento viene detto euclideo.

L'albero risultante può essere utilizzato come base per il partizionamento in cluster:come vedremo, infatti, ogni arco e dell'albero è l'arco di peso inferiore tra quelli in grado j3®di collegare le due porzioni di albero ottenute dall'eventuale rimozione di e.

Page 320: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 320/373

 

In termini di data mining, la rimozione di e dà luogo a una separazione tra due clu-ster in cui la distanza tra due qualsiasi punti, uno per cluster, non può essere inferioreal peso di e. Scegliendo di rimuovere gli archi più lunghi (di peso maggiore) presentinell'albero minimo di ricoprimento, abbiamo che iniziamo a separare i cluster più di-stanti tra di loro: osserviamo infatti che punti appartenenti alla stessa porzione di alberoformano un cluster e che cluster diversi tendono a essere collegati da archi più lunghi.La rimozione di ogni successivo arco lungo, separa ulteriormente i cluster. In generale,dopo k rimozioni di archi dall'albero, otteniamo k + 1 cluster, tendendo così a separareinsiemi di nodi "lontani" tra loro, al fine di ottenere cluster il più possibile significativi.

Comunque, non è detto che la sola eliminazione degli archi più lunghi nell'alberofornisca una partizione in cluster accettabile: ad esempio, alcuni cluster ottenuti possonoessere eccessivamente piccoli per le finalità dell'analisi dei dati effettuata . Spesso, puòessere necessario aggiungere ulteriori criteri di scelta degli archi da eliminare, in modo damigliorare la significatività della partizione ottenuta.

8.4.1 Problema della ricerca del minimo albero di ricoprimento

Come già visto nel Paragrafo 7.4, un qualunque albero di ricoprimento di un grafo puòessere trovato in tempo 0(n + m) mediante una visita (sia in ampiezza che in profondità)del grafo stesso: gli alberi BFS e DFS risultanti da tali visite non sono altro che particolarialberi di ricoprimento.

Consideriamo ora, come nel paragrafo precedente, il caso in cui l'insieme degli archisia pesato per mezzo di una funzione W : E — R + e che il grafo sia connesso e non

orientato e connesso. In questa ipotesi ogni albero ricoprente T = (V, E') di G haassociato un peso Y-eef W(e) pari cioè alla somma dei pesi dei suoi archi. Quel chevogliamo è allora trovare, fra tutti gli alberi ricoprenti di G, quello avente peso minimo(laddove gli alberi BFS e DFS non sono necessariamente di peso minimo).

Introduciamo ora due proprietà strutturali del minimo albero ricoprente, che saran-no utilizzate negli algoritmi che considereremo per questo problema: queste proprietàfanno riferimento al concetto di ciclo, già introdotto nel Paragrafo 6.1, e alla nuovanozione di taglio in un grafo connesso.

Dato un grafo G = (V, E), un taglio {cut) su G è un qualunque sottoinsieme C C E

di archi la cui rimozione disconnette il grafo, nel senso che il grafo G' = (V, E — C) è taleche esistono almeno due nodi u,v € V tra i quali non esiste un cammino.

Ad esempio, nella Figura 8.6 viene mostrato un taglio (riportato graficamente comeuna linea più spessa) che corrisponde all'insieme di archi C = {(v^vs), (vi,vs), (v4,vy),(  v  4 »  v  2 ) . (  v 3 >  v 7 ) > ( v 3 > v 2 ) > ( v i > v 5 ) l - Come possiamo verificare, la rimozione degli archinel taglio disconnette il grafo in due componenti disgiunte {vi, V3, V4} e (V2, V5, vg, V7, vg}.

Enunciamo ora le due proprietà di un minimo albero ricoprente che utilizzeremoper mostrare la correttezza degli algoritmi che considereremo nel seguito. Dato un grafo

Page 321: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 321/373

 

Figura 8.6 Esempio di taglio in un grafo.

G = (V, E) pesato sugli archi e un suo minimo albero ricoprente T = (V, E' ), per ogniarco e G E abbiamo che:

Condizione di taglio e £ E' se e solo se esiste un taglio in G che comprende e, per ilquale e è l'arco di peso minimo;

Condizione di ciclo e 0 E' se e solo se esiste un ciclo in G che comprende e, per il qualee è l'arco di peso massimo.

Per convincerci della prima proprietà, osserviamo che se e e E' allora la sua rimo-

zione disconnette i nodi su T in due componenti V', V" (ricordiamo che la rimozionedi un qualunque arco di un albero disconnette l'albero stesso). L'insieme che comprendesia e che tutti gli archi (v',v") e E - E' tali che v' e V' e v" G V" è un taglio inG. Per la minimali tà dell'albero T, per qualunque arco e' ^ e in tale insieme vale cheYV(e') ^ W(e): se così non fosse, infatti, l'insieme di archi E' — {e} U {e'} indurrebbe undiverso albero ricoprente di peso W(T) — W(e) + W(e') < W(T). Al tempo stesso, conla stessa motivazione, dato un qualunque taglio in G, l'arco di peso minimo in tale tagliodeve essere incluso nel minimo albero ricoprente.

Per quanto riguarda la seconda proprietà, per ogni arco e 0 E' , l'insieme E' U {e}

induce un ciclo che include tale arco: se in tale ciclo esistesse un arco e'  € E' tale cheW(e') > W(e) allora l'insieme di archi E' — {e'} U {e} indurrebbe un diverso alberoricoprente di peso W(T) — W(e') + W(e) < W(T), pervenendo a una contraddizione.Al tempo stesso, se e è l'arco di peso massimo in un ciclo, non può appartenere a E': secosì fosse, potremmo sostuirlo con uno del ciclo meno pesante, ottenendo per assurdoun albero di ricoprimento di costo inferiore al minimo.

Nel seguito introduciamo due algoritmi classici risalenti a metà degli anni '50, l'algo-ritmo di Kruskal e quello di Jarnik-Prim, per la ricerca del minimo albero ricoprente di

Page 322: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 322/373

 

un grafo. Prima di esaminarli, preannunciamo però che, in effetti, essi sono due variantidi un medesimo approccio generale alla risoluzione del problema.

In questo approccio, un algoritmo opera in modo goloso, inizializzando E' all'insie-me vuoto, e aggiungendo poi archi a tale insieme fino a che il grafo T = (V, E') nondiventa connesso. Un arco viene aggiunto a E' se è quello più leggero uscente da unaqualche componente connessa di T, vale a dire se è l'arco più leggero che collega un nododella componente a un nodo non appartenente a essa. Per quanto detto sopra, quindi,un arco è incluso nel minimo albero ricoprente se è più leggero di un qualunque taglioche separa la componente dal resto del grafo.

L'effetto di tutto ciò è che, a ogni passo, gli archi in E' formano un sottoinsieme(una foresta, in effetti) del minimo albero di ricoprimento. Dato che l'algoritmo terminaquando tutti gli archi sono stati esaminati, ne deriva che per ogni taglio esiste almeno unarco (il più leggero, in particolare) che è stato inserito in E' e quindi il grafo T = (V, E')è connesso. Inoltre, dato che per ogni arco inserito in E' due componenti connessedisgiunte di T vengono riunite e che, a partire da n componenti disgiunte (i singolinodi) per giungere ad avere un singola componente di n nodi bisogna effettuare n — 1 talioperazioni di riunione, ne deriva che il numero di archi in E' al termine dell'algoritmo èpari a n — 1 e che, quindi, T è un albero (essendo connesso e aciclico per costruzione).

8.4.2 Algoritmo di Kruskal

L'algoritmo di Kruskal per la ricerca del minimo albero di ricoprimento opera conside-rando gli archi l'uno dopo l'altro, in ordine crescente di peso, valutando se inserire ogni

arco nell'insieme E' degli archi dell'albero. Nel considerare l'arco (u, v), possiamo averedue possibilità:

1. i due nodi u e v sono già collegati in G = (V, E'), e quind i l'arco (u ,v ) chiudeun ciclo: in tal caso esso è l'arco più pesante nel ciclo, e quindi non appartiene alminimo albero di ricoprimento;

2. i due nodi u e v non sono già collegati in G = (V, E' ), e quindi esiste almeno untaglio che separa u da v: di tale taglio (u., v), essendo il primo arco considerato, e

il più leggero, e quindi esso appartiene all'albero e va messo in E'.L'algoritmo di Kruskal opera a partire da una situazione in cui esistono n com-

ponenti connesse distinte (gli n nodi isolati), ognuna con un proprio minimo alberodi ricoprimento (l'insieme vuoto degli archi). L'algoritmo unisce man mano coppie dicomponenti disgiunte, mantenendo al tempo stesso traccia del minimo albero di rico-primento della componente risultante. AI termine dell'esecuzione, tutte le componentisono state riunificate in una sola, il cui minimo albero ricoprente è quindi il minimoalbero ricoprente dell'intero grafo.

Page 323: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 323/373

 

Kruskal( ) :

FOR  (u = 0; u < n; u = u + 1) {

FOR (x = listaAdiacenza[U].inizio ; x != null; x = x.succ) {

v = x.dato;

elemento.dato = <u, v>;

elemento.peso = x.peso;PQ.Enqueue( elemento );

>set[u] = NuovoNodo( );

Crea( set[u] );

>  WHILE (!PQ.Empty( )) {

elemento = PQ.Dequeue( );

<u,v> = elemento.dato ;

IF (¡Appartieni( set[u], set[v] )) {

Unisci( set[u], set [v] );mst.InserisciFondo( <u,v> );

>

Codice 8.10 Algoritmo di Kruskal per la ricerca del minimo albero di ricoprimento.

Il Codice 8.10 presenta un'implementazione dettagliata dell'algoritmo delineato so-

pra e codice vengono utilizzate diverse strutture di dati:

1. una coda con priorità PQ con tenente l'insieme degli archi del grafo e i loro pesi;

2. una struttura di dati che rappresenta una partizione dell'insieme dei nodi in m od o

tale dà consentire di verificare se due nodi appartengono allo stesso sottoinsieme e

da effettuare l'unione dei sottoinsiemi di appartenenza di due elementi: a tal fine,

utilizziamo un insieme di liste, così come illustrato nel Paragrafo 3.4.1;

3. un array s e t che associa a ogni nod o del grafo un riferimento al corri spondenteelemento nella struttura di dati precedente;

4. una lista doppia mst (come defin ita nel Paragrafo 5.2) utilizzata per memorizzaregli archi nel minimo albero ricoprente, quando sono individuati dall'algoritmo.

Come possiamo vedere, l'algoritmo utilizza la coda con priorità per ottenere un or-dinamen to degli archi crescente rispetto al loro peso. In particolare, viene inizialmen-te creata una coda con priorità contenente tutti gli archi, oltre a una rappresentazione

Page 324: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 324/373

 

di componenti composte da liste disgiunte, ciascuna inizialmente contenente un solonodo u (righe 2-11).

Gli archi vengono poi considerati l'uno dopo l'altro, in ordine crescente di peso(righe 12-19): per ogni arco, ci chiediamo se esso collega due nodi posti in componentidiverse o, equivalentemente, se chiude un ciclo. Se ciò non avviene e, quindi, l'arco hapeso minimo su un qualunque taglio che divide le due componenti, tali componentisono unificate e l'arco viene inserito nel minimo albero di ricoprimento (righe 15—18).

Notiamo che, essendo il grafo non orientato, ogni arco viene inserito due volte nellacoda con priorità senza pregiudicare l'esito dell'algoritmo quando viene estratto due volte(la prima estrazione soltanto può sortire un effetto in quanto la seconda non supera lacondizione alla riga 15).

Per valutare il tempo di esecuzione dell'algoritmo di Kruskal, possiamo vedere che,su un grafo di n nodi e m archi, esso effettua al più m operazioni Enqueue, Empty eDeq ueu e, oltre a n operazioni Cr ea , Un i s c i e m operazioni A p p a r t i e n i : il costodell'algoritmo dipende quindi dai costi di esecuzione di tali operazioni sulle strutture didati utilizzate. Se ad esempio utilizziamo uno heap come implementazione della codacon priorità e l'insieme di liste del Paragrafo 3.4.1 per rappresentare partizioni di nodi,abbiamo che En qu eu e e Deq ueu e richiedono tempo O(log n), Empty, A p p a r t i e n i eCrea in tempo 0(1), e Appartieni in tempo O(logn) ammortizzato. Da ciò derivache il costo complessivo dell'algoritmo in tal caso è O f m l o g m + nl o g n ) = 0 ( ( m +n) log n) , e qui ndi O ( ml o g n) se il grafo è connesso, per cui abbiamo m ^ n.

A L V I E : algoritmo di Kruskal

Osserva, sperimenta e verifica

Kruskal

8.4.3 Algoritmo di Jarnik-Prim

Come abbiamo visto, l'algoritmo di Kruskal costruisce un minimo albero ricoprentefacendo crescere un insieme di minimi alberi ricoprenti relativi a sottoinsiemi dei nodi:gli alberi sono man mano unificati fino a ottenere quello relativo all'intero grafo.

L'algoritmo di Jarnik-Prim opera in modo più "centralizzato": esso parte da un qua-

lunque nodo s e fa crescere un minimo albero ricoprente a partire da tale nodo, aggiun-

gendo man mano nuovi nodi e archi all'albero stesso: se T indica la porzione di minimo

albero ricoprente attualmente costruita, l'algoritmo sceglie l'arco (u,v) tale che esso è di

Page 325: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 325/373

 

8.4 Opus libri: data mining e minimi alberi ricoprenti

peso minimo nel taglio tra u e T e v € V — T, aggiungendo v a T e (u,v) all'insieme E',

fino a coprire tutti i nodi del grafo.

Non è difficile renderci conto che l'insieme E' ottenuto al termine dell'algoritmo èl'insieme degli archi nel mi ni mo albero ricoprente. Infatti , ogni arco aggiunto a E' è ilpiù leggero nel taglio che separa T da V — T e quindi, per quanto detto sopra, deve farparte del minimo albero ricoprente del grafo. Dato che, inoltre, al termine dell'algoritmoabbiamo che | E' |= n — 1, tutti gli archi dell'albero compaiono in E'.

Come implementare in modo efficiente l'algoritmo presentato sopra? Il punto criticodi un'implementazione è rappresentato dal come realizzare efficientemente la selezionedell'arco di peso minimo tra T a V — T. La soluzione banale, consistente nell'effettuaretale selezione scandendo ogni volta tutti gli m archi porterebbe a ottenere un tempo diesecuzione O(nm), peggiore quindi di quello ottenuto dall'algoritmo di Kruskal.

Una soluzione più efficiente è fornita dall'utilizzo di una coda con priorità PQ all'in-terno della quale mantenere, a ogni istante, l'insieme dei nodi in V—T, utilizzando comepeso di ogni nodo v e V — T il peso dell'arco più leggero che collega v a un qualchenodo in T. Se ad esempio consideriamo la situazione rappresentata nel primo grafo nella

Figura 8.7, osserviamo che V — T = {v2, vg, V7, vg} e che i pesi associati a ognuno ditali nodi saranno, rispettivamente, 8, +00, 7, 14, dove il peso +00 per il nodo v<$ sta arappresentare il fatto che tale nodo non è adiacente a nessun nodo di T.

Page 326: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 326/373

 

Jarnik-Prim( ):

FOR  (u = 0; u < n; u = u + 1) {

incluso[u] = FALSE;pred [u] = u;

elemento.peso = peso[u] = +oo;

elemento.dato = u;PQ.Enqueue( elemento );

>  WHILE (!PQ.Empty( )) {

elemento = PQ.Dequeue( );

v = elemento.dato;

incluso[v] = TRUE;mst.InserisciFondoC <pred[v], v> );

FOR (X = listaAdiacenza[v].inizio; x != null; x = x.succ) {

u = x.dato;

IF (!incluso[u] && x.peso < peso[u]) {pred[u] = v;

peso[u] = x.peso;

PQ.DecreaseKeyC u, peso[u] );

>>

Codice 8.11 Algoritmo di Jarnik-Prim per la ricerca del minimo albero di ricoprimento.

Utilizzando una coda con priorità di questo tipo, la selezione dell'arco viene effettua-ta mediante una Dequeue, ma, al tempo stesso, è necessario prevedere l'aggiornamento,quando necessario, dei pesi associati ai nodi in PQ. Tale aggiornamento può derivare dalpassaggio di un nodo v da V —T a T, passaggio che fa sì che modifichiamo l'insieme degliarchi tra T e V — T considerati per la selezione.

Ad esempio, nel passaggio dalla situazione considerata sopra alla situazione succes-siva, descritta dal secondo grafo nella figura, abbiamo che, per effetto del passaggio del

nodo vj da V — T a T, il nodo vg risulta ora adiacente a un nodo in T (V7 stesso), e quindiil peso associato a vg in PQ viene decrementato da +00 a 17 = Wfv^, V7).

Se consideriamo la situazione successiva (descritta dal terzo grafo nella figura) pos-

siamo vedere come, per effetto del passaggio in T di V2 adiacente anch'esso a vg, l'arco

più leggero che collega tale nodo a un qualche altro nodo in T sia (v2,vg), e quindi, a

questo punt o, il peso associato a Vg in PQ è pari a W(v2, vg) = 8.

Da quanto osservato, possiamo concludere che l'utilizzo di PQ richiede che, in corri-

spondenza a ogni passaggio di un nodo v da T a V —T, vengano esaminati tutti gli i archi

Page 327: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 327/373

 

incidenti v. Per ogni arco (u, v) di questo tipo, se u e V — T allora il peso di u in PQ vieneposto pari a W(u, v), se tale valore è minore del peso attuale di u. In tal modo, se esisteora un modo "più economico" per collegare un nodo in T a V — T, ciò viene riportatonella coda con priorità. Qu indi , le operazioni che van no effe ttuate su PQ sono qui ndiEnqueue per costruire la coda, Dequeue per estrarre man mano i nodi da inserire in T eDec r ea se Ke y per aggiornare il peso dei nodi ancora in T (notiamo che l'aggio rnamentopuò essere soltanto un decremento). Il Codice 8.11 realizza tale strategia impiegando unarray booleano i n c l u s o per marcare i vertici in T, mentre quelli in V — T sono nellacoda con priorità PQ (l'inizializzazione è alle righe 2-8) . Nel ciclo principale (righe 9 -22), l'algoritmo estrae il nodo v da includere in T e decrementa il peso dei suoi verticiadiacenti u in V — T, quand o questi han no peso superiore a quello dell'arco (v, u) cheli collega a v. L'algoritmo esegue in particolare n operazioni Enqueue e Dequeue, e alpiù 2m operazioni DecreaseKey, e il suo costo, come nei casi precedenti degli algo-ritmi di Dijkstra e di Kruskal, dipende dal costo di tali operazioni sull'implementazione

della coda con priorità adottata. Ad esempio, utilizzando uno heap abbiamo, come giàvisto, che le tre operazioni in questione hanno ciascuna costo O(logn), per cui il costocomplessivo dell'algoritmo risulta 0((n + m) logn), e quindi O(mlogn) in quanto con-sideriamo il grafo connesso: l'utilizzo di uno heap di grado d > 2 ha come conseguenza,così come per l'algoritmo di Dijkstra, che il costo risulta 0(mlog T n y n n). Infine, l'uti-lizzo di heap di Fibonacci (che, ricordiamo, hanno costi ammortizzati per le operazioniEn qu eue , Dequ eue e D e cr ea se K ey pari a 0( 1) , O(l ogn ) e 0( 1) , rispettivamente) fasiche il costo complessivo dell'algoritmo risulti 0(nlogn-|- m), migliore quindi di quantoottenuto per mezzo dall'algoritmo di Kruskal.

A L V I E : algoritmo di Jarnik-Prim

Osserva, sperimenta e verifica

Jarnik-Prim

RIEPILOGO  In questo capitolo abbiamo esaminato le code con priorità, mostrandone e discutendone

l'implementazione più diffusa, lo heap. Abbiamo illustrato l'applicazione delle code con

  priorità a vari importanti problemi, come l'ordinamento, per il quale abbiamo considerato

l'algoritmo di heapsort, la ricerca di cammini minimi e la derivazione del minimo albero

ricoprente in grafi pesati. Per tali problemi, che rivestono grande rilevanza applicativa,

abbiamo mostrato diverse soluzioni algoritmiche, sia facenti uso di code con priorità che

operanti senza utilizzare tali strutture.

Page 328: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 328/373

 

ESERCIZI

1. Mostrate come ottenere una coda con priorità con la stessa complessità in tem-po dello heap, utilizzando un albero AVL (descrivete anche come realizzare lafunzione DecreaseKey). Discutete complessità in spazio.

2. Un min-max heap è una struttura di dati che implementa una coda con prioritàsia rispetto al minimo che rispetto al massimo. In uno heap di questo tipo, nellaradice è contenuto il minimo e, in ognuno dei suoi due figli, il massimo tra gli ele-menti presenti nel relativo sottoalbero. In generale, i nodi nell'i-esimo livello dal-l'alto (dove assumiamo che la radice è a livello 1) contengono gli elementi miniminei relativi sottoalberi, se i è dispari, e gli elementi massimi, se i è pari. Implemen-tate tale struttura con le operazioni Enqueue, ExtractMin, ExtractMax, cheinseriscono un elemento ed estraggono il minimo e il massimo, rispettivamente.

3. Nella trattazione dell'algoritmo di Dijkst ra abbi amo implici tamente supposto chetra ogni coppia di nodi esista un solo cammino minimo. Dimostrate che la corret-tezza e l'efficienza dell'algoritmo valgono anche nel caso generale in cui possonoesistere più cammini minimi tra due nodi. Mostrate anche che i cammini selezio-nati dall'algoritmo tramite l'array pred formano un albero dei cammini minimicon radice nel nodo di partenza s.

4. Impleme ntate uno heap di grado predefinito d > 2: a tal fine, prevedete una

funzione Crea(d) che crea uno heap del grado specificato.

5. Mostra te come utilizzare l'algori tmo di Dijkstra per determinare, dato un nodo v,il ciclo semplice di lunghezza minima che include tale nodo. Implementate quindiun algoritmo per ottenere il girovita (girth) di un grafo, vale a dire la lunghezzadel ciclo semplice di lunghezza minima nel grafo stesso.

6. Modificate il codice dell'algoritmo di Bellman-Ford per verificare la presenza di

cicli negativi, cosi come illustrato nel testo.

7. Implementate un semplice algoritmo di clustering basato sul minimo albero di

ricoprimento. L'algoritmo, dato un insieme S di punti in uno spazio a d = 3dimensioni, suddivide tale insieme in k cluster, dove k è un parametro passatoall'algoritmo stesso.

8. Fornit e un controesempio per mostrare che gli algoritmi di Kruskal e Jami k-Primnon calcolano sempre correttamente l'albero minimo di ricoprimento per un grafoorientato.

Page 329: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 329/373

 

Capitolo 9

NP-completezza

SOMMARIO  In questo capitolo finale riprendiamo in esame le classi di complessità introdotte nel primo

capitolo, dandone una definizione formale basata sul concetto di problema decisionale e su

quello di verifica di una soluzione. Introduciamo inoltre il concetto di riduzione polinomia-le e quello di problema NP-completo, dimostrando la NP-completezza di alcuni problemi

computazionali esaminati nel resto del libro e fornendo alcuni suggerimenti su come dimo-strare un nuovo risultato di NP-completezza. Infine, mostriamo come affrontare la difficoltà

computazionale intrinseca di un problema facendo uso di algoritmi polinomiali di approssi-mazione: forniamo un tale algoritmo per il problema del ricoprimento tramite vertici e per 

quello del commesso viaggiatore ristretto a istanze metriche e mostriamo come, in generale,quest'ultimo problema non ammetta algoritmi di approssimazione ma possa essere risolto,

in pratica, mediante euristiche basate sul paradigma della ricerca locale.

DIFFICOLTÀ

1,5 CFU

9.1 Problemi NP-completi

Abbiamo più volte incontrato nei capitoli precedenti problemi per i quali non conoscia-mo algoritmi di risoluzione polinomiali, ma per i quali non siamo neanche in grado didimostrare che tali algoritmi non esistano. In particolare, nel pr imo capitolo abbiamointrodotto il problema del gioco del Sudoku, per poi considerare nel capitolo successi-vo i problemi della partizione di numeri interi e della bisaccia e, nel quinto capitolo, iproblemi della colorazione di grafi e della ricerca del massimo insieme indipendente . Intutti questi casi, abbiamo detto che i problemi erano NP-completi, intendendo con que-sto termine problemi per i quali, nonostante gli sforzi ripetuti di molti ricercatori, non

Page 330: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 330/373

 

conosciamo algoritmi di risoluzione polinomiale e tali che se uno solo di essi è risolvibilein tempo polinomiale, allora ogni problema NP-completo è risolvibile in tempo polino-miale. Per questo motivo, è opin ione diffusa che un problema NP-completo non puòessere risolto in tempo polinomiale, anche se non conosciamo ancora una dimostrazionedi tale affermazione.

Il concetto di problema NP-completo consente di affrontare la risoluzione di unproblema computazionale da due punti di vista complementari. Da un lato, il nostroobiettivo è chiaramente quello di progettare un algoritmo di risoluzione efficiente peril problema in esame: le strut ture di dati e i paradigmi algoritmici discussi in questolibro sono gli strumenti più adatti per tentare di raggiungere quest'obiett ivo. Dall'altrolato, avendo a disposizione la nozione di problema NP-completo, possiamo tentare didimostrare che quello che stiamo analizzando è, probabilmente, intrinsecamente difficilee che la progettazione di un algoritmo polinomiale per esso sarebbe un risultato conconseguenze molto più significative (ovvero, migliaia di altri problemi risulterebberoimprovvisamente risolvibili in tempo polinomiale).

Sebbene mostrare l'intrinseca difficoltà di un problema computazionale possa sem-brare meno interessante che progettare per esso un algoritmo polinomiale di risoluzione,un risultato di NP-completezza ha l'indubbio vantaggio di far rivolgere i nostri sforziverso obiettivi meno ambiziosi: come vedremo al termine di questo capitolo, esistonodiversi modi per affrontare la difficoltà di un problema NP-completo, tra cui uno deipiù esplorati consiste nella progettazione di algoritmi di approssimazione, ovvero algo-ritmi efficienti le cui prestazioni, in termini di qualità della soluzione calcolata (non

necessariamente ottima), siano in qualche modo garantite.

9.1.1 Classi P e NP

Nel primo capitolo abbiamo int rodotto in modo informale le classi di complessità P e NP,evidenziando che un problema contenuto nella prima classe ammette un algoritmo dirisoluzione efficiente, ovvero polinomiale, mentre un problema contenuto nella secondaclasse ammette un algoritmo efficiente di verifica di una soluzione.1

Per formalizzare queste definizioni, restringiamo la nostra attenzione (per ora) ai soli

problemi di decisione, ossia ai problemi la cui soluzione è una risposta binaria — sì o no.Utilizzando i meccanismi di codifica binaria (Paragrafo 1.2.1) delle istanze di un pro-blema decisionale, identifichiamo un problema decisionale con il corrispettivo insieme(potenzialmente infinito) di istanze la cui risposta è "sì": risolvere tale problema consistequindi nel decidere l'appartenenza di una sequenza binaria all'insieme (osserviamo chenon è restrittivo restringersi alle sole sequenze binarie, in quanto il calcolatore stesso ope-

1 L'acronimo NP sta per non-deterministico polinomiale, motivato storicamente dalla definizione dellaclasse NP usando la macchina di Turing non-deterministica.

Page 331: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 331/373

 

ra solo su tale tipo di sequenze). Pertanto , un prob le ma di decisione TT non è altro che

un sottoinsieme dell'insieme di tutte le possibili sequenze binarie, in particolare di quelle

che soddisfano una determinata proprietà.

Ad esempio, il problema di decidere se un dato numero intero è primo consiste di

tutte le sequenze binarie che codificano numeri interi primi, per cui la sequenza 1101(13 in decimale) appartiene a tale problema mentre la stringa 1100 (12 in decimale)non vi appartiene. Il problema di decidere se un grafo può essere colorato con tre coloriconsiste di tutte le sequenze binarie che codificano grafi che possono essere colorati contre colori.

Nel seguito, per motivi di chiarezza, continueremo a definire un problema deci-

sionale facendo uso di descrizioni formulate in linguaggio naturale, anche se implicita-

mente intenderemo definirlo come uno specifico insieme di sequenze binarie, facendo

riferimento a opportuni meccanismi di codifica (Paragrafo 1.2.1).

Un problema decisionale TI appartiene alla classe P se esiste un algoritmo polinomia-le A che, presa in ingresso una sequenza binaria x, determi na se x appartiene a l l o meno .Ad esempio, sappiamo che il problema di decidere se, dato un grafo (orientato) G e duesuoi nodi s e t , esiste un cam mino semplice da s a t appart iene a P, in quanto abbiamovisto nel Paragrafo 7.4.1 come visitare tutti i nodi raggiungibili da s operando una visitain ampiezza del grafo: se t è incluso tra questi vertici, allora la risposta al problema èaffermativa, altrimenti è negativa.

Non sappiamo se il problema di decidere se un grafo può essere colorato con trecolori appartiene a P, ma possiamo mostrare che lo stesso problema ristretto a due colori

vi appartiene: a tale scopo, mostriamo come utilizzare il fatto che se un vertice è coloratocon il primo colore, allora tutti i suoi vicini devono essere colorati con il secondo colore.L'idea dell'algoritmo consiste nel colorare un vertice i con uno dei due colori e dedurretut te le colorazioni degli altri vertici che ne conseguono: se riusciamo a colorare tut tii vertici, possiamo concludere che il grafo è colorabile con due colori. Alt rimenti , searriviamo a una contraddizione (ovvero, siamo costretti a colorare un vertice con duecolori diversi), possiamo dedurre che il grafo non è colorabile con due colori (in realtà,questo algoritmo deve essere applicato a ogni componente connessa del grafo).

Il Codice 9.1 realizza l'algoritmo appena descritto ipotizzando che il grafo in ingres-

so sia connesso: dopo aver cancellato i colori di tutt i i vertici (righe 2 e 3), il codiceprova ad assegnare al primo vertice il colore 1 e aggiorna il numero di vertici colorati,memorizzato nella variabile f a t t i (riga 4). Il ciclo w h i l e successivo dete rmina le even-tuali conseguenze della colorazione attuale: ogni vertice i viene esaminato all'interno delciclo f o r alle righe 6- 21 , per vedere se non è già stato colorato (riga 7). In tal caso,il codice controlla se i vicini di i hanno usato entrambi i colori (righe 8-13): se è così,allora abbiamo trovato una contraddizione (riga 15). Se invece i vicini di i hanno usatouno solo dei due colori, l'altro viene assegnato a i stesso (righe 17 e 18): ovviamente, vi

Page 332: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 332/373

 

DueColorazione( A ): (pre: A matrice di adiacenza di un grafo connesso di n nodi)FOR  (i = 0; i < N; i = i+1)

c o l o r e [ i ] = - 1 ;

c o l o r e [ 0 ] = f a t t i = 1 ;

W HI LE ( f a t t i < n )

FOR  ( i = 0 ; i < N; i = i+1) {

IF ( c o l o r e [ i ] < 0 ) {

u s a t o [ 0 ] = u s a t o t i ] = FALSE;

FOR  ( j = 0 ; j < i ; j = j + 1) {

IF ( A [ j ] [ i ] = = 1 kk c o l o r e [ j ] > = 0 ) {

u s a t o [ c o l o r e [ j ] ] = TRUE;

}>IF ( u s a t o [ 0 ] kk u s a t o t i ] ) {

RETURN FALSE;

> ELSE {IF ( u s a t o [ 0 ] ) { c o l o r e [ i ] = 1; f a t t i = f a t t i + 1; }

IF ( u s a t o t i ] ) { c o l o r e [ i ] = 0 ; f a t t i = f a t t i + 1; }

>>

>R E T U R N T R U E ;

Co di ce 9.1 Algoritmo per decidere se un grafo connesso è colorabi le con due colori.

è anche la possibilità che nessun colore sia stato usato dai vicini di i, nel qual caso nonpossiamo ancora concludere nulla sulla sua colorazione. Al termine del ciclo while, senon troviamo una contraddizione, concludiamo che il grafo è colorabile con due colori(in quanto tutti i nodi sono stati colorati): altrimenti deduciamo che non lo è (riga 22).

A L V I E : colorazione di un grafo con due colori

Osserva, sperimenta e verifica

TwoColoring

Poiché a ogni iterazione del ciclo while coloriamo un nuovo nodo oppure incon-triamo una contraddizione, abbiamo che il numero di iterazioni eseguite dal ciclo è alpiù n. Ciascuna iterazione richiede 0(n 2 ) tempo, per cui la complessità temporale del-l'algoritmo è 0(n 3 ) . Facendo rifer imento alla rappresentazione mediante liste di adia-

Page 333: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 333/373

 

cenza e utilizzando una variante della procedura di visita in ampiezza di un grafo vistanel Paragrafo 7.4.1, possiamo mostrare che tale algoritmo può essere implementato più ,efficientemente in tem po 0 ( n + m) , dove m indica il numero degli archi del grafo.

Migliaia di problemi decisionali appartengono alla classe P e in questo libro ne abbia-mo incontrati diversi. Da questo punto di vista, uno dei risultati recenti più interessantiè stato ottenuto dagli indiani Manindra Agrawal, Neeraj Kayal e Nitin Saxena e consistenella dimostrazione che appartiene a P il problema di decidere se un dato numero intero (

è primo: tale problema aveva resistito all'attacco di centinaia di valenti ricercatori mate-matici e informatici, senza che nessuno fosse stato in grado di progettare un algoritmodi risoluzione polinomiale o di dimostrare che un tale algoritmo non poteva esistere.

La classe P, tuttavia, non esaurisce l'intera gamma dei problemi decisionali: comeabbiamo visto nel primo capitolo, esistono molti problemi per i quali non conosciamoun algoritmo di risoluzione polinomiale e ve ne sono diversi per i quali siamo sicuri chetale algoritmo non esiste (come il problema delle torri di Hanoi).

Introduciamo ora la classe NP che include, oltre a tutti i problemi in P, molti altriproblemi computazionali. Intuit ivamente, un proble ma FI in NP non necessariamenteammette un algoritmo di risoluzione polinomiale, ma è tale che se una sequenza x appar-tiene a Fi allora deve esistere una dimostrazione breve di questo fatto, la quale può essereverificata in tempo pol inomiale. Formalmente, la classe NP include tutti i problemi didecisione Fi per i quali esiste un algoritmo polinomiale V e un polinomio p che, per ognisequenza binaria x, soddisfano le seguenti due condizioni:

completezza: se x appartiene a Fi, allora esiste una sequenza y di lunghezza p(|x|) tale

che V con x e y in ingresso termina restituendo il valore TRUE;

consistenza: se x non appar tiene a FI, allora, per ogni sequenza y, V con x e y in ingressotermina restituendo il valore FALSE.

Osserviamo che P è contenuta in NP. Dato un problema FI in P, sia A un algoritmodi risoluzione pol inomiale per IT. Possiamo allora defin ire V nel mo do seguente: perogni x e y, l'algoritmo V con x e y in ingresso restituisce il valore TRUE se A con x iningresso risponde in mo do affermat ivo, altr imenti restituisce il valore FALSE. Chiara-mente, V (assieme a un qualunque polinomio e senza aver bisogno di usare y) soddisfa

le condizioni di completezza e consistenza sopra descritte: quindi, Fi appartiene a NP.No n sappiamo, invece, se NP è contenuta in P. Dat a l'enorme quant ità di pro-

blemi in NP per i quali non conosciamo un algoritmo polinomiale di risoluzione, lacongettura più accreditata è che P sia diversa da NP. Non avendo una dimostrazionedi quest'affermazione, possiamo solamente individuare all'interno della classe NP i pro-blemi decisionali che maggiormente si prestano a fungere da problemi "separatori" delledue classi: tali problemi sono i problemi NP-completi, che costituiscono i problemi piùdifficili all'interno della classe NP.

Page 334: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 334/373

 

9.1.2 Riducibilità polinomiale

Per poter definire il concetto di problema NP-completo, abbiamo bisogno della nozio-ne di riducibilità polinomiale. Intu itivamente, u n problema di decisione FI è riducibilepolinomialmente a un altro problema di decisione FI' se l'esistenza di un algoritmo di

risoluzione polinomiale per IV implica l'esistenza di un tale algoritmo anche per IT. Ab-biamo già visto come il concetto di riducibilità può essere applicato per dimostrare cheun dato problema computazionale è risolvibile in modo efficiente (pensiamo ad esempioal problema del minimo antenato comune analizzato nel Paragrafo 4.2.1). Forniamo oraun ulteriore esempio di tale applicazione considerando il problema della soddisfacibilitàristretto a clausole con due letterali.

Sia X = {xo.xi,... ,x n _i) un insieme di n variabili booleane. Una formula booleana

in forma normale congiuntiva su X è un insieme C = {co, C i , . . . , c m _i) di m clausole,

dove ciascuna clausola Ct, per 0 ^ i < m, è a sua volta un insieme di letterali, ovvero un

insieme di variabili in X e/o di loro negazioni (indicate con x^). Un'assegnazione di valoriper X è una funzione T : X —>{TRUE, FALSE} che assegna a ogni variabile un valore diverità. Un letterale L è soddisfatto da T se L = Xj e X(xj) = TRUE oppure se L = Xj" ex(XJ) = FALSE, per qualche 0 ^ j < n. Una clausola è soddisfatta da T se almeno un suoletterale lo è e una formula è soddisfatta da T se tutte le sue clausole lo sono.

Il problema della soddisfacibilità (indicato con SAT) consiste nel decidere se unaformula booleana in forma normale congiuntiva è soddisfacibile. In particolare, il pro-blema 2-SAT è la restrizione di SAT al caso in cui le clausole contengano esattamentedue letterali. Per mostrare che 2-SAT è risolvibile in tempo polinomiale, definiamo una

riduzione da 2-SAT al problema di decidere se, dato un grafo orientato G e due nodi s et, esiste un cammino in G da s a t: poiché quest'ultimo problema ammette un algoritmodi risoluzione polinomiale, abbiamo che anche 2-SAT ammette un tale algoritmo.

Data un formula booleana cp in forma normale congiuntiva formata da m clauso-le co,Ci, . . . , c m _i su n variabili xo,xi,... ,x n _i, costruiamo un grafo orientato G =(V, E) contenente 2n vertici (due per ogni variabile booleana) e 2m archi (due per ogniclausola).2 In particolare, per ogni variabile xt, G include due vertici v?° s e v" ee cor-rispondent i, rispettivamente, ai letterali x\ e x[. Inolt re, per ogni clausola {li, I2} dellaformula, G include un arco tra il vertice corrispondente a li e quello corrispondente a

I2 e un arco tra il vertice corrispondente a I2 e quello corrispondente a lj (Figura 9.1):intuitivamente, questi due archi modellano il fatto che se uno dei due letterali dellaclausola non è soddisfatto, allora lo deve essere l'altro letterale.

Notiamo che il grafo G soddisfa la seguente proprietà: se esiste un cammino tra ilvertice corrispondente a un letterale l e il vertice corrispondente a un letterale l', allora

2Senza perdita di generalità, possiamo supporre che nessuna clausola è formata da una variabile e dallasua negazione: in effetti, una tale clausola è soddisfatta da qualunque assegnazione di valori e può, quindi,essere eliminata dalla formula.

Page 335: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 335/373

 

Fi gu ra 9 .1 Un esempio di riduz ione da 2-SAT al problema della ricerca di cammini in un grafo: leclausole sono (x0,x7}, {xo.xj), {xi ,x 2 } e {xf,x5}.

esiste un cammino tra il vertice corrispondente a l' e il vertice corrispondente a T. Adesempio, nella Figura 9.1, esiste il cammino da VQCg a v ^ (che passa per v"eg) ed esisteanche il cammino da v^ 8 a VqOS (che passa per V|0S). Mostriamo ora che cp è soddisfacibilese e solo se, per ogni variabile Xi con 0 ^ i < n, abbiamo che v"eg non è raggiungibileda vP°s e v?°s non è raggiungibile da v"eg (ovvero, se non vi sono contraddizioni in cp).

Sia T un'assegnazione di verità che soddisfa cp e supponiamo, per assurdo, che esistauna variabile Xi tale che v"eg è raggiungibile da v?os e che v?os è raggiungibile da v"eg:supponiamo che t (x ì . ) = TRUE (possiamo gestire il caso opposto in modo simile). Poiché

esiste un cammino da v?os

a v"eg

e poiché T soddisfa x^ ma non soddisfa x^, deve esistereun arco (p, q) di tale cammino tale che il letterale l p a cui corrisponde p è soddisfattomentre il letterale l q a cui corrisponde q non lo è: per definizione di G, cp include laclausola {lp, l q} la quale non è soddisfatta da t, contraddicendo l'ipotesi.

Viceversa, supponiamo che, per ogni variabile x^ con 0 ^ i < n , v"eg non è rag-giungibile da v?os oppure v?°s non è raggiungibile da v"eg , e mostriamo come costruireun'assegnazione di valori T che soddisfa cp ripetendo il seguente procedimento fino aquando a tutte le variabili abbiamo assegnato un valore (notiamo la similitudine traquesto procedimento e l'algoritmo per decidere se un grafo è colorabile con due colori).

Sia l un letterale alla cui variabile corrispondente non abbiamo assegnato un valoree tale che il vertice p corrispondente a I non è raggiungibile dal vertice q corrispondentea l (per ipotesi, tale letterale deve esistere se vi sono ancora variabili a cui non abbiamoassegnato un valore). Estendiamo T in modo da soddisfare tutti i letterali a cui corri-spondono vertici raggiungibili da q e in modo da non soddisfare tutti i letterali a cuicorrispondono vertici raggiungibili da p.

Tale estensione dell'assegnazione non crea contraddizioni, in quanto se i vertici cor-rispondenti a un letterale e alla sua negazione fossero entrambi raggiungibili da q, allora

Page 336: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 336/373

 

(per simmetria) p sarebbe da essi raggiungibile e, quindi, esisterebbe un cammino da q a

p. Inoltre, se un letterale l' corrispondente a un vertice raggiungibile da q fosse già stato

non soddisfatto in un passo precedente, allora p sarebbe raggiungibile dal vertice corri-

spondente alla negazione di L' e, quindi, T avrebbe già assegnato un valore alla variabile

corrispondente a l.

Poiché a ogni passo, estendiamo T soddisfacendo l e tutti i letterali che corrispon-

dono a vertici raggiungibili da q, abbiamo che al termine del procedimento non può

esistere un arco (r, s) tale che il letterale a cui corrisponde r è soddisfatto mentre quello

a cui corrisponde s non lo è. Quindi, T soddisfa tutte le clausole.

Nel caso del grafo mostrato nella Figura 9.1, possiamo scegliere l = Vq0S, in quanto

VQ66 non è raggiungibile da VgOS. Quindi, estendiamo l'assegnazione T (attualmente vuo-

ta) in modo da soddisfare tutti i letterali a cui corrispondono vertici raggiungibili da V qOS,

che sono v"eg e v^05. Pertanto, T(X0) = TRUE, T(XI ) = TRUE e T(X2) = FALSE. Poiché

a ogni variabile abbiamo assegnato un valore, il procedimento ha termine: in effetti, Tsoddisfa le quattro clausole {xo, xì~}, {xo, X2}, {xj, x2} e {xì~, X2}.

9.1.3 Problemi NP-completi

In questo capitolo, siamo principalmente interessati a utilizzare il concetto di riducibilitàper ottenere risultati negativi piuttosto che positivi, ovvero per dimostrare che un pro-blema non è risolvibile facendo uso di risorse temporali limitate. Un semplice esempio ditale applicazione consiste nel dimostrare che il problema geometrico del minimo insieme

convesso ha, in generale, una complessità temporale n ( n l o g n ) . Tale problema consi-ste nel trovare, dato un insieme di punti sul piano, il più piccolo (rispetto all'inclusioneinsiemistica) insieme convesso S che li contiene tutti:3 nella Figura 9.2 mostriamo dueesempi di insiemi convessi di cardinalità min ima. Un pun to p e S è un estremo di S,se esiste un semipiano passante per p tale che p è l'unico punto che giace sulla retta chedelimita il semipiano: il problema del minimo insieme convesso consiste nel calcolaregli estremi di S come una lista (ciclicamente) ordinata di punti (ad esempio, la soluzionenella parte sinistra della Figura 9.2 è data da Po. Pi - P2 e P3).

Notiamo che se i punti dell'istanza del problema giacciono su una parabola (co-

me nella parte destra della Figura 9.2), allora la soluzione al problema del minimoinsieme convesso consiste nella lista dei pun ti ordinata in base alle loro ascisse. Que-sta osservazione ci permette di ridurre il problema dell'ordinamento di n numeri interiQo, d i , . . . , a n _ i a quello del calcolo del mi ni mo ins ieme convesso nel mo do seguente:per ogni i con 0 ^ i ^ n — 1, definiamo un punto di coordinate (ai, a?). Chiaramente,gli n punti cosi costruiti giacciono sulla parabola di equazione y = x2 e quindi il loro

'Ricordiamo che un un insieme S è detto convesso se, per ogni coppia di punti in S, il segmento che liunisce è interamente contenuto in S.

Page 337: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 337/373

 

Figura 9.2 Due esempi di minimo insieme convesso.

minimo insieme convesso consiste nel loro elenco ordinato in base alle loro ascisse: taleelenco è du nque l'o rdi nament o degli n numeri interi. Poiché la costruzione sudde ttapuò essere eseguita in tempo O(n), se il problema del minimo insieme convesso è risol-vibile in tempo o(nlogn), allora anche il problema dell'ordinamento di n numeri interi

è risolvibile in tempo o (n lo gn ) : nel Paragrafo 2.5 .3 abbiamo visto che ciò non è in ge-nerale possibile, per cui abbiamo appena dimostrato un limite inferiore alla complessitàtemporale del problema del minimo insieme convesso.

In generale, se un problema TI è riducibile polinomialmente a un problema fi' e se

sappiamo che IT non ammette un algoritmo di risoluzione polinomiale, allora possiamo

concludere che neanche Fi' ammette un tale algoritmo.

In altre parole, IT' è almeno tanto difficile quanto IT (notiamo che l'uso "positivo"del concetto di riducibilità consiste nell'affermare che FI è almeno tanto facile quantoIT'): quindi, le cattive notizie, ovvero, la non trattabilità di un problema, si propagano

da sinistra verso destra (mentre le buone notizie lo fanno da destra verso sinistra).Per definire la nozione di NP-completezza, introduciamo una restrizione del concetto

di riducibilità in cui ogni istanza del problema di partenza viene trasformata in un'istan-za del problema di arrivo, in modo che le due istanze siano entrambe positive oppureentrambe negative. Formalmente, un problema IT è pol ino mia lmen te trasformabile inun problema Fi' se esiste un algoritmo polinomiale T tale che, per ogni sequenza bina-ria x, vale x e Fi se e solo se T con x in ingresso restituisce una sequenza binaria in Fi'.Chiaramente, se IT è polinomialmente trasformabile in TI', allora Fi è polinomialmente

Page 338: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 338/373

 

riducibile a II': infatti, se esiste un algoritmo polinomiale A di risoluzione per Fi', allorala composizione di T con A fornisce un algoritmo polinomiale di risoluzione per TI.

Un problema di decisione FI è NP-completo se FI appartiene a NP e se ogni altro pro-blema in NP è polinomialmente trasformabile in FI. Quindi, FI è almeno tanto difficilequanto ogni altro problema in NP: in altre parole, se dimostriamo che Fi è in P, alloraabbiamo che l'intera classe NP è contenuta in P (e quindi le due classi coincidono).

E naturale a questo punto chiedersi se esistono problemi NP-completi (anche se illettore avrà già int ui to la risposta a tale domanda) . Inolt re, se P / NP, possono esistereproblemi che né appartengono a P e né sono NP-completi (un possibile candidato diquesto tipo è il problema aperto dell'isomorfismo tra grafi, discusso nel Capitolo 6,del quale non conosciamo un algoritmo polinomiale di risoluzione e nemmeno unadimostrazione di NP-completezza).

Prima di discutere l'esistenza di un problema NP-completo, però, osserviamo che

una volta dimostratane l'esistenza, possiamo sfruttare la proprietà di transitività dellatrasformabilità polinomiale per estendere l'insieme dei problemi siffatti. La definizionedi trasformabilità soddisfa infatti la seguente proprietà: se FIQ è polinomialmente trasfor-mabile in F[[ e Fii è polinomialmente trasformabile in IT2, allora Fio è polinomialmentetrasformabile in FI2. A questo punto, volendo dimostrare che un certo problema com-putazionale FI è NP-completo, possiamo procedere in tre passi: prima dimostriamo cheFI appartiene a NP mostrando l'esistenza del suo certificato polinomiale; poi individuia-mo un altro problema Fi', che già sappiamo essere NP-completo; infine, trasformiamopolinomialmente Fi' in FI.

9.1.4 Teorema di Cook-Levin

Per applicare la strategia sopra esposta dobbiamo necessariamente trovare un primo pro-

blema NP-completo. Il teorema di Cook-Levin afferma che SAT è NP-complet o. Os-

serviamo che SAT appartiene a NP, in quanto ogni formula soddisfacibile ammette una

dimostrazione breve e facile da verificare che consiste nella specifica di un'assegnazione

di valori che soddisfa la formula. La parte difficile del teorema di Cook-Levin consiste,

quindi, nel mostrare che ogni problema in NP è polinomialmente trasformabile in SAT.

Non diamo la dimostrazione del suddetto teorema, ma ci limitiamo a fornire una

breve descrizione dell 'approccio utilizzato. Da to un problema Fi e NP, sappiamo che

esiste un algoritmo polinomiale V e un polinomio p tali che, per ogni sequenza binaria

x, se x Fi, allora esiste una sequenza y di lunghezza p( |x|) tale che V con x e y in

ingresso termina restituendo il valore TRUE, mentre se x £ FI, allora, per ogni sequenza

y, V con x e y in ingresso termina resti tuendo il valore FALSE. L'idea della dimostra-

zione consiste nel costruire, per ogni x, in tempo polinomiale una formula booleana cpx

le cui uniche variabili libere sono p(|x|) variabili yo,yi>• • • >y p(|x|)-i> intendendo con

ciò che la soddisfacibilità della formula dipende solo dai valori assegnati a tali variabili:

Page 339: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 339/373

 

intuitivamente, la variabile yi corrisponde al valore del bit in posizione i all'interno dellasequenza y. La formula <px in un certo senso simula il comportamento di V con x e y

in ingresso ed è soddisfacibile solo se tale computazione termina in modo affermativo(ovvero, se y è una dimostrazione che x appartiene a FI). Il fatto che possiamo costruireuna tale formula non dovrebbe sorprenderci più di tanto, se consideriamo che, in fin deiconti, l'esecuzione di un algoritmo all'interno di un calcolatore avviene attraverso circuitilogici le cui componenti di base sono porte logiche che realizzano la disgiunzione (or), lacongiunzione (and) e la negazione (not).

Per convincerci ulteriormente che la dimostrazione del teorema di Cook-Levin cosìtracciata può effettivamente essere realizzata, mostriamo, ad esempio, come un'istanzadel problema di decidere se un grafo G può essere colorato con tre colori (che è chiara-

mente un problema in NP) può essere descritta mediante una formula booleana (pc- Inparticolare, epe non sarà in forma normale congiuntiva: tuttavia, possiamo facilmentedimostrare che una formula booleana tj) può essere trasformata in tempo polinomiale inuna formula booleana 4>' in forma normale congiuntiva, tale che V |J è soddisfacibile se esolo se è soddisfacibile.

Sappiamo che G = (V, E) può essere rappresentato mediante la matrice di adiacenzaA tale che A[i][j] = 1 se e solo se (i, j) S E. A tale matrice facciamo corrispondere n x nvariabili booleane ciy e usiamo in (pc le seguenti formule booleane, per 0 ^ i, j < n:

(in altre parole, queste formule forzano le variabili a y a rappresentare la matrice diadiacenza di G). Per ogni vertice i € V, introduciamo poi tre variabili booleane Ti, gt ebi che corrispondono ai tre possibili colori che possono essere assegnati al vertice (quindi,queste sono le variabili libere i cui valori di verità forniscono la dimostrazione y). Perimpedire che due colori vengano assegnati allo stesso vertice, usiamo in epe le seguentiformule booleane, per 0 < i < n:

Infine, per verificare che l'assegnazione dei colori ai vertici sia compatibile con gli archidel grafo, epe usa le seguenti formule booleane, per 0 ^ i, j < n:

Ci,j = Qi.j =• [(r t A Tj") V ( g t A g^) V (b t A b, )] = oIJV (n Arfl V ( g i Ag[) V (b t Ab, )

(informalmente, C y afferma che se vi è un arco tra i due vertici i e j, allora questi duevertici non possono avere lo stesso colore). In conclusione, la formula <pc è la seguente:

Bi = (ri A gt A bi) V (i\ A gt A bt ) V (fi A gì A bi)

f\ Ai, jA f\ Bi A / \ Ci,,

Page 340: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 340/373

 

Chiaramente, epe è soddisfacibile se e solo se i vertici di G possono essere colorati con trecolori. Il teorema di Cook-Levin afferma sostanzialmente che quanto abbiamo appenafatto per il problema della colorazione può in realtà essere fatto per qualunque problemain N P .

Notiamo che la NP-completezza di SAT non implica che il problema della soddisfa-cibilità di formule booleane in forma normale disgiuntiva sia anch'esso NP-completo:se una formula è in forma normale disgiuntiva, allora una clausola è soddisfatta daun'assegnazione di valori T se tutti i suoi letterali lo sono e la formula è soddisfatta daT se almeno una sua clausola lo è. In tal caso, possiamo mostrare che il problema dellasoddisfacibilità è risolvibile in tempo polinomiale e, quindi, non è molto probabilmenteNP-completo.

9.1.5 Problemi di ottimizzazione

Prima di passare a dimostrare diversi risultati di NP-completezza, introduciamo il con-cetto di problema di ottimizzazione, inteso in qualche modo come estensione di quellodi problema decisionale.

In un problema di ottimizzazione, a ogni istanza del problema associamo un insiemedi soluzioni possibili e a ciascuna soluzione associamo una misura (che può essere uncosto oppure un profitto): il problema consiste nel trovare, data un'istanza, una soluzioneottima, ovvero una soluzione di misura minima se la misura è un costo, una di misuramassima altrimenti.

Abbiamo già incontrato diversi problemi di ottimizzazione nei capitoli precedenti(ad esempio, il problema della sequenza ottima di moltiplicazioni di matrici del Paragra-fo 2.6.2 oppure quello della sotto-sequenza comune più lunga del Paragrafo 2.7.1).

Osserviamo che a ogni problema di ottimizzazione corrisponde in modo abbastanzanaturale un problema di decisione definito nel modo seguente: data un'istanza del pro-blema e dato un valore k, decidere se la misura della soluzione ottima è inferiore a k (nelcaso di costi) oppure superiore a k (nel caso di profitti).

Nella maggior parte dei problemi di ottimizzazione che sorgono nella realtà, abbiamoche se il corrispondente problema di decisione è risolvibile in tempo polinomiale, allora

anche il problema di ottimizzazione è risolvibile in tempo polinomiale.Ciò è principalmente dovuto al fatto che il valore massimo che la misura di una

soluzione può assumere è limitato esponenzialmente dalla lunghezza dell'istanza: questaosservazione ci consente di ridurre polinomialmente un problema di ottimizzazione alsuo corrispondente problema di decisione, operando in base a un meccanismo simile aquello di ricerca binaria descritto nel Paragrafo 2.4.1.

Quindi, se il problema di decisione associato a un problema di ottimizzazione èNP-completo, abbiamo che quest'ultimo non può essere risolto in tempo polinomiale a

Page 341: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 341/373

 

meno che P = NP: nel paragrafo finale di questo capitolo, analizzeremo in dettaglio unodei più famosi problemi di ottimizzazione intrinsecamente difficili.

9.2 Esempi e tecniche di NP-completezza

A partire da SAT, mostriamo ora come sia possibile verificare la NP-completezza di altri

problemi computaz ionali : alcuni che abbiamo esaminato nei capitoli precedenti e che

abbiamo dichiarato essere NP-completi e altri che useremo come problemi di passaggio

nelle catene di trasformazioni polinomiali.

Per prima cosa, dimostriamo che la restrizione 3-SAT di SAT a clausole formate da

esattamente tre letterali è anch'esso un problema NP-completo (chiaramente 3-SAT è in

NP per lo stesso motivo per cui lo è SAT).

9.2.1 Tecnica di sostituzione locale

Sia C = {co,..., c m _i} un insieme di m clausole costruite a partire dall'insieme X di

variabili booleane {xo,... ,x n i). Vogliamo costruire, in tempo polinomiale, un nuovo

insieme D di clausole, ciascuna di cardinalità 3, costruite a partire da un insieme Z di

variabili booleane e tali che C è soddisfacibile se e solo se D è soddisfacibile . A tale

scopo usiamo una tecnica di trasformazione detta di sostituzione locale, in base alla

quale costruiremo D e Z sostituendo ogni clausola c S C con un sottoinsieme D c di D

in modo indipendente dalle altre clausole di C: l'insieme D è quindi uguale a U c 6 c D c e

Z è l'unione di tutte le variabili booleane che appaiono in D.Data una clausola c = {lo,..., lk-i) dell'insieme C, definiamo D c distinguendo i

seguenti quattro casi:

1. k = 1: in questo caso, Dc = { { l o . U o ' y i K U o . y o ' y Ì ^ ^ ' y o ' y Ì ^ ^ O ' y o ' y i W(chiaramente D c è soddisfacibile se e solo se lo è soddisfatto);

2. k = 2: in questo caso, Dc = {{lo>li>yo}>{lo>t-i>yo} (chiaramente D c è soddisfaci-bile se e solo se lo oppure li è soddisfatto);

3. k = 3: in questo caso, Dc è formato dalla sola clausola c;

4. k > 3: in questo caso, che è il più difficile, l'insieme D c contiene un insieme di

k — 2 clausole collegate tra di loro attraverso nuove variabili booleane e tali che la

loro soddisfacibilità sia equivalente a quella di c. Formalmente, D c è l'insieme

{{io. i i ,yo}.{yo' l2-yi} .{yi . l3.y2}.- - - '{yk-5. l ic -3.yk-4} '{yk-4 ' lk-2.W-i}

Page 342: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 342/373

 

Dalla definizione di D c abbiamo che

Z = XU U {y0c ,yi}u U {yS)u U { y§ , . . . , y f  c l _ 4 }

c6CA|c | = l c€C A|c | =2 ce CA| c | >3

e che la costruzione dell'istanza di 3-SAT può essere eseguita in tempo polinomiale.

Supponiamo che esista un'assegnazione T di verità alle variabili di X che soddisfa C.Quindi, T soddisfa c per ogni clausola c e C: mostr iamo che tale assegnazione può essereestesa alle nuove variabili di tipo y c introdotte nel definire D c , in modo che tutte leclausole in esso contenute siano soddisfatte (da quanto detto sopra, possiamo supporreche |c| > 3). Poiché c è soddisfat ta da T, deve esistere H tale che T soddisfa IH con0 ^ h ^ |c| — 1: estendiamo T assegnando il valore TRUE a tutte le variabili y? con0 ^ i ^ h — 2 e il valore FALSE alle rimanenti variabili. In questo modo, siamo sicuri che

la clausola di D c contenente Ih è soddisfatta (da Ih stesso), le clausole che la precedonosono soddisfatte grazie al loro terzo letterale e quelle che la seguono lo sono grazie al loroprimo letterale.

Viceversa, supponiamo che esista un'assegnazione T di verità alle variabili di Z che

soddisfi tutte le clausole in D e, per assurdo, che tale assegnazione ristretta alle sole

variabili di X no n soddisfi almeno una clausola c e C, ovvero che tutt i i letterali contenuti

in c non siano soddisfatti (di nuovo, ipotizziamo che |c| > 3). Ciò implica che tutte le

variabili di tipo y c devono essere vere, perché altrimenti una delle prime |c| — 3 clausole

in D c non è soddisfatta, contraddicendo l'ipotesi che T soddisfa tutte le clausole in D.

Quindi, x(yf c |_4) = TRUE, ovvero T(yf c |_4) = FALSE: poiché abbiamo supposto cheanche l| c|-2 e t|c|- i n o n sono soddisfatti, l'ultima clausola in D c non è soddisfatta,

contraddicendo nuovamente l'ipotesi che T soddisfa tutte le clausole in D.

In conclusione, abbiamo dimostrato che C è soddisfacibile se e solo se D lo è e,quindi, che il problema SAT è trasformabile in tempo polinomiale nel problema 3-SAT:quindi , quest'u ltimo è NP- comple to. Notiamo che, in mo do simile a quant o fatto per3-SAT, possiamo mostrare la NP-completezza del problema della soddisfacibilità nel casoin cui le clausole contengono esattamente k letterali, per ogni k ^ 3: tale affermazioneno n si estende però al caso in cui k = 2, in quanto, come abbiamo visto nel Paragra-

fo 9.1.2, in questo caso il problema diviene risolvibile in tempo polinomiale e, quindi,difficilmente esso è anche NP-completo.

La NP-completezza di 3-SAT ha un duplice valore: da un lato esso mostra che la dif-ficoltà computazionale del problema della soddisfacibilità non dipende dalla lunghezzadelle clausole (fintanto che queste contengono almeno tre letterali), dall'altro ci consentenel seguito di usare 3-SAT come problema di partenza, il quale, avendo istanze più rego-lari, è più facile da utilizzare per sviluppare trasformazioni volte a dimostrare risultati diN P-completezza.

Page 343: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 343/373

 

Figura 9.3 Un esempio di minimo ricoprimento tramite vertici.

9.2.2 Tecnica di progettazione di componenti

Per dimostrare la NP-completezza del problema del massimo insieme indipendente inun grafo (o meglio della sua versione decisionale), mostriamo prima che il seguenteproblema, detto minimo ricoprimento tramite vertici è NP-completo: dato un grafoG = (V, E) e un intero k > 0, esiste un sottoinsieme V' di V con |V' | < k, tale che ogniarco del grafo è coper to da V' ovvero, per ogni arco (u, v) € E, u € V ' oppure v S V'?

Nella Figura 9.3 mostriamo un esempio di ricoprimento tramite 3 vertici del grafo delleconoscenze discusso nel Paragrafo 6.1.1. Notiamo che, in questo caso, un qualunquesottoinsieme di vertici di cardinalità minore di 3, non può essere un ricoprimento: ineffetti, due vertici sono necessari per coprire il triangolo formato da V], V2 e V4 e unulteriore vertice è necessario per coprire l'arco tra V3 e V5.

Il problema del minimo ricoprimento tramite vertici ammette dimostrazioni brevi everificabili in tempo polinomiale: tali dimostrazioni sono i sottoinsiemi dell'insieme deivertici del grafo che costituiscono un ricoprimento degli archi di cardinalità al più k.

Mostriamo ora che 3-SAT è trasformabile in tempo polinomiale nel problema del

minimo ricoprimento tramite vertici: a tale scopo, faremo uso di una tecnica più so-fisticata di quella vista nel paragrafo precedente, che viene generalmente indicata conil nome di progettazione di componen ti. In particolare, la trasformazione opera defi-nendo, per ogni variabile, una componente (gadget ) del grafo il cui scopo è quello dimodellare l'assegnazione di verità alla variabile e, per ogni clausola, una componente ilcui scopo è quello di modellare la soddisfacibilità della clausola. I due insiemi di compo-nenti sono poi collegati tra di loro per garantire che l'assegnazione alle variabili soddisfitutte le clausole.

Page 344: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 344/373

 

Più precisamente, sia C = {co,..., c m _i} un insieme di m clausole costruite a partire

dall'insieme X di variabili booleane {xo,..., x n_i}, tali che |c-L| = 3 per 0 ^ i < in-

vogliamo definire un grafo G e un intero k tale che C è soddisfacibile se e solo se G

include un ricoprimento di esattamente k vertici.

Per ogni variabile con 0 ^ i < n , G include due vertici v?ero e v[also collegati tra

di loro mediante un arco. Queste sono le componenti di verità del grafo, in quanto ogni

ricoprimento di G deve necessariamente includere almeno un vertice tra vVero e v-also per

0 ^ i < n : il valore di k sarà scelto in modo tale che ne includa esat tamente uno, ovvero

quello corrispondente al valore di verità della variabile corrispondente.

Per ogni clausola Cj con 0 ^ j < m, G include una cricca di tre vertici v?, v- e v2.

Queste sono le componenti corrispondenti alla soddisfacibilità delle clausole, in quanto

ogni ricoprimento di G deve necessariamente includere almeno due vertici tra v9, v' e

v? per 0 ^ j < m: il valore di k sarà scelto in mo do tale che ne includa esat tamente due,

in modo che quello non selezionato corrisponda a un letterale certamente soddisfattoall'interno della clausola corrispondente.

Le componenti di verità e quelle di soddisfacibilità sono collegate tra di loro aggiun-

gendo un arco tra i vertici contenuti nelle prime componenti con i corrispondenti vertici

contenuti nelle seconde component i. Più precisamente, per ogni t, j e k con 0 < i < n,

0 < j < m e 0 ^ h < 3 :

• il vertice vVero è collegato al vertice vj1 se e solo se l' (h + 1)-esimo let terale della

clausola Cj è Xt;

• il vertice v-also è collegato al vertice v-1 se e solo se l'(h + l)-esimo letterale della

clausola Cj è

Nella Figura 9.4 mostriamo il grafo così ottenuto a partire dal seguente insieme di

clausole: {xo, xi , xj}, {xo, xi , X2} e {xo, xi, X2}. Rimane da definire il valore di k: come ab-

biamo già detto, vogliamo che tale valore ci costringa a prendere esattamente un vertice

per ogni componente di verità ed esattamente due vertici per ogni componente di soddi-

sfacibilità. Poiché abbiamo n componenti del primo tipo e m componenti del secondo

tipo, poniamo k = n + 2m.Sia T un'assegnazione di verità che soddisfa C, ovvero tale che, per ogni clausola CJ

con 0 ^ j < m, esiste un letterale soddisfatto contenut o in Cj: indichiamo con Pj la

posizione del pri mo tale letterale, dove 0 ^ p j < 3 . Costr uia mo un ricopr imento V' nel

modo seguente: per 0 ^ i < n, V' include vVero se t(xì) = TRUE, altrimenti include

v[also; inoltre, per 0 ^ j < m, V ' include v-1 dove 0 < h . < 3 e h . ^ p j (notiamo che

l'arco tra vF' e il corrispondente vertice contenuto in una componente di verità è coperto

da ques t'ul timo). Poiché, per ogni comp onente di verità, V' include un vertice e, per

Page 345: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 345/373

 

Figu ra 9. 4 Un esempio di riduz ione da 3-SAT al problema del minimo ricoprimento tramite vertici:le clausole sono {xo, xi, xj}, {xo, xi, xj} e {xo, xi", xj}.

ogni componente di soddisfacibilità, ne include due, abbiamo che V' è un ricoprimentoe che |V' | = n + 2 m = k .

Facendo riferimento all'esempio mostrato nella Figura 9.4, supponiamo che T asse-gni il valore TRUE alla sola variabile xo: in tal caso, V' include i vertici VGERO, VJALSO e ls o.Il primo letterale soddisfatto contenuto nella prima clausola è Xo, che si trova in posizio-ne 0: quindi, V' include i due vertici V q e V q. Analogamente, possiamo mostrare che V'

include i vertici v®, v j . v ' e v j c che, quindi, è un ricoprimento del grafo di cardinalità3 + 6 = 9.

Viceversa, supponiamo che V' sia un ricoprimento di G che include esattamenten + 2m nodi . Ciò implica che V' deve includere un vertice per ogni compo nente diverità e due vertici per ogni componente di soddisfacibilità. Definiamo un'assegnazionedi verità x tale che t(XÌ) = TRUE se e solo se vVero e V per 0 ^ i < n: chiaramente,T è un'assegnazione di verità corretta (ovvero, non assegna alla stessa variabile due valoridi verità diversi). Inoltre, per ogni clausola Cj con 0 ^ j < m, deve esistere h. con0 ^ h. < 3 tale che vj 1 £ V': l'arco che unisce v-1 al vertice corrispondente contenuto

in una componente di verità deve, quindi, essere coperto da quest'ultimo che è inclusoin V . Pertanto , l'(h. + l)- esi mo letterale in Cj è soddisfatto da T e la clausola e, èanch'essa soddisfatta. Facendo sempre riferimento all'esempio mostrato nella Figura 9.4,supponiamo che V' includa i vertici V q I S O , V J A L S O , V ^ S O , v{], V q . V j , vj , V  e V ^ : in tal caso,x assegna il valore FALSE a tut te e tre le variabili booleane. Tale assegnazione soddisfatutte le clausole di C: ad esempio, della componente corrispondente alla prima clausolaV non include il vertice V q ma della componente corrispondente a X2 include il verticevip0 , per cui t(x2) = TRUE e la clausola è soddisfatta.

Page 346: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 346/373

 

Poiché il grafo G può essere costruito in tempo polinomiale a partire dall'insiemedi clausole C, abbiamo che 3-SAT è polinomialmente trasformabile nel problema delminimo ricoprimento tramite vertici e, quindi, che quest'ultimo è NP-completo.

9.2.3 Tecnica di similitudineA partire dal problema del minimo ricoprimento tramite vertici siamo ora in grado didimostrare la NP-completezza del seguente: da to un grafo G = (V, E) e un intero k ^ 0,esiste un sottoinsieme V' di V con |V'| > k, tale che V' è un insieme indipe ndenteovvero, per ogni arco (u, v) £ E, u 0 V' oppure v 0 V'? In questo caso, la trasformazioneè molto più semplice di quelle viste finora e si basa sulla tecnica della similitudine, checonsiste appunto nel mostrare come un problema sia simile a uno già precedentementedimostrato essere NP-completo.

Nel nos tro caso, dat o un grafo G = (V, E), il concet to di simili tudine si manifesta

nell'equivalenza tra il fat to che V C V è un insieme ind ipendente di G e quello cheV — V' è un ricoprimento tramite vertici di G. Infatti, se V' è un insieme indipendente,allora V—V' è un ricoprimento in quanto, se così non fosse, esisterebbe un arco (u, v) £ Etale che u ^ V - V ' e v ^ V - V' : quindi, esisterebbe un arco (u, v) £ E tale cheu £ V' e v £ V' contraddicendo l'ipotesi che V' è un insieme indipendente. Viceversa,se V — V' è un ricoprimento tramite vertici, allora V' è un insieme indipendente inquanto, se così non fosse, esisterebbe un arco (u ,v ) £ E tale che u £ V' e v £ V' :quindi, esisterebbe un arco (u, v) £ E tale che u. 0 V — V' e v 0 V — V' contraddicendol'ipotesi che V — V' è un ricoprimento. Ad esempio, nel caso della Figura 9.3, abbiamo

che l'insieme formato dai vertici vi, V4 e V5 è un ricoprimento tramite vertici, mentrel'insieme complementare formato dai vertici vo, V2 e V3 è un insieme indipendente.Pertanto, il problema del minimo ricoprimento tramite vertici è trasformabile in quellodel massimo insieme indipendente e viceversa.

Notiamo che il problema del massimo insieme indipendente ammette dimostrazionibrevi e verificabili in tempo polinomiale, che altro non sono se non i sottoinsiemi divertici che formano un insieme indipendente. Abbiamo pertanto aggiunto, alla nostralista di problemi NP-completi, il problema del massimo insieme indipendente.

9.2.4 Tecnica di restrizione

L'ultimo esempio di dimostrazione di NP-completezza che forniamo si basa sulla tecni-

ca più semplice in assoluto, detta della restrizione, che consiste nel mostrare come un

problema già noto essere NP-completo sia un caso speciale di un altro problema: da ciò

ovviamente deriva la NP-completezza di quest'ultimo.

Come esempio, consideriamo il problema del minimo insieme di campionamento,

che è definito nel modo seguente: dato un insieme C di sottoinsiemi di un insieme A e

Page 347: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 347/373

 

dato un numero intero k > 0, esiste un sottoinsieme A' di A tale che |A'| ^ k e A ' è u ncampionamento di C ovvero, per ogni insieme C É C , c i l A ' / | ? Possiamo restringerequesto problema a quello del minimo ricoprimento tramite vertici, limitandoci a con-siderare istanze in cui ciascun elemento di C contiene esattamente due elementi di A:intuitivamente, A corrisponde all'insieme dei vertici del grafo e C all'insieme degli archi.

Poiché il problema del minimo ricoprimento tramite vertici è NP-completo e poichéquello del minimo insieme di campionamento ammette dimostrazioni brevi e verificabi-li in tempo polinomiale (costituite dal campione A'), abbiamo che anche quest'ultimoproblema è NP-completo: d'altra parte, se riuscissimo a progettare un algoritmo polino-miale per questo, potremmo ugualmente risolvere il problema del minimo ricoprimentotramite vertici in tempo polinomiale, applicando tale algoritmo alle sole istanze ristrette.

9.2.5 Come dimostrare risultati di NP-completezza

Il concetto di NP-completezza è stato int rodotto alla metà degli anni '70. Da allora,migliaia di problemi computazionali sono stati dimostrati essere NP-completi, di tipo-logie diverse e provenienti da molte aree applicative. Un p un to cruciale nel cercare didimostrare che un nuovo prob lema TI è NP-completo consiste nella scelta del proble-ma da cui partire, ovvero il problema NP-completo TI' che deve essere trasformato inIT (notiamo che un tipico errore che si commette inizialmente è quello di pensare cheIT deve essere trasformato in Fi' e non viceversa). A tale scopo, nel loro libro Algorithm

 Design, Jon Kleinberg e Eva Tardos identificano i seguenti sei tipi primitivi di problema,suggerendo per ciascuno uno o più potenziali candidati a svolgere il ruolo del problema

computazionale FI'.

Prob lemi di sot toinsiemi mass imali. Dat o un insieme di oggetti, cerchiamo un suo sot-

toins ieme di cardinalità massima che soddisfi determinati requisiti: un tipico

esempio di problemi siffatti è il problema del massimo insieme indipendente.

Prob lemi di sot toinsiemi min imali . Dat o un insieme di oggetti, cerchiamo un suo sot-toinsieme di cardinalità minima che soddisfi determinati requisiti: due tipici esem-pi di problemi siffatti sono il problema del minimo ricoprimento tramite vertici equello del minimo insieme di campionamento.

Problemi di pa rti zio namen to . Dat o un insieme di oggetti, cerchiamo una sua partizio-ne nel minor numero possibile di sottoinsiemi disgiunti che soddisfino determi-nati requisiti (in alcuni casi, viene anche richiesto che i sottoinsiemi della partizio-ne siano scelti tra una collezione specificata nell'istanza del problema): un tipicoesempio di problemi siffatti è il problema della colorazione di grafi.

Problemi di ord ina men to. Dat o un insieme di oggetti, cerchiamo un suo ordina mento

che soddisfi determinati requisiti: tipici esempi di problemi di questo tipo sono

Page 348: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 348/373

 

il problema del circuito hamiltoniano e quello del commesso viaggiatore (di cuiparleremo nel prossimo paragrafo).

Problemi numerici. Da to un insieme di numeri interi, cerchiamo un suo sottoinsiemeche soddisfi determinati requisiti: tipici esempi di problemi di questo tipo sono

il problema della part izione e quello della bisaccia. No tiamo che la difficoltà diquesti problemi risiede principalmente nel dover trattare numeri arbitrariamentegrandi: in effetti, i due problemi suddetti, ristretti a istanze in cui i numeri in giocosono polinomialmente limitati rispetto alla lunghezza dell'istanza, sono risolvibiliin tempo polinomiale (Paragrafo 2.7.4).

Problemi di sod dis fac imento di vincoli. Da to un sistema di vincoli espressi, general-mente, mediante formule booleane o equazioni lineari su uno specifico insieme divariabili, cerchiamo un'assegnazione alle variabili che soddisfi il sistema: un tipi-

co esempio di problemi siffatti è il problema della soddisfacibilità (eventualmenteristretto a istanze con clausole contenenti esattamente tre letterali).

Una volta scelto il problema Fi' da cui partire, la progettazione della trasformazionedi TV in n è un compito diffìcile tanto quanto quello di progettare un algoritmo po-linomiale di risoluzione per FI: in effetti, in Computers and Intractability. A Guide to

the Theory of NP-Completeness, Michael Garey e David Johnson suggeriscono di pro-cedere parallelamente nelle due attività, in quanto le difficoltà che si incontrano nellaprogettazione di un algoritmo possono fornire suggerimenti alla progettazione della tra-sformazione e viceversa. Sebbene la capacità di dimostrare risultati di NP-completezza

sia un'abilità che, una volta acquisita, può risultare poi di facile applicazione, non è certopossibile, come nel caso della capacità di sviluppare algoritmi efficienti, spiegarla in mo-do formale. Ciò nondimeno, Steven Skiena, nelle sue dispense di un corso su algoritmi,fornisce i seguenti suggerimenti di cui possiamo tener conto quando ci accingiamo avoler dimostrare che un dato problema è NP-completo:

• rendiamo TV il più semplice possibile (ad esempio, conviene usare 3-SAT invecedi SAT) ;

• rend iamo TI il più difficile possibile, eventualmente aggiungendo (temporanea-mente) vincoli ulteriori;

• identi fich iamo in Fi le soluzioni canoniche e introduciamo qualche forma di pe-nalizzazione nei confronti di una qualunque soluzione che non sia canonica (adesempio, nel caso del problema del minimo ricoprimento tramite vertici, una so-luzione canonica è formata da un vertice per ogni componente di verità e duevertici per ogni componente di soddisfacibilità);

• prima di produrre gadget (nel caso della tecnica della progettazione di comp onen-ti), ragioniamo ad alto livello chiedendoci cosa e come intendiamo fare per forzarea scegliere soluzioni canoniche.

Page 349: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 349/373

 

Per quanto utili, questi suggerimenti sono abbastanza vaghi: nella realtà, non esistealtro modo di imparare a progettare trasformazioni tra problemi computazionali se nonfacendolo. Per questo motivo, in questo capitolo abbiamo preferito fornire pochi esempidi tali trasformazioni, lasciando al lettore il compito di cimentarsi con altri problemi,presi magari dalla lista di problemi NP-completi presenti nel libro di Garey e Johnson.

«9.3 Algoritmi di approssimazione

Dimostrare che un problema è NP-completo significa rinunciare a progettare per esso unalgoritmo polinomiale di risoluzione (a meno che non crediamo che P sia uguale a NP). Aquesto punto, però, ci chiediamo come dobbiamo comportarci: dopo tutto, il problemadeve essere risolto. A questo interrogativo possiamo rispondere in diversi modi. Il primoe il più semplice consiste nell'ignorare la complessità temporale intrinseca del problema,

sviluppare comunque un algoritmo di risoluzione e sperare che nella pratica, ovvero conistanze provenienti dal mondo reale, il tempo di risoluzione sia significativamente minoredi quello previsto: dopo tutto, l'analisi nel caso pessimo, in quanto tale, non ci dice comesi comporterà il nostro algoritmo nel caso di specifiche istanze.

Un secondo approccio, in linea con quello precedente ma matematicamente più fon-dato, consiste nell'analizzare l'algoritmo da noi progettato nel caso medio rispetto a unaspecifica distribuzione di probabilità: questo è quanto abbiamo fatto nel caso dell'algorit-mo di ordinamento per distribuzione (Paragrafo 2.5.4). Vi sono due tipi di problemati-che che sorgono quando vogliamo perseguire tale approccio. La prima consiste nel fatto

che un'analisi probabilistica del comportamento dell'algoritmo è quasi sempre difficilee richiede strumenti di calcolo delle probabilità talvolta molto sofisticati. La seconda e,probabilmente, più grave questione è che l'analisi probabilistica richiede la conoscenzadella distribuzione di probabilità con cui le istanze si presentano nel mondo reale: pur-troppo, quasi mai conosciamo tale distribuzione e, pertanto, siamo costretti a ipotizzareche essa sia una di quelle a noi più familiari, come la distribuzione uniforme.

Un terzo approccio si applica al caso di problemi di ottimizzazione, per i quali a ognisoluzione è associata una misura e il cui scopo consiste nel trovare una soluzione di misuraottimale: in tal caso, possiamo rinunciare alla ricerca di soluzioni ottime e accontentarci

di progettare algoritmi efficienti che producano si soluzioni peggiori, ma non troppo.In particolare, diremo che A è un algoritmo di r-approssimazione per il problema diottimizzazione Fi se, per ogni istanza x di FI, abbiamo che A con x in ingresso restituisceuna soluzione di x la cui misura è al più r volte quella di una soluzione ottima (nel casola misura sia un costo) oppure almeno i-esimo di quella di una soluzione ottima (nelcaso la misura sia un profitto), dove r è una costante reale strettamente maggiore di 1.

Per chiarire meglio tale concetto, consideriamo il problema del mini mo ricoprimentotramite vertici, che nella sua versione di ottimizzazione consiste nel trovare un sottoin-

Page 350: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 350/373

 

sieme dei vertici di un grafo di cardinalità minima che copra tutti gli archi del grafo

stesso. Il Codice 9.2 realizza un algoritmo di approssimazione per tale problema basato

sul paradigma dell'algoritmo goloso (abbiamo già detto che nel caso degli algoritmi di

approssimazione, il paradigma dell'algoritmo goloso risulta spesso essere efficace). In par-

ticolare, dopo aver inizializzato la soluzione ponendola uguale all'insieme vuoto (righe 3

e 4), il codice esamina uno dopo l'altro tutti gli archi del grafo (righe 5-11): ogni qual-

volta ne trova uno i cui due estremi non sono stati selezionati (riga 7), include entrambi

gli estremi nella soluzione (righe 8 e 9).

La complessità temporale del Codice 9.2 è 0(n 2 ), in quanto ogni iterazione dei due

cicli annidati u no den tro l'altro richiede un numero costante di operazioni. Inoltre, la

soluzione prodotta dall'algoritmo è un ricoprimento tramite vertici, poiché ogni arco vie-

ne coperto con due vertici se, al momento in cui viene esaminato, questi sono entrambi

non inclusi nella soluzione e con almeno un vertice in caso contrario.

D'altra parte, non possiamo garantire che tale soluzione sia di cardinalità minima:considerando il grafo mostrato nella Figura 9.3, la soluzione prodotta dall'algoritmo

include tutti e sei i vertici mentre quella di cardinalità minima ne ha solamente tre.

Possiamo però mostrare che la soluzione calcolata dal Codice 9.2 include un numero di

vertici che è sempre minore oppure uguale al doppio della cardinalità di una soluzione

ottima.

A tale scopo, dato un grafo G, notiamo che il sottografo indotto dalla soluzione S

calcolata dall'algoritmo con G in ingresso, è formato da ^ archi a due a due disgiunti,

ovvero senza estremi in comune. Chiaramente, un qualunque ricoprimento di tale sot-

tografo (e qu indi di G) deve includere almeno ^ vertici: pertanto , |S| è min ore oppureuguale al doppio della cardinalità di un qualunque ricoprimento tramite vertici di G e,

quindi, della cardinalità minima.

A L V I E : minimo ricoprimento tramite vertici

Osserva, sperimenta e verifica

Ver texCover

9.4 Opus libri: il problema del commesso viaggiatore

Concludiamo questo capitolo e il libro con un'ultima opera algoritmica, relativa a uno

dei problemi di ottimizzazione più analizzati (in tutte le sue varianti) nel campo dell'in-

formatica e della ricerca operativa, ovvero il problema del commesso viaggiatore.

Page 351: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 351/373

 

RicoprimentoVertici( A ):

(pre: A è la matrice di adiacenza di un grafo di n nodi)FOR  (i = 0; i < n; i = i + 1)

presoti] = FALSE;

FOR  (i = 0; i < n; i = i + 1)

FOR  (j = 0; j < n; j = j + 1) {IF (A[i][j] == 1 && ! preso [i] && !preso[j]) {

presoti] = TRUE;

preso[j] = TRUE;

>}

RETURN preso;

Cod ice 9 .2 Algoritmo per il calcolo di un ricoprimento tramite vertici.

Dato un insieme di città e specificato, per ogni coppia di città, la distanza chilome-

trica per andare dall'una all'altra o viceversa, un commesso viaggiatore si chiede quale sia

il modo più breve per visitare tutte le città una e una sola volta, tornando al termine del

giro alla città di partenza. Consideriamo, ad esempio, la seguente istanza del problema

in cui 9 città olandesi sono analizzate e in cui le distanze chilometriche sono tratte da

una nota guida turistica internazionale:

A B D E H L M R U

Amsterdam 0 101 98 121 20 55 213 73 37Breda 0 30 57 121 72 146 51 73Dordrecht 0 92 94 45 181 24 61Eindhoven 0 136 134 86 113 88Haarlem 0 51 228 70 54L'Aia 0 223 21 62Maastricht 0 202 180Rotterdam 0 57Utrecht 0

Se il commesso viaggiatore decide di percorrere le città secondo il loro ordine alfabe-tico, allora percorre un numero di chilometri pari a

101 + 30 + 92 + 136 + 51 + 22 3 + 202 + 57 + 37 = 929

Supponiamo, invece, che decida di percorrerle nel seguente ordine: Amsterdam, Haar-lem, L'Aia, Rotterdam, Dordrecht, Breda, Maastricht, Eindhoven e Utrecht. In tal caso,il commesso viaggiatore percorre un numero di chilometri pari a

20 + 51 + 21 + 24 + 30 + 146 + 86 + 88 + 37 = 50 3

Page 352: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 352/373

 

Mediante una ricerca esaustiva di tutte le possibili 9! = 362880 permutazioni delle novecittà, possiamo verificare che quest'u ltima è la soluzione migliore possibile. Sfortunata-mente, il commesso viaggiatore non ha altra scelta che applicare un algoritmo esaustivoper trovare la soluzione al suo problema, in quanto la sua versione decisionale è un pro-blema NP-completo. Più precisamente, consideriamo il seguente problema di decisione:dati un grafo completo G = (V, E), una funzione p che associa a ogni arco del grafo unnumero intero non negativo e un numero intero k ^ 0, esiste un tour del commesso viag-giatore di peso non superiore a k, ovvero un ciclo hamiltoniano in G (Paragrafo 6.1.1) lasomma dei cui archi è minore oppure uguale a k?

Per dimostrare che tale problema è NP-completo, consideriamo il problema del cir-cuito hamiltoniano che consiste nel decidere se un grafo qualsiasi include un ciclo hamil-toniano. Utilizzando la tecnica della progettazione di componenti possiamo dimostrareche tale problema è NP-completo. Mostriamo ora che la ricerca di un ciclo hamiltonianoè trasformabile in tempo polinomiale nella versione decisionale del problema del com-messo viaggiatore. Dato un grafo G = (V, E), definiamo un grafo completo G ' = (V, E')e una funzione p tale che, per ogni arco e in E', p(e) = 1 se e G E, altrimenti p(e) = 2.Scegliendo k = |V|, abbiamo che se esiste un ciclo hamiltoniano in G, allora esiste untour in G ' il cui costo è uguale a k. Viceversa, se non esiste un ciclo hamiltoniano inG, allora ogni tour in G' deve includere almeno un arco il cui peso sia pari a 2, per cuiogni tour ha un costo almeno pari a k + 1. In conclusione, il problema del commessoviaggiatore (nella sua forma decisionale) è NP-completo.

Sfortunatamente, possiamo mostrare che il problema di ottimizzazione non ammetteneanche un algoritmo efficiente di approssimazione. A tale scopo, consideriamo nuova-mente il problema del circuito hamiltoniano e, facendo uso della tecnica detta del gap,dimostriamo che se il problema del commesso viaggiatore ammette un algoritmo effi-ciente di approssimazione, allora il problema del circuito hamiltoniano è risolvibile intempo polinomiale.

Sia r > 1 una costante e sia A un algor itmo polinomiale di r-appross imazione peril problema del commesso viaggiatore. Dato un grafo G = (V, E), definiamo un grafocompleto G' = (V, E') e una funzione p tale che, per ogni arco e in E', p(e) = 1se e e E, altrimenti p(e) = 1 + s|V| dove s > r — 1. Notiamo che G' ammette untour del commesso viaggiatore di costo pari a |V| se e solo se G include un circuitohamilton iano: infatti, un tale tour deve necessariamente usare archi di peso pari a 1,ovvero arphi contenuti in E.

Sia T il tour del commesso viaggiatore che viene restituito da A con G ' in ingresso.Dimostriamo che T può essere usato per decidere se G ammette un ciclo hamiltoniano,distinguendo i seguenti due casi.

1. Il costo di T è uguale a |V|, per cui T è un tour ott imo. Qu indi , G ammette unciclo hamiltoniano.

Page 353: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 353/373

 

CommessoViaggiatoreC P ):

(pre: ? è la matrice di adiacenza e dei pesi di un grafo G completo di n nodi)mst = Jarnik-Prim( P );

cicloEuleriano = Euler( mst );

FOR  (i = 0; i < n; i = i +1)

visitatoti] = -1;posizione = 0;

FOR  (i=0; i < 2 x n - 1; i = i +1) {

IF (visitato[ cicloEuleriano[i] ] < 0) {

visitato[ cicloEuleriano[i] ] = posizione;

posizione = posizione + 1;

>>RETURN v i s i t a t o ;

Codice 9.3 Algoritmo per il calcolo di un tour approssimato del commesso viaggiatore.

2. Il costo di T è maggiore di |V|, per cui il suo costo deve essere almeno pari a|V| - 1 + 1 + s|V| = (1 + s)|V] > r|V[. In questo caso, il tour ot ti mo non pu òavere costo pari a |V|, in quanto altrimenti il costo di T è maggiore di r volte ilcosto ottimo, contraddicendo il fatto che A è un algoritmo di r-approssimazione:quindi, non esiste un ciclo hamiltoniano in G.

In conclusione, applicando l'algoritmo A al grafo G '  e verificando se la soluzione resti-tuita da A ha un costo pari oppure maggiore al numero dei vertici, possiamo decidere intempo polinomiale se G ammette un circuito hamiltoniano, contraddicendo il fatto cheil problema del circuito hamiltoniano è NP-completo.

9.4.1 Problema del commesso viaggiatore su istanze metriche

Sebbene il problema del commesso viaggiatore non sia, in generale, risolvibile in modoapprossimato mediante un algoritmo polinomiale, possiamo mostrare che tale proble-

ma, ristretto al caso in cui la funzione che specifica la distanza tra due città soddisfi ladisuguaglianza triangolare, ammette un algoritmo di 2-approssimazione.

Un'istanza del problema del commesso viaggiatore soddisfa la disuguaglianza trian-golare se, per ogni tripla di vertici i, j e k, p(i , j) ^ p( i, k) + p(k , j): intuit ivamente, ciòvuol dire che andare in modo diretto da una città i a una città j non può essere più costo-so che andare da i a j passando prima per un'altra città k. L'esempio delle città olandesivisto in precedenza soddisfa la disuguaglianza triangolare, anche se tale disuguaglianzanon sempre è soddisfatta quando si tratta di distanze stradali.

Page 354: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 354/373

 

La disuguaglianza triangolare è invece sempre soddisfatta se i vertici del grafo rap-presentano punti del piano euclideo e le distanze tra due vertici corrispondono alle lorodistanze nel piano, in quanto in ogni triangolo la lunghezza di un Iato è sempre minoredella somma delle lunghezze degli altri due lati. Inoltre, la disuguaglianza è soddisfattanel caso in cui il grafo completo G sia ottenuto nel modo seguente, a partire da un grafoG ' connesso non necessariamente completo: G ha gli stessi vertici di G ' e la distanza tradue suoi vertici è uguale alla lunghezza del cammino minimo tra i corrispondenti verticidi G'. Per questo motivo, la risoluzione del problema del commesso viaggiatore, ristrettoal caso di istanze che soddisfano la disuguaglianza triangolare, è un problema di per séinteressante che sorge abbastanza naturalmente in diverse aree applicative.

La versione decisionale di tale problema è NP-completo, in quanto la trasformazioneche abbiamo mostrato in precedenza a partire dal problema del circuito hamiltonianogenera istanze che soddisfano la disuguaglianza triangolare: se i pesi degli archi sono

solo 1 e 2, ovviamente tale disuguaglianza è sempre soddisfatta. Mostriamo ora unalgoritmo polinomiale di 2-approssimazione per il problema del commesso viaggiatore,ristretto al caso di istanze che soddisfano la disuguaglianza triangolare.

L'idea alla base dell'algoritmo è che la somma dei pesi degli archi di un minimoalbero ricoprente R di un grafo completo G costituisce un limite inferiore al costo di untour ottimo. Infatti, cancellando un arco di un qualsiasi tour T, otteniamo un camminohamiltoniano e, quindi, un albero ricoprente: la somma dei pesi degli archi di questocammino deve essere, per definizione, non inferiore a quella dei pesi degli archi di R.Quindi, il costo di T (che include anche il peso dell'arco cancellato) è certamente non

inferiore alla somma dei pesi degli archi di R.L'algoritmo (realizzato nel Codice 9.3) costruisce un minimo albero ricoprente di G

(riga 3) e, in modo analogo a quanto fatto nel caso del problema del minimo antenatocomune (Paragrafo 4.2.1), visita tale albero in profondità creando un ciclo euleriano(riga 4). Ricordiamo che, in tale ciclo, ogni vertice dell'albero può essere visitato piùdi una volta, ma che ogni arco dell'albero viene percorso esattamente due volte: unaandando dal padre verso il figlio e una tornando dal figlio al padre . Pertanto, il cicloeuleriano è formato da 2n — 1 elementi, di cui il primo e l'ultimo coincidono, comemostrato nella Figura 9.5.

A questo punto, l'algoritmo percorre l'intero ciclo euleriano (righe 8—13): ogni qual-volta incontra un vertice non ancora visitato (riga 9), lo inserisce nel tour del commessoviaggiatore nella posizione attuale (riga 10) e aggiorna quest'ultima (riga 11). In altre pa-role, il codice costruisce il tour del commesso viaggiatore viaggiando attraverso gli archiche uniscono le prime occorrenze di ciascun nodo nel ciclo euleriano.

L'algoritmo restituisce, infine, l'array delle posizioni dei vertici all'interno del tour (ri-ga 14), supponendo implicitamente che il vertice in ultima posizione (ovvero, posizionen — 1) deve essere connesso a quello in prima posizione (ovvero, posizione 0).

Page 355: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 355/373

 

(L)

r ) ( H ) ( R J ( H

r i ) ( A ) C d ) ( A

J ) ( u ) C b ) ( U

Ì ) i | )

m) (M)

Figura 9.5 Un minimo albero ricoprente del grafo delle città olandesi, il corrispondente cicloeuleriano e il tour del commesso viaggiatore.

Poiché il calcolo del minimo albero ricoprente e del corrispondente ciclo eulerianopossono essere realizzati in 0(n 2 logn) e poiché il resto del codice richiede O(n) tempo,abbiamo che l'algoritmo appena descritto è polinomiale. Per dimostrare che è anche unalgoritmo di 2-approssimazione, notiamo anzitutto che la somma dei pesi degli archiinclusi nel ciclo euleriano è al più due volte la somma dei pesi degli archi presenti nelminimo albero ricoprente.

Inoltre, in base alla disuguaglianza triangolare, i salti che vengono eseguiti per co-struire il tour non possono essere più costosi della parte di ciclo euleriano su cui essipassano sopra: quindi, il costo complessivo del tour è al più pari alla somma dei pesidegli archi inclusi nel ciclo euleriano, la quale a sua volta è al più due volte la somma deipesi degli archi presenti nel minimo albero ricoprente che, come abbiamo osservato inprecedenza, non è superiore al costo del tour ottimale.

Più precisamente, sia. io, i-i , . . . , i^—i la sequenza dei nodi inclusi nel ciclo euleriano,dove m = 2rt — 1. Inoltre, siano jo> ) i » • • • > )n-i ' e posizioni delle prime occorrenze degli

Page 356: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 356/373

 

n vertici di G all'interno del ciclo euleriano: quindi, ij0 = 0, ijh / ijk per 0 ^ h. < k < ne ijn_! ^ ij0 . Infine, poniamo j n = m — 1. Il tour del commesso viaggiatore costruitodal Codice 9.3 include gli archi (ij h>ijh+ 1) per 0 ^ h < n. In base alla disuguaglianzatriangolare, abbiamo che

PÌijh'ijK+i) < Piijh'^h + !) +P( i)K + l,ij h + 2) + • • • +p( i j h + 1 - l , i j h + , )

per 0 ^ h. < n : qu indi , il costo del tour calcolato dall' algoritmo è limi tato superiormen tedalla somma dei pesi degli archi inclusi nel ciclo euleriano. In conclus ione, tale tour haun costo pari al più a due volte il costo minimo.

Per concludere, osserviamo che l'algoritmo di approssimazione realizzato dal Co-dice 9.3 non è il migliore possibile. In effetti, con un oppo rt un o accorgimento nelloscegliere gli archi del minimo albero ricoprente da duplicare, possiamo modificare talealgoritmo ottenendone uno di 1,5-approssimazione. Inoltre, nel caso di istanze formate

da pu nt i sul piano euclideo, possiamo dimostrare che, per ogni r > 1, esiste un algor itmopolinomiale di r-approssimazione: in altre parole, il problema del commesso viaggiatoresul piano può essere approssimato tanto bene quanto vogliamo (ovviamente al prezzo diuna complessità temporale che, pur mantenendosi polinomiale, cresce al diminuire di r).

A L V I E : problema del commesso viaggiatore

Osserva, sperimenta e verifica

T ra ve l i ngS a l e s m a n

9.4.2 Paradigma della ricerca locale

Un algoritmo per la risoluzione di un problema di ottimizzazione basato sul paradig-ma della ricerca locale opera nel modo seguente: a partire da una soluzione iniziale delproblema, esplora un insieme di soluzioni "vicine" a quella corrente e si sposta in unasoluzione che è migliore di quella corrente, fino a quando non giunge a una che non

ha nessuna soluzione vicina migliore. Pertanto, il compor tam ento di un tale algoritmodipende dalla nozione di vicinato di una soluzione (solitamente generato applicando ope-razioni di cambiamento locale alla soluzione corrente), dalla soluzione iniziale (che puòessere calcolata mediante un altro algoritmo) e dalla strategia di selezione delle soluzio-ni (ad esempio, scegliendo la prima soluzione vicina migliore di quella corrente oppureselezionando la migliore tra tutte quelle vicine alla corrente).

Non esistono regole generali per decidere quali siano le regole di comportamentomigliori: per questo, ci limitiamo in questo paragrafo finale a descrivere due algoritmi

Page 357: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 357/373

 

basati sul paradigma della ricerca locale per la risoluzione (non ottima) del problemadel commesso viaggiatore. Not iamo sin d'ora che non siamo praticamente in gradodi formulare nessuna affermazione (non banale) relativamente alle prestazioni di questialgoritmi né in termini di complessità temporale né in termini di qualità della soluzioneottenuta. Tuttavia, questo tipo di strategie (dette anche euristiche) risultano nella praticaestremamente valide e, per questo, molto utilizzate.

Entrambi gli algoritmi che descriviamo fanno riferimento a operazioni locali di cam-biamento: essi tuttavia differiscono tra di loro per quello che riguarda la lunghezza mas-

sima della sequenza di tali operazioni. In particolare, i due algoritmi modificano lasoluzione corrente selezionando un numero fissato di archi e sostituendoli con un altroinsieme di archi (della stessa cardinalità) in modo da ottenere un nuovo tour.

Il primo algoritmo, detto 2-opt, opera nel modo seguente: dato un tour T del com-messo viaggiatore, il suo vicinato è costituito da tutti i tour che possono essere ottenuticancellando due archi (x,y) e (u, v) di T e sostituendoli con due nuovi archi (x, u) e(y, v) in modo da ottenere un tour differente T' (notiamo che questo equivale a invertirela percorrenza di una parte del tour T, come mostrato nella Figura 9.6).

Se il nuovo tour T' ha un costo minore di quello di T, allora T' diviene la soluzionecorrente, altrimenti l'algoritmo procede con una diversa coppia di archi: il procedimentoha termine nel momento in cui giungiamo a un tour che non può essere migliorato.

Il Codice 9.4 realizza l'algor itmo 2-opt. Do po aver inizializzato il tour iniziale eil relativo costo, visitando i vertici nell'ordine in cui appaiono nel grafo (righe 3—5),il codice esamina tut te le coppie di archi (i, i + 1) e ( j , j + I) con 0 < i < } — 1 <n — 1 (righe 6-26) e per ognuna di esse genera il nuovo tour operando la sostituzioneprecedentemente descritta (righe 9-15). Quindi, il codice calcola il costo del nuovo tour(righe 16—19): se tale costo è minore del costo precedente, allora il tour corrente viene

Page 358: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 358/373

 

2-0pt( P ):

{pre: ? è la matrice di adiacenza e dei pesi di un grafoG completo di n nodi)costo = 0;

FOR  (i = 0; i < N; i = i +1)

{ tour[i] = i; costo = costo + P[i] [(i+1) '/, n] ; }

FOR  (i = 0; i < N; i = i +1) {FOR  (j = i+2; j < n-1; j = j + 1) {

FOR  (h = 0; h <= i; h = h +1)

nuovo[h] = tour[h];

nuovo[i+l] = tour[j];

FOR (H = 1; h < j-i; h = h +1)

nuovo[i+1+h] = tour[j-h];

nuovo[j+l] = tour[j+l] ;

FOR  (h = J+2; H < n; h = h +1)

nuovo[h] = tour[h];

nuovoCosto = 0;FOR  (h = 0; h < n; h = h +1) {

nuovoCosto = nuovoCosto + P[nuovo[h]][nuovo[(h+1) '/, n]];

>IF (nuovoCosto < costo) {

{ costo = nuovoCosto; i = 0; }

FOR  (h = 0; h < n; h = h+1)

tour[h] = nuovoTour[h];

>>

>R ETU RN t o u r ;

Codice 9.4 Algoritmo 2-opt.

aggiornato e il ciclo f o r più esterno viene fatto riparti re dall'inizio (righe 21 e 23). Senon troviamo nessun tour migliore di quello attuale, allora il codice restituisce il tourattuale come soluzione del problema (riga 27).

Poiché, ogni qualvolta viene trovato un tour di costo minore, tale costo diminuiscealmeno di un'unità , abbi amo che il numer o totale di iterazioni del ciclo f o r più esternoè limitato dal costo del tour iniziale: per tanto, il Cod ice 9.4 termina in tempo 0 ( n 3 C )dove C indica il costo del tour iniziale.

Il secondo algoritmo di risoluzione del problema del commesso viaggiatore basatosul paradigma della ricerca locale è detto 3-opt, in quanto opera in modo analogo a 2-opt, ma considera come vicinato di un tour T tutti i tour che possono essere ottenutiscambiando tre archi di T.

Page 359: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 359/373

 

Figu ra 9. 7 L'operazione di modifica di un tour realizzata dall'algori tmo 3-opt.

In particolare, se lo, ii,..., i-n-i è il tour corrente, l'algoritmo 3-opt sceglie tre indiciio, ji e Ì2 con jo < - 1 < Ì2 - 2 e sostituisce i tre archi ( i j 0 , i  j0 + i), ( i j 1 , i  j l + 1)

e ( i  j 2 , i j 2 + i ) con gli archi ( i j o , i j | + i ) , ( i h , i  j o + 1 ) e ( i j p i ^ + i ) (notiamo che in questocaso non abbiamo bisogno di invertire una parte del tour corrente, come mostrato nellaFigura 9.7).

Possiamo verificare sperimentalmen te che 3-opt ha delle prestazioni migliori di 2 -op tper quello che riguarda la qualità della soluzione, ma richiede un tempo di calcolo supe-

riore. Sebbene, in linea di principio, possiamo pensare di generalizzare i due algoritmiappena esposti def inendo una strategia k-opt per qua lun que k ^ 2, il miglioramentoche si ottiene nella qualità della soluzione calcolata nel passare da 3-opt a 4-opt nonsembra giustificare il significativo peggioramento delle prestazioni in termini di tempodi esecuzione.

A L V I E : algoritmo 2-opt

Gli algoritmi 2-opt e 3-opt risultano efficaci nella pratica, ma possono avere presta-zioni molto scarse nel caso pessimo (anche in dipendenza della scelta del tour iniziale):in effetti, non conosciamo alcun limite superiore all'approssimazione raggiunta dallasoluzione calcolata da questi algoritmi nel caso generale.

Osserva, sperimenta e verificaTwoOpt

Page 360: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 360/373

 

Ciò nonostante, il fatto che in situazioni reali i due algoritmi si comportino relati-vamente bene fa sì che essi, insieme ad altre euristiche basate sul paradigma della ricercalocale, siano diffusamente utilizzati.

RIEPILOGO  In questo capitolo abbiamo definito le classiP f N P f   abbiamo introdotto il concetto di  problema NP-completo. Abbiamo quindi mostrato laNP-completezza di diversi problemi

computazionali, indicando alcune linee guida su come sia possibile dimostrare un risultatodi NP -completezza. Inoltre, abbiamo introdotto il concetto di algoritmo polinomiale di ap-

  prossimazione, fornendo un tale algoritmo per il problema del ricoprimento tramite vertici e

  per quello del commesso viaggiatore ristretto a istanze metriche. Infine, abbiamo dimostratoche quest'ultimo problema non ammette algoritmi di approssimazione nel caso generale, ma

viene solitamente risolto mediante euristiche basate sul paradigma della ricerca beale, come

l'algoritmo 2-opt e quello 3-opt.

ESERCIZI

1. Facendo rife rimento alla rappresentazione median te liste di adiacenza e utilizzan-do una variante della procedura di visita in ampiezza di un grafo vista nel Para-grafo 7.4.1, mostrate che il problema di decidere se un grafo è colorabile con duecolori è risolvibile in tempo 0 ( n + m) , dove n e m indicano, rispettivamente, ilnumero di nodi e il numero di archi del grafo.

2. Osservando che, date due formule booleane cp e cp' che non contengono la varia-bile booleana x, (pVip'è soddisfacibile se e solo se (cp V x) A (cp' V x), mostrateche una formula booleana t^ può essere trasformata in tempo polinomiale in unaformu la booleana ' in forma normale congiuntiva, tale che tj> è soddisfacibile see solo se è soddisfacibile.

3. Most rate che il problema di decidere se, dato un insieme di clausole ciascuna informa congiuntiva, una di esse è soddisfacibile, può essere risolto in tempo lineare.

4. Sia FI un problema di ottimizzazione tale che, per ogni sua istanza x di dimensionen, la misura della soluzione ottima è limitata da 2 n . Utilizzando la tecnica dellaricerca binaria, dimostrate che se il problema di decisione associato a il è in P,allora Fi è risolvibile in tempo polinomiale.

5. Mostrate mediante la tecnica di progettazione delle componenti che il seguenteproblema è NP-completo: dato un grafo G = (V, E), esiste una colorazione deivertici in V mediante tre colori?

Page 361: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 361/373

 

6. Dato un grafo G, sia Gc = (V, Ec) il grafo complementare di G, tale che (u, v) e Ec

se e solo se (u, v) 0 E. Facendo uso di tale nozione, mostrate per similitudine cheil seguente problema è NP-completo: dato un grafo G = (V, E) e un intero k ^ 0,esiste un sottoinsieme V' di V con |V'| > k, tale che V' induce un grafo completoin G ovvero, per ogni u,v S V', (u, v) S E?

7. Considerate una variante del Codice 9.2 in cui, ogni qualvolta esaminiamo unarco i cui estremi non sono inclusi nella soluzione, decidiamo di inserire nellasoluzione uno solo dei due estremi (scelto a piacere). Mostrate che non è unalgoritmo di approssimazione per il problema del minimo ricoprimento tramitevertici.

8. Considerate una variante del Codice 9.3 in cui, dopo aver calcolato il minimoalbero ricoprente T, determiniamo l'insieme di peso minimo M formato da archi

a due a due disgiunti i cui estremi sono vertici di grado dispari in T (nel casomostrato nella Figura 9.5, tale insieme è formato dal solo arco tra Maastricht eUtrecht). Aggiungiamo gli archi di M a T e calcoliamo un ciclo euleriano per que-sto nuovo grafo: possiamo fare ciò partendo da un nodo i qualsiasi e attraversandogli archi senza mai passare due volte per lo stesso arco. A partire da questo cicloeuleriano, l'algoritmo procede come il Codice 9.3. Mostrate che tale algoritmo èun algoritmo polinomiale di 1,5-approssimazione per il problema del commessoviaggiatore.

9. Scrivete il codice che implementa l'algori tmo 3-opt .

Page 362: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 362/373

 

Appendice A

Notazioni

Notazione Significato

R L'insieme dei numeri realix% y Il resto della divisione intera di x per y

W Il più grande numero intero n ^ x

M Il più piccolo numero intero n ^ x

1*1 Il valore assoluto di x oppure la dimensione di x0(f(n)) L'insieme di funzioni g tali che, per ogni costante c > 0, esiste una

costante no > 0 per cui vale g(n) ^ cf(n), per ogni n > no

0(f(n)) L'insieme di funzioni g tali che, per ogni costante c > 0, esistono unacostante no > 0 e infiniti valori di n > no per cui vale g(n) ^ cf (n )

o(f(n)) L'insieme di funzioni g tali che linin^,*, = 0l o g b

Q Il logaritmo in base b di a, ovvero il numero x tale che b* = aIoga log2an! Il fattoriale n x ( n — 1 ) x (n — 2) x • • • x 2 x 1 per n > 0

(B Il coefficiente binomiale, ovvero k , ( i^ k ) i

2_i = a* i La somma xa + xQ+i + xQ+2 + • • • + Xb-i + (per convenzione, talesomma è pari a 0 se b < a)

HiSS *i La somma di tutti i valori Xi per cui i appartiene all'insieme Sa A b La congiunzione dei due valori booleani a e b, che è vera se e solo se

entrambi a e b sono veria V b La disgiunzione dei due valori booleani a e b, che è vera se e solo se

almeno uno tra a e b è veroa La negazione del valore booleano a, che è vera se e solo se a non è verox e A L'elemento x appartiene all'insieme Ax ^ A L'elemento x non appartiene all'insieme A|A| La cardinalità di A, ovvero il numero di elementi contenuti in AAUB L'unione dei due insiemi A e B, ovvero {x : x e A A x 6 B}A n B L'intersezione dei due insiemi A e B, ovvero {x : x e A V x e B}A - B La differenza dei due insiemi A e B, ovvero {x : x e A A x ^ B}

Page 363: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 363/373

 

Appendice B

Teorema delle ricorrenze

Se f(n) è una funzione e a, (3 e no sono tre costanti tali che 1, |3 > 1 e no > 0,allora l'equazione di ricorrenza

Tinl = /  0 ( 1 ) se n ^ no f )1 1

\ aT ( n / P ) +f ( n ) a l tr imenti

K

'

(dove n/(3 va interpretato come |n/PJ o [n/|3~|) ha le seguenti soluzioni per ogni n :

1. T(n) = 0(f(n)) se esiste una costante y < 1 tale che af (n/ (3) = y f(n);

2. T(n) = 0(f(n) logp n) se af(n/P) = f(n);

3. T(n) = 0(n'°6P ") se esiste una costante y'  > 1 tale af ( n/ |3 ) = y'  f(n).

Per la dimostrazione dell'enunciato, usiamo l'approccio suggerito da Jeff Erickson,ipotizzando per semplicità che no = 1 e applicando il metodo descri tto nel Paragra-fo 2.5.5, in modo da calcolare la forma chiusa di T(n) per l'equazione (B.l ): il pri-mo livello di ricorsione contribuisce con f(n), il secondo con af(n/(3), il terzo cona 2 f (n / |3 2 ) e così via, fino all'ultimo livello dove abbiamo al più a h f ( n / | 3 h ) (in quantoalcune chiamate ricorsive modellate dalla ricorrenza potrebbero essere terminate prima),ottenendo

T(n, , f(n) + af ( £ ) + . . . + B tf ( £ ) + . . . + «H f = £ a i f  ^ 2 )

Osserviamo che il valore di h. è tale che n/|3 H = no = 1, implicando così che h. =O(logpn), di cui teniamo conto nel valutare l'equazione (B.2) nei tre casi previsti dalteorema.

Page 364: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 364/373

 

Appendice B - Teorema delle ricorrenze

Nel primo caso, abbiamo che af(n/(3) = yf(n), dove y < 1. Una semplice in-

duzione su i ^ 0 mostra che o c l f ( n / = y 1 ^ ^ ) - Infa tti, il caso base (0 ^ i ^ 1) è

immediato. Nel caso induttivo, osserviamo che a l + 1 f ( n / | 3 l + 1 ) può essere scritto come

(*f ((£)• /p))-»' (Yf U))(a i f (£))dove abbiamo usato la proprietà che af(x/3) = yf(x) nella prima uguaglianza e l'ipotesi

indutt iva nell'ultima. Ne deriva che poss iamo scrivere l'equazione (B.2) come T(n) ^

L t 0 y f ( n ) = f f n l l t o Y 1 = 0( f (n ) ) inquanto Z t o T ' = 0(1) poichéy < 1.

Nel secondo caso abbiamo ocf(n/|3) = f(n) e possiamo dimostrare per induzio-

ne, con dei passaggi analoghi a quelli del primo caso, che o^ffn/P1-) = f(n), per cui

l'equazione (B.2) diventa T(n ) s$ L i U f  ( n ) = (h + Uff™) = 0 (f (n ) logp n) .

Nel terzo caso, analogamente agli altri, vale a l f ( n / | 3 l ) = (y / ) l f(n): l'equazio-

ne (B.2) diventa T(n ) ^ L Ì U i V M n ) = f ( n ) Z t o W f = 0 ( ( V) h f(n)) , in quan-

t o ^ ì ^ o i V ) 1 — 0( ( y ' ) h ) poiché y'  > 1. Utilizzando la proprietà indutt iva su i =

h. = O(logp a) , ovvero che (y ' ) h f(ri) = a H f ( n / | 3 h ) , possiamo derivare che T(n) =

0 ( a h f ( n / ( 3 h )) = 0(a H f ( l ) ) = O(cx l o ^ n ) = o(2 l o 6 a l o s n / l o &e) = O(n l oB0a), conclu-

dendo la dimostrazione del teorema.

Page 365: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 365/373

 

Indice analitico

1-bilanciato, 165-168

2-3-albero, 177

2-opt, 345-348

3-opt, 346-349

abbinamento, 90-94, 206

accesso diretto, 19, 23, 24, 26, 80, 83,

9 1 , 9 2

accesso sequenziale, 23, 25, 80, 83

accoppiamento perfetto, 206

aciclico, 264, 270, 271, 273, 280

adiacenti, 201

aggregazione, 108, 109, 161, 227, 229-

232 ,234

al-Khwarizmi, Muhammad, 1

alberi, 2

alberi cardinali, 143, 144, 191

alberi di Fibonacci, 165, 166, 197

alberi ordinali, 144-148, 264

albero AVL, 165, 167, 169, 174, 187

albero BFS, 264-266, 270, 280

albero binario, 113-124, 127, 133-138,

142, 143, 145, 148, 149, 162,

165, 283

albero binario di ricerca, 162, 165

albero dei cammini minimi, 316

albero dei suffissi, 195, 196

albero DFS, 267-271, 275

albero di ricoprimento, 264, 266, 307,

3 0 8 , 3 1 0 - 3 1 2 , 3 1 4albero euclideo, 307

algoritmo di r-approssimazione, 337

algoritmo di Bellman-Ford, 295, 302

algoritmo di Dijkstra, 295, 297

algoritmo di Floyd-Warshall, 305algoritmo di Strassen, 60, 68, 215

algoritmo esponenziale, 11

algoritmo goloso, 199, 224, 225, 250,

251 ,338

algoritmo polinomiale, 11, 16—18, 66,

218, 222 ,31 8,31 9, 321,325,

326,335-337, 340-342,344,

348, 349

all pair shortest path, 296

all'indietro, 265, 269

altezza, 95, 117

ALVI E, 3, 4, 7, 12, 13, 17, 28, 31 , 32,

36, 39, 46, 49, 51,62, 69,74,

76, 79, 94, 98, 102, 107, 110,

118, 120, 122, 123, 131, 135,

141, 148, 158, 161, 165, 170,

177,179,182, 188, 189, 194,221,224, 228,233, 237, 243,

248, 255-259, 267, 270, 273,

279, 288, 291,293,299, 303,

306,312,315, 320, 338, 344,

347

analisi ammortizzata, 99, 100, 102, 107,

108, 110-112

antenato, 114

approssimazione, 59, 225, 229, 317, 318,337,338, 340-344, 347-349

Page 366: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 366/373

 

approssimazione di Stirling, 290archi, 199arco, 113, ITI 

arco back, 269

arco cross, 269arco forward, 269array associativo, 156array bidimensionale, 56, 66, 68, 79,

91, 130, 140, 149, 208ASCII, 10Ask, 241assegnazione delle lunghezze d'onda, 216,

217

assegnazione di valori, 307, 322, 323,326,328AT&T Bell Labs, 2AT&T call graph, 226authority, 246

auto-organizzazione, 99, 102, 103, 108,111

autorità, 246awersariale, 94

B-albero, 171-177, 180, 197B-tree, 171backtrack, 15, 22backup, 75basi di dati, 170BFS, 262bilanciato, 121bit o binary digit, 2, 10

bisaccia, 77, 79, 80, 82, 317, 336bitmap, 178blocchi, 171Breadth-First Search (BFS), 262bucket, 219

cammino (orientato o meno), 201, 205,213

cammino hamiltoniano, 206, 207, 342

cammino minimo (pesato o meno), 202-203 ,205 ,295 ,297-301 ,303-306, 342

caso medio, 19

caso pessimo, 19casuale, 52-53Central Processing Unit, 28certificati polinomiali, 17chaining, 157chiave, 37

chiave di ricerca, 151chiusura transitiva, 213, 214ciclico, 270

ciclo, 201, 302ciclo euleriano, 127, 128, 149, 208, 274ciclo hamiltoniano, 207, 208, 340, 341

ciclo orientato, 205cima della pila, 254, 258, 259classe NP, 17, 208, 321classe P, 319

clausole, 322-324, 329-334, 336, 348dient, 156

dipping, 57clique, 204

cluster, 161, 227, 307, 308cluster analysis, 307coda con priorità, 281coefficiente di aggregazione, 227collisione, 156, 157, 161complessità computazionale, 36complessità in spazio, 19

complessità in tempo, 19completamente bilanciato, 121completo, 121

completo a sinistra, 133-135, 283-285,292

componente connessa, 203, 204, 214,230, 253, 273-274, 302, 307,310,319

Page 367: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 367/373

 

componente fortemente connessa, 205,274,275

computer graphics, 56concentratore, 246, 248

congettura di Goldbach, 5connesso, 203contrazione, 211convesso, 324, 325Cook, Stephen, 18

corrispondenza biunivoca, 13, 136, 145,146, 148, 150, 191, 218, 271

costo ammortizzato, 28, 100, 102, 108—110, 197, 288, 301

costo medio, 52, 98, 99, 160costo minimo, 63, 64, 68-70, 82, 295,344

costo ottimo, 69costo uniforme, 19crawler, 241, 261, 262credito, 109

cricca, 204, 216, 218, 221, 227, 230,237,332

Crick, Francis, 2cut, 308cut-off, 235

Data Base Management System, 170data mining, 306database, 170dati satellite, 151, 162decomponibili, 119, 121denso, 200Depth-First Search (DFS), 267di arrivo, 205di destinazione, 205di partenza, 205

diametro, 226, 227, 230, 231, 265digital fingerprint, 156dimensione, 10, 116, 151, 200dinamico, 27, 152

Directed Acyclic Graph (DAG), 271, 272,

275,278, 280diretto, 205discendente, 114

disco, 171distance vector, 295distanza (pesata o meno), 202-203, 296distribuzione del grado, 226, 234distribuzione di Bernoulli, 228, 229distribuzione di Poisson, 229disuguaglianza triangolare, 341-344divide et impera, 23, 40, 41, 43-45, 47,

49, 60, 64, 65, 69, 70, 80, 82,

119divisione, 175dizionario, 151

embedding planare, 211,212entrante, 205entropia, 2

equazioni di ricorrenza, 21, 23, 42, 54,80, 82

Erickson, Jeff, 353esponenziale, 7espressioni regolari, 189estremo, 201etichettato, 201euristiche, 317, 345, 348ExpertRank, 241

Extensible Markup Language (XML), 131

fabbriche di link, 245fattore di caricamento, 158Fibonacci, 21, 165, 166, 301, 315figlio (sinistro, destro), 113-114, 115,

124, 133, 134, 136, 138, 144,162-164, 285

file system, 262finale, 205

finger search, 182

Page 368: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 368/373

 

First Come First Served (FCFS), 29First In First Out (FIFO), 259foglie, 114

foresta di ricoprimento, 307

forma normale congiuntiva, 322, 327,348forma normale disgiuntiva, 328formula booleana, 322, 326, 327, 348formula di Stirling, 229frame buffer, 56, 57fratello, 114

fuori linea, 104, 107, 111fusione, 46-49, 51, 53, 55, 69, 177,

180, 181,257, 293, 295

Godei, Kurt, 4

gadget, 331, 336gap di complessità, 60, 340girovita (girth), 316Google, 241, 245

GoogleMaps, 295grado, 144, 201, 205, 227, 229

grado dell'albero, 144grado in ingresso, 205grado in uscita, 205grafo a intervalli, 216—219, 221-223,

251grafo bipartito, 206, 211,212grafo completo, 204, 211, 307, 340, 342,

349grafo etichettato, 201grafo fortemente connesso, 205grafo planare, 211, 216grafo regolare, 230grafo orientato, 205, 225, 232, 240, 241,

251,261,268, 270, 271,274,275, 277, 280, 294, 296, 322

grafo pesato, 201, 208, 209, 295, 296greedy o goloso, 224

Harel, David, 3hash, 151, 154-161, 171, 178, 179, 183,

187, 189, 190, 197, 266hash doppio, 161

heap, 283, 284heap di Fibonacci, 301, 315Heapsort, 292heaptree, 283-285host, 262hub, 246

Hypertext Induced Topic Selection (HI-TS), 241, 246-249

HyperText Markup Language (HTML),

262

in avanti, 269in linea, 103, 235, 239, 241, 244, 268,

337,347in loco, 36, 47, 51, 289, 291, 292incidente, 201indecidibile, 6independent set, 221indice, 170

information retrieval, 177, 183iniziale, 205inorder, 120insertion sort, 31

insieme indipendente, 199, 221-225, 230,250, 251,317, 331,334, 335

Internet Protocol (IP), 262, 293intervallo, 217

intrinsecamente difficile, 318intrusion detection, 71invariante di scala, 235, 239inversione, 105-107isolato, 201isomorfi, 212iterativa, 155

k-ari, 143

Page 369: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 369/373

 

kernel, 161, 162knapsack, 77

Landau, Gad, 19

Last In First Out (LIFO), 253lati, 200Least Recently Used (LRU), 103legge di potenza, 235letterali, 322Levin, Leonid, 18

limite inferiore, 36, 40, 46, 47, 60, 146,172, 189, 197, 325, 342

limite superiore, 36, 40, 60, 98, 10 0-

102,235, 347lineare, 161link, 225link analysis, 241

lista, 2, 3, 25lista circolare, 88

lista circolare doppia, 89lista di adiacenza, 209, 210, 212, 263,

264,267, 272, 279

lista doppia, 86, 87, 111, 123, 152-154, 178,311

liste a salti, 83, 94

liste invertite, 151, 177-181, 183, 196,

197,239livelli di memoria, 21, 171longest common subsequence (LCS), 72Lucas, Edouard, 6lunghezza, 201

macchina di Turing, 5, 318macro-vertici, 274, 275, 278, 280MapQuest, 295

massimale, 203master theorem, 42match, 90

matrice, 56

matrice di adiacenza, 208, 210, 211-

213, 241,242, 247, 327memoization, 77memoria principale, 49, 53, 161, 162,

171, 173,175,180,197memoria secondaria, 49, 53, 161, 162,171, 172, 175-177, 180

memoria virtuale, 161, 162memorizzazione binarizzata, 144, 145,

280mergesort, 47, 180, 189Message-Digest Algorithm (MD), 155microarray, 307

minimo albero di ricoprimento (MST),307minimo antenato comune (LCA), 113,

125-127, 129, 149, 150, 196,322,342

minimo insieme convesso, 324minimo insieme di campionamento, 334,

335

minimo ricoprimento tramite vertici, 331

modello matematico, 226moltiplicazione veloce, 43, 60, 62motori di ricerca, 178, 180, 239, 241,

250

Move-To-Front (MTF), 103MSNMaps, 295multigrafo, 207

navigatore casuale, 274nodo, 113, 199nodo critico, 167-169, 197nodo interno, 114, 121, 143, 191,291,

304

non orientato, 204non determinismo, 318NP-completo, 18, 80, 216, 221, 317,

318,322,326, 328-331,334-337, 340-342, 348, 349

Page 370: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 370/373

 

numeri di Fibonacci, 62, 166

numero cromatico, 216, 218, 219

numero di Catalan, 63, 142, 146

numero di Fibonacci, 21

numero di Ramsey, 230numero di trasferimenti, 171, 197

Odifreddi, Piergiorgio, 19offline, 104online, 103open addressing, 158Open Directory Project (ODP), 262Open Shortest Path First (OSPF), 295opus libri (opere algoritmiche), 2, 28,

55,89, 99, 113,125,154,161,170, 177,215,239, 256, 261,293,306, 338

ordinamento topologico, 253, 270-272,280

ordinata, 144ordinato, 152ordine, 200orientato, 205ottimo, 36output sensitive, 182

pacchetti, 293-295packet switching, 293padre, 114page fault, 162

PageRank, 241-246, 248,249paradigma della ricerca locale, 344

parentesi bilanciate, 143, 145-148, 150partizione, 75passo elementare, 18

pattern matching, 196

peer-to-peer, 154,156

peggiore, 19

perfect match, 90, 206

perfetto, 90, 157

permutazione, 105, 159, 160, 206, 207

pesato, 201

peso, 281

peso di un cammino, 203

piccolo mondo, 230—234pila, 253

pivot, 50-53, 94

pixel, 56, 57

polinomiale, 8

polinomialmente trasformabile, 325, 326,

334

Portable Document Format (PDF), 257

posizionale, 143

posting, 178postorder, 120

Postscript, 256, 257

potenziale, 47, 110, 195, 245

power law, 235

predecessore, 95

prefisso, 183

preorder, 120

primaria, 170

principio di località temporale, 103probing, 159

problem solving, 2

problema aperto, 326

problema dei matrimoni stabili, 83, 89,

90, 92, 93, 111

problema del commesso viaggiatore, 338,

340-342,344-346, 349

 problema della fermata, 4—6, 10

problema della soddisfacibilità, 322

problema di decisione, 319, 322, 326,

328,340, 348

problema di ottimizzazione, 77, 328, 337,

340,344, 348problemi computazionali, 1-3, 6, 11,

21, 32, 205, 317, 321, 329,

335, 337, 348

Page 371: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 371/373

 

problemi di ottimizzazione, 69problemi intrattabili, 11problemi NP-completi, 18problemi trattabili, 11, 14

prodotto, 57prodotto scalare, 57profondità, 117

progettazione di componenti, 331programmazione dinamica, 23, 62, 69-

72,75-80,130,225,304, 305

proprietà fondamentale delle occorren-ze, 195

pseudo-polinomiale, 80

quadratica, 161

query, 125, 127, 129, 131, 148, 149,239

quicksort, 49, 94, 99raddoppio, 28, 130radice, 113radiosità, 59radixsort, 189raggruppamenti di nodi, 227random, 53, 83, 94, 97-99, 102, 111,

153 ,228 ,233 ,237Random Access Machine (RAM), 19rango, 42, 90, 239, 270rank, 270rapporto aureo, 22

rappresentazione implicita, 133, 135, 136,218,283, 284

rappresentazione succinta, 133, 135, 136,138 ,142 ,146-148

rastering, 57record, 170, 172-175recupero dei documenti, 177red-black tree, 177rendering, 57-59restrizione, 334

reti biologiche, 226reti complesse, 225, 227, 230, 231, 234-

236,250reti di informazioni, 226

reti sociali, 225, 226, 231reti tecnologiche, 226ricerca, 37

ricerca binaria, 37, 39^3, 96, 157, 163,172, 174-176, 187, 328, 348

ricerca locale, 317, 344-346, 348ricerca sequenziale, 37ricoprimento, 334rotazioni, 168-170, 174, 187, 197

round robin, 89, 111router, 190, 293-295Routing Information Protocol, 295

scheduling, 28, 29, 215, 222search spam, 240, 248secondarie, 170

Secure Hash Algorithm (SHA), 155secure sockets layer, 18segmento, 33segmento di somma massima, 32—36,

81selection sort, 30selezione, 54self-adjusting, 103self-organizing, 103semplice, 202sequenza, 3

sequenza di scansione, 159sequenza lineare, 23-25, 27, 29, 37, 55,

83 ,84 ,217sequenza random, 97server, 156Shannon, Claude, 2, 10Shortest Job First (SJF), 29shortest path, 295-297signature file, 178

Page 372: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 372/373

 

similitudine, 334single source shortest path (SSSP), 297sistema lineare ottico, 217sistemi di recupero dei documenti, 177

sistemi distribuiti, 156sistemi distribuiti di condivisione dei fi-le, 156

Six Degrees of Kevin Bacon, 225skip list, 94, 177small world, 231snippet, 240software, 3, 4soglia, 230

somma telescopica, 106, 110sostituzione locale, 329sotto-problema, 41, 43, 63, 69-71, 75-

79,225sotto-sequenza, 33, 50, 72-74, 80, 328sotto-sequenza comune, 72-74, 80, 328sotto-sequenza comune più lunga (LCS),

72sottoalbero, 114

sottografo, 203sottografo indotto, 203, 204, 221, 227,230, 241,338

spanning tree, 264, 307sparso, 200spazio, 19spider, 241split, 175stabile, 81, 90

statico, 152Structured Query Language (SQL), 170Sudoku, 14-18, 317suffix tree, 195surfer, 242sweeping line, 219

tabella di routing, 294, 295tabelle hash, 156

taglio, 308task, 28tempo, 19

teorema di Cook-Levin, 326-328

teorema di Kuratowski-Pontryagin-Wagner,211

teorema di Perron-Frobenius, 249, 250teorema fondamentale delle ricorrenze,

42-44, 46, 48, 54,55,61,82teoria di Ramsey, 230terminali, 201terminazione, 4termine, 178

testo, 178topic drift, 249Torri di Hanoi, 6—10, 16tour, 340-347

trasformata veloce di Fourier, 46trasversale, 269trie, 184trie compatto, 190trovare, 17

Turing, Alan, 4

unario, 190, 194Unicode, 10Uniform Resource Locator (URL), 261—

262union-find, 100universo, 151, 152, 154, 155Unix, 2

uscente, 205

verificare, 17vertici, 199vettore di rango, 242, 249visita, 119, 262visita anticipata, 120visita in ampiezza, 133, 262visita in profondità, 267

Page 373: Strutture Di Dati e Algoritmi

5/7/2018 Strutture Di Dati e Algoritmi - slidepdf.com

http://slidepdf.com/reader/full/strutture-di-dati-e-algoritmi 373/373

 

visita per livelli, 133visita posticipata, 120visita simmetrica, 120von Neumann, John, 19

Watson, James, 2World Wide Web (WWW), 225

YahooMaps, 295

zaino, 77zone, 52