Dizionario Dizionario • Il dizionario è un TDA dove si vogliono eseguire ricerche dove l’informazione è individuata da una chiave che è unica. • La parola del TDA deriva proprio dal dizionario (vocabolario) nel quale: • le parole sono le chiavi di accesso e sono tutte distinte • la definizione che segue la parola (informazione associata alla parola, attributo) caratterizza la parola. Dizionario • Un Dizionario è un contenitore di coppie: Coppia = (chiave, attributo) • la chiave è confrontabile: intero, stringa • la chiave è unica: se si vuole inserire una chiave si deve verificare che non sia presente, se lo è si esegue una sostituzione. • Le operazioni del TDA Dizionario sono: • • • • Albero verifica se è vuoto inserimento ricerca cancellazione Albero Albero binario • Un albero è un insieme di punti, detti nodi, a cui è associata una struttura d’ordine parziale (non tutti gli elementi sono confrontabili). • Un albero binario è un TDA definito nel modo seguente: • un insieme vuoto di nodi • una radice con due sottoalberi sinistro e destro che sono alberi binari. • Il TDA albero binario si può realizzare con un array o con una lista concatenata. • Nella realizzazione con lista concatenata utilizziamo due riferimenti per indicare i due sottoalberi: struct tiponodo{ int chiave; tiponodo *sinistro; tiponodo *destro; }; (par. 9.5) 1 Albero binario • Possiamo rappresentarlo modo seguente: graficamente nel chiave Albero binario • Le foglie sono rappresentate con i due riferimenti uguali a NULL: chiave sinistro destro • • • Si considerano alberi binari completi, perfettamente bilanciati e bilanciati. Albero binario Albero binario • Albero binario completo: da ogni nodo partono sempre due rami e le foglie sono tutte allo stesso livello. • Albero binario perfettamente bilanciato: per ogni nodo il numero dei nodi del sottoalbero sinistro differisce al più di 1 dal numero dei nodi del sottoalbero destro. Albero binario Albero binario • Albero binario bilanciato: per ogni nodo le altezze dei sottoalberi sinistro e destro differiscono al più di 1. • Un albero binario di profondità k perfettamente bilanciato è completo fino al livello k-1. • Un albero binario completo Bk ha n=2k+1-1 nodi e 2k foglie (dimostrazione per induzione). • Tutte le operazioni di ricerca di un nodo (inserimento e cancellazione) su un albero binario bilanciato (perfettamente bilanciato) hanno complessità O(log2 n). 2 Albero binario Albero binario • Albero binario di ricerca: i valori dei nodi del sottoalbero sinistro sono minori della radice e quelli del sottoalbero destro sono maggiori della radice. • Un albero binario di ricerca può essere totalmente sbilanciato; esistono delle tecniche di ribilanciamento che, con un numero finito 1 di assegnazioni sui puntatori, permettono 2 di costruire alberi di ricerca bilanciati. 3 8 4 1 11 9 12 4 Albero binario • Si può “visitare” un albero di ricerca esaminando i nodi in un ordine stabilito. Si hanno le seguenti tipologie di visita dell’albero: • inordine ARB R • preordine R A B A B • postordine A B R Gestione hash di un array di interi dove R è la radice e A e B i sottoalberi sinistro e destro. Gestione hash di un array di interi Gestione hash di un array di interi • Vogliamo inserire dei numeri interi in un array e pensiamo ad una memorizzazione efficiente con lo scopo di ottimizzare successivamente la ricerca del numero. • Esempio. • Pensiamo di memorizzare in un array i numeri di matricola di un corso di n=180 studenti; alla matricola si possono poi far corrispondere ulteriori informazioni riguardanti lo studente. • Oppure potremmo pensare di individuare lo studente tramite la sua postazione al PC del laboratorio: in questo caso avremo 180 numeri diversi per i 180 studenti. In questo secondo caso consideriamo un array di dimensione 180 e memorizziamo al posto i-esimo lo i-esimo studente: l’indice dell’array coincide con l’informazione che vogliamo memorizzare: indice = informazione 3 Gestione hash di un array di interi Gestione hash di un array di interi • Se andiamo ad eseguire una ricerca il costo sarà sempre O(1); l’accesso diretto alla posizione iesima ci fornisce l’informazione cercata: postazione[i] = i • Potremmo pensare di risolvere in modo analogo il problema di memorizzare il numero di matricola, supponiamo compreso tra 575000 e 580000. Se avessimo a disposizione un array di 580000 componenti nuovamente troveremmo la matricola cercata in un tempo O(1) matricola[575231] = 575231 • Possiamo vedere la matricola e la postazione come delle chiavi di accesso all’informazione vera, costituita dai dati dello studente. Se l’insieme delle possibili chiavi coincide con il numero di elementi che vogliamo rappresentare, come nel caso della postazione, allora la scelta è buona. • Nel caso della matricola avremmo un notevole spreco di memoria: un array di 580000 componenti per memorizzare 180 dati. Gestione hash di un array di interi Gestione hash di un array di interi • L’idea pertanto è la seguente: • Vogliamo determinare una funzione h tale che, per ogni valore della chiave (matricola) si abbia: h(chiave) = posizione con 0 ≤ posizione ≤ dim-1 • Una funzione di trasformazione può essere il calcolo del resto della divisione con dim: posizione = h(k) = k mod dim • Si può dimostrare che, con questa scelta della funzione, è bene che sia dim ≥ n del 10-20% e che dim sia un numero primo. Infatti una buona scelta di h comporta che i resti siano, nella maggior parte dei casi, diversi tra loro. • Se fosse dim=100, tutti i numeri del tipo 575100, 575200, . . ., 576100, 576200, . . ., avrebbero lo stesso resto. Gestione hash di un array di interi Gestione hash di un array di interi • Non sempre le chiavi troveranno resti diversi: si parla allora di “collisione” dato che due chiavi dovrebbero andare nello stesso posto. Come risolvere il problema? • Lo spazio nell’array c’è, visto che dim ≥ n , pertanto si effettua una scansione cercando un “posto libero”, provando con il successivo modulo dim: se posizione = k mod dim è occupata, allora si cerca in posizione = (posizione+1) mod dim • Come nella Coda, questa tecnica permette di “fare il giro” dell’array. • Esempio. Consideriamo i seguenti n=9 valori: 1, 3, 4, 15, 18, 20, 22, 35, 48 e proviamo ad inserirli con la tecnica del resto in un array di dim=11 elementi. • Calcoliamo i resti: 1%11 = 1; 3%11 = 3; 4%11 = 4; 15%11 = 4 ma la posizione 4 è occupata • lo spazio in più può dare dei vantaggi • per non sprecarlo cerchiamo una funzione che trasformi la matricola (chiave) in un valore di indice possibile per un array di dimensione dim, con dim un po’ più grande di n=180. 4 Gestione hash di un array di interi • Facciamo: (4+1)%11 = 5%11= 5 che è libera continuiamo: 18%11= 7; 20%11= 9; 22%11= 0; 35%11= 2; 48%11= 4 che è occupata: (4+1)%11 = 5%11= 5 che è occupata (5+1)%11 = 6%11= 6 che è libera I numeri nell’array sono memorizzati in ordine diverso da quello di inserimento (rimescolati): Gestione hash di un array di interi 0 1 2 3 22 1 35 3 4 5 6 7 8 4 15 48 18 9 10 20 • Quanto costa inserire con questa tecnica? • L’inserimento di un numero che trova subito il suo posto è O(1): calcolo del resto, invece di +1; quando c’è una collisione si deve cercare il posto: se n=dim il caso peggiore è O(n), si fa tutto il giro; nel nostro caso abbiamo 12 confronti (per cercare il posto libero) per 9 elementi. • Si può dimostrare che, se dim è “buona”, in media il costo è O(1). Gestione hash di un array di interi Gestione hash di un array di interi • Proviamo ora ad effettuare una ricerca di un valore nell’array. La tecnica per la ricerca sarà la stessa: calcolare il resto della divisione con dim: valore%dim fornisce l’indice in cui cercare il valore (chiave). • Il costo della ricerca di un valore presente è lo stesso di quello dell’inserimento: in media O(1). Per un valore non presente il costo è superiore: può capitare di fare tutto il giro, O(n) nel caso peggiore. • Per costruire l’array dobbiamo dare un significato al posto “vuoto”. Possiamo inizializzare l’array con un valore non appartenente all’universo delle chiavi; ad esempio 0 per le matricole, -1 per le postazioni dei PC, ecc. • Durante la costruzione dell’array il posto vuoto indica la posizione libera in cui poter inserire l’elemento. • Durante la ricerca il posto vuoto indica “elemento non presente”. Gestione hash di un array di interi Gestione hash di un array di interi • Nel caso della ricerca si effettua la stessa scansione facendo il giro dell’array. In caso di array pieno (n=dim) e di elemento non presente, si fa tutto il giro ritornando alla posizione iniziale senza trovarlo. • Per gestire questa situazione, si salva il valore della chiave presente all’indice chiave%dim in una variabile temporanea, si copia in tale posizione l’elemento da cercare, si effettua la ricerca (che avrà esito positivo). • Se l’elemento viene trovato nella posizione iniziale (si è fatto tutto il giro) significa che non c’è, altrimenti lo si è trovato più avanti e ciò significa che c’era stata una collisione. Infine si ripristina il valore salvato. • Si può ottimizzare la ricerca del caso non trovato, inserendo le chiavi in ordine: se si trova un elemento più grande ciò significa che quello più piccolo non c’è. 5 Tavola Hash Tavola Hash • Un Dizionario con chiave intera viene chiamato Tavola. • Nella costruzione dell’array di interi con la tecnica hash, avevamo risolto il problema delle collisioni con un sovradimensionamento della tavola; cerchiamo di risolverlo in altro modo: mantenere le stesse prestazioni e non sprecare spazio. • L’array offre con l’accesso diretto O(1), la lista concatenata offre un inserimento O(1). Tavola Hash Tavola Hash • Costruiamo un array di liste concatenate: la scelta dell’indice nell’array si effettua con chiave trasformata (ridotta) e la collisione si risolve con un inserimento in testa alla lista. • Abbiamo quindi un Tavola realizzata tramite due strutture di dati che prende il nome di Tavola Hash con bucket (bucket è la lista concatenata). • Le prestazioni della Tavola dipendono dalla funzione hash e dalle chiavi. • La funzione h potrebbe fornire sempre lo stesso valore di chiave ridotta: caso peggiore, O(n) Tavola Hash Tavola Hash • Caso favorevole: tutte le liste hanno la stessa lunghezza, n/dim, O(n/dim); se n~dim si ha circa O(1) • Se le chiavi sono uniformemente distribuite, la funzione hash con il resto della divisione con dim è una buona scelta: anche le chiavi ridotte sono uniformemente distribuite. coppia ... coppia coppia coppia coppia • • • • • • Come fare se la chiave è una stringa? • Dobbiamo associare ad una stringa un valore intero per poter poi calcolare il resto. coppia 6 Tavola Hash Tavola Hash • Ricordiamo la notazione posizionale dei numeri: 257 = 2 ×102 + 5 × 101 + 7 × 100 possiamo trasformare una stringa in un numero pensando alla base b = 256 (ASCII): “ABC” = ‘A’ × b2 + ‘B’ × b1 + ‘C’ × b0 • Sarebbe una buona idea, dato che in C++ un char è anche un intero piccolo, ma si avrebbe un numero troppo elevato e spesso overflow. • Spesso per le stringhe viene usato il valore 31 (numero primo) come moltiplicatore, al posto di b: “ABC” = ‘A’ × 312 + ‘B’ × 311 + ‘C’ × 310 • Possiamo quindi calcolare il valore hash per ogni stringa. Riepilogo TDA e strutture di dati TDA TDA • Un Tipo di Dato Astratto è: • un insieme di elementi chiamato dominio del tipo di dato • caratterizzato da un nome • possiede delle funzioni che operano sul dominio • I seguenti TDA sono contenitori caratterizzati dalle loro diverse operazioni : • Pila (Stack) • Coda (Queue) • Lista • Dizionario e Tavola • Albero • operatori, predicati, operatori di creazione • possiede delle costanti che lo caratterizzano 7 Strutture di dati Strutture di dati • Una struttura di dati è un modo di organizzare i dati in un contenitore di dati ed ha una sua propria modalità di accesso. Sono state viste le seguenti strutture: Array Lista concatenata diretto i+1 sì info sì sequenziale p->punt no info e punt no • array • lista concatenata. accesso successivo dimensione massima spazio spostamento dati (inserire e cancellare) Pila Stack (Last In First Out) Coda o Queue (First In First Out) • Le operazioni coinvolgono solo il primo elemento della Pila. • Le operazioni coinvolgono il primo elemento e l'ultimo elemento della Coda. Il TDA viene descritto nella seguente interfaccia: • verifica di Pila vuota • visione della testa • inserimento in testa • estrazione dalla testa • • • • verifica di Coda vuota visione della testa inserimento in coda estrazione dalla testa Lista Dizionario o Dictionary • Generalizza il concetto di sequenza: è un contenitore di informazioni che hanno un ordine posizionale. • Si esegue una scansione lineare tramite una variabile che a partire dal primo elemento della lista si posiziona sull'elemento cercato (se esso è presente). • In una lista si può accedere a qualunque elemento. • E' costituito da coppie (chiave, attributo); la chiave è unica e deve permettere il confronto. • Le operazioni che si possono fare sono: • verifica se è vuoto • ricerca di una chiave • inserimento di una coppia • rimozione della coppia 8 Tavola Hash • Si esegue una trasformazione della chiave per ottimizzarne la memorizzazione: posizione = h(chiave) • h : resto della divisione intera chiave/dim • dim : dimensione della tavola Il linguaggio C++ • Si utilizzano assieme le due strutture dati costruendo un array di liste concatenate (bucket). • Se le chiavi sono n e le liste hanno la stessa lunghezza, le prestazioni sono O(n/dim). C++ C++ • Il linguaggio C++ è un linguaggio di programmazione ad alto livello. • Il linguaggio C++ è dotato di un compilatore che traduce il codice sorgente in un codice binario. • Il compilatore, durante la traduzione in codice macchina, esegue una analisi lessicale, sintattica e semantica. • Il codice binario viene elaborato dal linker che produce il codice eseguibile (aggancio dei vari moduli). • Un programma C++ è composto da una o più unità compilabili (moduli). • Ciascuna unità contiene una o più funzioni e può contenere istruzioni chiamate "direttive" per il preprocessore. • L’istruzione include effettua l’inserimento del file indicato. Alfabeto Unità lessicali • L'alfabeto del linguaggio è costituito dai simboli del codice ASCII. • I primi 128 caratteri vengono usati per scrivere le istruzioni e le unità lessicali del programma. • Si distinguono le lettere minuscole dalle maiuscole. • Lo spazio è un separatore. • Le parole chiave (parole che hanno un particolare significato per il linguaggio) sono riservate (minuscole), ossia non si possono usare come nomi di identificatori. • Una sola unità contiene la funzione main. • Le unità lessicali (token) con cui costruiscono le frasi del linguaggio sono: • identificatori • parole chiave • costanti • separatori • operatori si • I nomi degli identificatori sono costruiti con caratteri alfanumerici e devono iniziare con un carattere alfabetico . 9 Tipi di dato Tipo Intero • Abbiamo visto i seguenti tipi di dato predefiniti: • intero • reale • carattere • logico (non è nello standard) • puntatori • Si sono usate anche le struct del C per gestire record (collezione di elementi di tipo diverso). • A seconda dell'occupazione in byte si hanno i seguenti tipi: short (1, 2), int (2, 4), long (8). Tipo Intero • Si possono rappresentare 2n -1 valori diversi. • Costanti del tipo di dato: - 2n -1 e 2n -1 -1 che delimitano l'intervallo di rappresentazione. • Degli n bit a disposizione, il primo è il bit del segno 0 positivi 1 negativi • I rimanenti n-1 sono per il numero. Tipo Intero • La rappresentazione dell'opposto è definita in complemento a 2 r(x) + r(-x) = 2n • Dato x, per trovare la sua rappresentazione r(x) in base b (b=2) si divide per la base e si prendono i resti. • Per trovare la rappresentazione dell'opposto, indicata con r(-x), si invertono tutti i bit di r(x) fatta eccezione degli 0 a destra e del primo 1 a destra (oppure si invertono tutti i bit e si aggiunge 1). Tipo Reale Mantissa • A seconda dell'occupazione in byte si hanno i seguenti tipi: float (4), double (8) e long double (12). • Dati n bit a disposizione il primo è il bit del segno 0 positivi 1 negativi • Dei rimanenti n-1 bit si ha una ripartizione tra esponente e mantissa. • I numeri reali sono rappresentati in modulo e segno, riferiti alla base 2, con mantissa normalizzata e bit nascosto. • Dato x per costruire la rappresentazione r(x) si deve trasformare in binario il numero reale: parte intera: si divide per la base e si prendono i resti parte frazionaria: si moltiplica per la base e si prendono e parti intere • Si deve poi portare il numero binario in forma normalizzata 1.c1c2c3.... × 2e 10 Esponente Funzione • L'esponente e viene rappresentato con l'eccesso (in traslazione) vale a dire che e deve essere aumentato di 127 per i float (eccesso). Il nuovo esponente (intero) viene trasformato in binario. • Una funzione può avere zero, uno o più argomenti ed ha un esplicito valore scalare di ritorno. • Costanti del tipo di dato minimo reale ≠ 0 float ~ 10-38 massimo reale float ~ 1038 • Ogni funzione è caratterizzata dal tipo del valore restituito. • Se una funzione non restituisce esplicitamente un valore, allora il suo tipo di ritorno è void. Variabile locale Variabile di scambio o parametro • Si chiama blocco un gruppo di istruzioni all'interno di una coppia di parentesi graffe. • Una variabile locale è nota (visibile) solo nel blocco in cui è stata definita. • All’interno del blocco deve essere definita una ed una sola volta. • Una variabile di scambio è una variabile che permette la comunicazione dell’informazione tra funzioni. • Parametro formale: parametro che appare nella intestazione della funzione. • Parametro attuale: parametro (argomento) che appare nell'istruzione di chiamata della funzione. • La corrispondenza tra i parametri è posizionale. Passaggio dei parametri Variabile locale • Il passaggio dei parametri nella chiamata di una funzione (sottoprogramma) può essere: • per valore: • nel sottoprogramma chiamato viene costruito un nuovo spazio per la variabile e in esso viene copiato il valore (scalare) • per indirizzo: • al programma viene passato l'indirizzo di memoria della variabile (per gli array viene passato l’indirizzo della prima componente). Una variabile locale: • appartiene a un blocco o ad una funzione • è visibile all'interno del blocco o della funzione • deve essere inizializzata esplicitamente • viene creata, "nasce", quando viene eseguito l'enunciato in cui è definita • viene eliminata, "muore", quando l'esecuzione del programma esce dal blocco nel quale è stata definita • Ogni funzione contiene uno o più blocchi. 11 Variabile parametro Allocazione in memoria Una variabile parametro (formale): • appartiene a una funzione • è visibile all'interno della funzione • viene inizializzata all'atto della invocazione della funzione (con il valore fornito dall'invocazione) • viene creata, "nasce", quando viene invocata la funzione • viene eliminata, "muore", quando l'esecuzione della funzione termina • La definizione delle variabili comporta una allocazione di spazio durante la compilazione del programma. Lo spazio di memoria utilizzato si chiama Stack. • Durante l'esecuzione del programma si può allocare dello spazio con la funzione new. Lo spazio di memoria utilizzato si chiama Heap. Stack e Heap • Rappresentano parti diverse della memoria: Stack (tipi base) e Heap (elementi creati con new). • Schema della disposizione degli indirizzi di memoria: Algoritmi indirizzi di memoria crescenti Codice di Memoria RuntimeStack libera programma lunghezza cresce verso → fissa la memoria alta Heap ← cresce verso la memoria bassa Algoritmi Strutture di controllo • Un algoritmo (deterministico) è un procedimento di soluzione costituito da un insieme di operazioni eseguibili tale che: esiste una prima operazione, dopo ogni operazione è individuata la successiva, esiste un'ultima operazione. • La risoluzione avviene in due passi: • Con le seguenti strutture di controllo si può scrivere qualsiasi algoritmo: • sequenza • alternativa • ciclo • Queste strutture sono caratterizzate dall'avere un unico punto di ingresso e un unico punto di uscita. • analisi, definizione del problema e progettazione • codifica, trasformazione in un linguaggio programmazione di 12 Iterazione e Ricorsione Divide et impera • La scomposizione in sottoproblemi può essere: • iterativa: • i sottoproblemi sono simili tra loro • ricorsiva: • almeno uno dei sottoproblemi è simile a quello iniziale, ma di dimensione inferiore • E' una strategia generale per impostare algoritmi: • In entrambe le scomposizioni si pone il problema della terminazione. • suddividere l'insieme dei dati in un numero finito di sottoinsiemi sui quali si può operare ricorsivamente. • Se la suddivisione in sottoinsiemi è bilanciata (sottoinsiemi di uguale dimensione) si ottengono algoritmi efficienti. Complessità Complessità computazionale • L'efficienza di un algoritmo si valuta in base all'utilizzo che esso fa delle risorse di calcolo: memoria (spazio), CPU (tempo). • Il tempo che un algoritmo impiega per risolvere un problema è una funzione crescente della dimensione dei dati del problema. Se la dimensione varia, può essere stimato in maniera indipendente dal linguaggio, dal calcolatore e dalla scelta di dati, considerando il numero di operazioni eseguite dall'algoritmo. • Indicata con n (numero naturale) la dimensione dei dati di ingresso, la funzione F(n) che calcola il numero di operazioni viene chiamata complessità computazionale e utilizzata come stima della complessità di tempo. Andamento asintotico della complessità • Se F(n) è crescente con n e se si ha che: F(n) → +∞ per n → +∞ • date le semplificazioni per la valutazione di F(n), si stima il comportamento della funzione per n→+∞ utilizzando le seguenti notazioni: • O (o-grande) • Ω (omega) • Θ (theta) limitazione superiore limitazione inferiore uguale andamento • Si fanno delle semplificazioni individuando quali operazioni dipendono da n. Calcolo della complessità • Nel calcolo della complessità si valuta il comportamento dell'algoritmo su particolari scelte di dati e si distinguono: • un caso peggiore in cui l'algoritmo effettua il massimo numero di operazioni, • un caso favorevole in cui l'algoritmo effettua il minimo numero di operazioni, • un caso medio in cui l'algoritmo effettua un numero medio di operazioni. 13 Calcolo della complessità Classi di complessità • Quando si scrive un algoritmo si deve sempre dare una stima del caso peggiore e di quello favorevole. • Le funzioni di complessità sono distinte in classi che rappresentano i diversi ordini di infinito O(1) costanti O(log (log n)) O(log n) logarimto O(n) lineari O(n*log n) O(nk) polinomi k = 2, 3, ... O(n!), O(nn), O(an) esponenziali (a ≠ 0,1) • Gli algoritmi con complessità esponenziale sono impraticabili • Se la dimensione n dei dati non varia, la funzione di complessità è costante. • Se n si mantiene limitato le considerazioni asintotiche non valgono. Limiti inferiori e superiori • Ogni algoritmo determina una limitazione superiore della complessità del problema. Complessità di algoritmi fondamentali • Calcolo dell‘n-esimo numero di Fibonacci f0 = 1 • Cercare le limitazioni inferiori alla complessità del problema vuol dire cercare algoritmi più efficienti. • Un algoritmo la cui complessità coincide con la limitazione inferiore è detto ottimo. Complessità di algoritmi fondamentali • Ricerca lineare binaria hash fn = fn-1 + fn-2 definizione ricorsiva iterazione con vettore iterazione con scalari moltiplicazione di matrici matrice quadrati (ricorsione) approssimazione numerica tempo O(2n) O(n) O(n) O(n) O(log2 n) O(1) n>1 spazio O(n) O(n) O(1) O(1) O(1) O(1) Complessità di algoritmi fondamentali • Ordinamento caso peggiore O(n) (vettore ordinato) (array di interi) f1 = 1 caso favorevole Ω(1) O(log2 n) Ω(1) O(1)* Ω(1) (in media) (*dipende dalla dimensione dell’array e dalla funzione hash) lineare Θ(n2 /2) bubblesort O(n2 /2) Ω(n) (con varianti) inserimento O(n2 /2) Ω(n) quicksort O(n2 /2) O(n log2 n) anche medio mergesort Θ(n log2 n) sempre • Si può dimostrare che la limitazione inferiore della complessità nel problema dell'ordinamento è Ω(nlog2n): mergesort è ottimo nel caso medio e peggiore, quicksort è ottimo nel caso medio. 14 Un problema divertente: il problema del torneo • Questo problema interessò vari matematici tra i quali Lewis Carrol, più noto per aver scritto Alice nel paese delle meraviglie; egli lo propose nella stagione tennistica inglese del 1883. • In un torneo ad eliminazione diretta si affrontano n giocatori: chi perde viene subito eliminato. Si postula la transitività della bravura: se a perde con b e b perde con c si conclude che a perderebbe comunque con c. Tabellone di un torneo rappresentato su un albero binario tra 24 = 16 giocatori m è il vincitore Il problema del torneo • Supposto n = 2k, i giocatori si affrontano a coppie in una serie di n/2 incontri; successivamente si affrontano i vincitori in una nuova serie di n/4 incontri e così via fino ai quarti di finale (8 giocatori), alle semifinali (4 giocatori) e alla finale (2 giocatori) che deciderà il vincitore del torneo. • Si può rappresentare il tabellone del torneo con un albero binario, completo, con 2k foglie e di profondità k. Il problema del torneo • Un albero binario Bk completo possiede 2k+1-1 nodi dei quali 2k sono foglie (dimostrazione per induzione), pertanto i nodi interni sono 2k-1, vale a dire n-1. • Gli incontri che vengono effettuati sono tanti quanti i nodi interni. • Questo problema è quello di determinare il massimo in un insieme di n elementi, di cui sappiamo occorrono n-1 confronti nei quali gli elementi non-massimi risultano perdenti nel confronto con il massimo. Il problema del torneo Girone di consolazione per il torneo p è il secondo dopo m • Il vincitore della finale è il vincitore del torneo. Ma chi è il secondo? • Se il “secondo in bravura” capita nella stessa metà del tabellone del primo, verrà eliminato prima di arrivare alla finale. • Per non commettere ingiustizie bisogna far eseguire un secondo torneo tra tutti coloro che hanno perduto in un incontro diretto con il primo. • Questi giocatori sono k = log2n, perché k sono gli incontri disputati dal vincitore (k profondità dell'albero). 15 Algoritmo del doppio torneo • Eseguendo lo stesso algoritmo in k-1 (log2n -1) incontri si determinerà il secondo. • L'algoritmo del doppio torneo determina il valore del primo e del secondo eseguendo un numero di confronti uguale a C(n) = n-1 + log2(n) -1 = n + log2(n) -2 per n = 16 C(n) = 18 Algoritmo: primo e secondo se a[1] > a[2] allora altrimenti primo ← secondo ← primo ← secondo ← a[1] a[2] a[2] a[1] //finese per i da 3 a n se primo < a[i] allora secondo ← primo primo ← a[i] altrimenti se secondo < a[i] allora secondo ← a[i] //finese // finese //fineper Algoritmo: primo e secondo Algoritmo del doppio torneo • L'algoritmo, che determina il valore del primo e del secondo, esegue invece nel caso peggiore (il massimo è in uno dei primi due posti, per cui si deve sempre eseguire anche il secondo confronto) un numero di confronti uguale a • Si può dimostrare che, nel problema della determinazione del primo e del secondo, il limite inferiore dei confronti nel caso peggiore è Ω(n + log2(n) -2) C(n) = n-1 + n-2 = 2n -3 per n = 16 C(n) = 46 • Nel caso favorevole (ordine crescente in un array) C(n) = n-1. e per finire … come fa Google a scegliere le pagine più importanti? • Pertanto l'algoritmo del doppio torneo è ottimo nel caso peggiore. Google: il problema del page ranking • Due studenti dell’Università di Stanford, Sergey Brin e Larry Page, hanno dato vita a Google nel 1998, uno dei più importanti motori di ricerca nel Web. • La parola google deriva da googol, termine “inventato” da Milton Sirotta nel 1938 (all’epoca ragazzino e nipote di un matematico), per indicare un numero seguito da 100 cifre zero. • L’azienda che gestisce tale motore di ricerca ha utilizzato questo nome per la vastità di informazioni trattate. 16 Google: il problema del page ranking Google: il problema del page ranking • Uno dei problemi era: • come ordinare le pagine presenti sul web in base alla loro importanza? come definire l’importanza? • I due fondatori si ispirarono all’algoritmo HyperSearch dell’italiano prof. Massimo Marchiori, che all’epoca era ricercatore negli USA e che attualmente insegna al Dipartimento Matematica Pura ed Applicata dell’Università di Padova. • Si possono scegliere varie possibilità: • il numero di volte che una parola viene cercata • il numero di link che partono o arrivano a quella parola • il numero delle pagine importanti che partono o arrivano alla pagina. Google: il problema del page ranking Google: il problema del page ranking • L’idea di Page e Brin era che una pagina ha una sua importanza che deriva dalle sue connessioni (e non dal suo contenuto). • L’importanza di una pagina si trasferisce in parti uguali alle pagine che essa punta (una persona importante dà importanza alle persone che frequenta). • L’importanza di una pagina è data dalla somma dell’importanza che viene dalle pagine che puntano ad essa (una persona è importante se frequenta molte persone importanti). • Vediamo il modello matematico. • Supponiamo che siano n le pagine del web, e definiamo la matrice seguente: Google: il problema del page ranking Google: il problema del page ranking • Esempio. matrice: Sia n = 4 e sia H la seguente 0 1 0 0 1 0 0 1 1 0 0 1 1 1 1 0 • Sommando i valori sulla riga i-esima si trova il numero di link che partono dalla pagina i: ri • Sommando i valori sulla colonna j si trova il numero di pagine che puntano alla pagina i: cj. H= con h11 h21 ... hn1 h12 h22 hn2 h1n h2n ... hnn 1 se c’è un link dalla pagina i alla pagina j 0 altrimenti hij = • Indichiamo ora con xj l’importanza della pagina j; risulta: xj = h1j x1/r1 + h2j x2/r2 + … + hnj xn/rn per j = 1, 2, 3, …, n • Questo è un sistema lineare in n equazioni ed n incognite. Le soluzioni x1, x2, …, xn forniscono il livello di importanza delle n pagine: page rank. • L’equazione usata in Google è un po’ diversa, perché “pesata” con un parametro e le soluzioni che derivano sono tutti valori compresi tra 0 e 1. 17 Google: il problema del page ranking Google: il problema del page ranking • Le pagine attive odierne sono oltre n = 8.5 ×109 • Un sistema lineare con la regola di Cramer si risolve con un numero di operazioni dell’ordine di n!. • Il metodo di eliminazione di Gauss risolve il sistema con circa 2n3/3 operazioni. • Se usassimo quest’ultimo avremmo un numero di operazioni dell’ordine di: 2/3 × (8.5 109)3 ~4.1 × 1029 (miliardi di miliardi di miliardi) di operazioni aritmetiche. • Uno dei calcolatori più veloci al mondo è attualmente il Blue Gene dell’IBM. • Ha una velocità di circa 360 teraflops: 3.6× ×1014 operazioni al secondo (1tera=1000giga) • Per eseguire 4.1× ×1029 operazioni impiegherebbe più di 36 milioni di anni. • Come può essere se Page e Brin calcolano il page rank ogni mese? • Anche un calcolatore mille volte più veloce non risolverebbe il problema. Google: il problema del page ranking • Esistono metodi numerici (metodi che costruiscono soluzioni approssimate che possono essere implementate sul calcolatore) con i quali si riesce a risolvere il sistema lineare, in tempi più brevi. • Si parte da una ipotetica soluzione, si stima un errore e in maniera iterativa la si migliora; si costruisce così una successione che converge indipendentemente dalla approssimazione iniziale: xj (1) , xj (2) , xj (3) , …. xj con un errore di approssimazione che risulta essere: e(k) ≤ λk dove λ (0 < λ < 1) è il modulo del secondo autovalore più grande di una certa matrice. 18