La presentazione è in caricamento. Aspetta per favore

La presentazione è in caricamento. Aspetta per favore

Orario: Martedì 1013 Giovedì 1113 1416 Lab.Martedì 1416 RicevimentoGio.16 aula a.

Presentazioni simili


Presentazione sul tema: "Orario: Martedì 1013 Giovedì 1113 1416 Lab.Martedì 1416 RicevimentoGio.16 aula a."— Transcript della presentazione:

1

2

3

4 Orario: Martedì 1013 Giovedì 1113 1416 Lab.Martedì 1416 RicevimentoGio.16 aula a

5 Problemi, Algoritmi e Strutture Dati

6 Problema: Ordinamento Input: una sequenza di n numeri Output: i numeri ricevuti in input ordinati dal più piccolo al più grande ovvero: dove a π (i) ≤ a π (i+1) π è una opportuna permutazione degli indici 1,…,n

7 Idea per ordinare… Ad ogni passo ho una sottosequenza ordinata in cui inserisco un nuovo elemento dell’input: ordinati elemento da inserire ordinati Non necessariamente ordinati

8 Insertion Sort Insertion-sort(A) 1.for j=2 to length(A) 2.do key = A[j] 3. {insert A[j] in A[1,…,j-1]} 4. i = j-1 5. while i>0 and A[i]>key 6.do A[i+1] = A[i] 7. i=i-1 8. A[i+1] = key

9 Esempio: A = {5,2,4,6,1,3} j=25 2 4 6 1 3 j=32 5 4 6 1 3 j=42 4 5 6 1 3 j=52 4 5 6 1 3 j=61 2 4 5 6 3 j=71 2 3 4 5 6

10 Ordinamento Insertion Sort A[1,…,n] = vettore i,j,key = variabili Numeri ordinati n numeri

11 Problema Algoritmo Strutture Dati OutputInput

12 Strutture dati usate da Insertion-Sort DATI: Insieme di numeri: S OPERAZIONI: read(i) size(S) modify(i,x)

13 Insertion Sort Insertion-sort(A) 1.for j=2 to size(A) 2.do key = read(j) 3. {insert A[j] in A[1,…,j-1]} 4. i = j-1 5. while i>0 and read(i)>key 6.do modify(i+1,read(i)) 7. i=i-1 8. modify(i+1,key)

14 Strutture Dati Astratte DATI + OPERAZIONI “Che cosa” DATI OP 1 OP 2 OP n ……………

15 Esempio di ADS Dati = insieme S di numeri OP 1 = estrai il minimo OP 2 = estrai il massimo OP 3 = restituisci la taglia di S OP 4 = inserisci un nuovo numero in S

16 Insertion sort ADS = DS = Insieme S di numeri + Read, Size, Modify S=“A[1,…,n]” (vettore) Read(i)=“A[i]” Modify(i,x)=“A[i]=x”

17 ADS = che cosa vogliamo ? DS = come lo implementiamo ?

18 Quando una struttura dati è “buona” ? Una DS è buona quando non usa troppe risorse. Risorse Tempo Spazio di memoria Numero di processori …

19 Dimensione del problema Le risorse usate (tempo, spazio) si misurano in funzione della dimensione dell’istanza del problema che vogliamo risolvere. Esempio: se ordiniamo n numeri la taglia dell’input sarà n. se moltiplichiamo due matrici nxn sarà n 2. Etc… Il tempo e lo spazio saranno funzioni in n ( TIME(n), SPACE(n) )

20 Analisi di Insertion Sort Dati: A= Dimensione di A = nc 1 Read(i) impiega tempo c 2 Modify(i,x) impiega tempo c 3 NOTA: c 1, c 2, c 3 sono indipendenti da n e vengono perciò dette costanti

21 Insertion Sort: analisi del costo computazionale t j numero di elementi maggiori di A[j] dipende dai dati in input caso ottimo: t j = 0 caso pessimo: t j = j-1 caso medio: t j = (j-1)/2 tjtj A[j]

22 Insertion Sort: analisi del costo computazionale Insertion-sort(A) 1.for j=2 to length(A) 2.do key = A[j] 3. i = j-1 4. while i>0 and A[i]>key 5.do A[i+1] = A[i] 6. i=i-1 7. A[i+1] = key C1nC1n C 2 n-1 C 3 n-1 C 4 ∑(t j +1) C 5 ∑t j C 6 ∑t j C 7 n-1 j=2 n n n

23 T(n) = c 1 n + c 2 (n-1) + c 3 (n-1) + c 4 ∑(t j +1) + (c 5 +c 6 ) ∑t j + c 7 (n-1) caso ottimo: t j = 0 T(n) = an + b; lineare caso pessimo: t j = j-1 T(n) = an 2 + bn + c; quadratico caso medio: t j = (j-1)/2Esercizio!!!

24 Procedimento di astrazione Costo in microsecondi Costanti c i Costanti a, b, c an 2 + bn + c Ordini di grandezza: quadratico

25 Analisi Asintotica

26 Analisi asintotica Obiettivo: semplificare l’analisi del tempo di esecuzione di un algoritmo prescindendo dai dettagli implementativi o di altro genere. Classificare le funzioni in base al loro comportamento asintotico. Astrazione: come il tempo di esecuzione cresce in funzione della taglia dell’input asintoticamente. Asintoticamente non significa per tutti gli input. Esempio: input di piccole dimensioni.

27 “O grande” Limite superiore asintotico f(n) = O(g(n)), se esistono due costanti c e n 0, t.c. f(n)  cg(n) per n  n 0 f(n) e g(n) sono funzioni non negative Notazione usata ad esempio nell’analisi del costo computazionale nel caso pessimo

28 Limite inferiore asintotico f(n) =  (g(n)) se esistono due costanti c e n 0, t.c. cg(n)  f(n) per n  n 0 Usato per: tempo di esecuzione nel caso ottimo; limiti inferiori di complessità; Esempio: il limite inferiore per la ricerca in array non ordinati è  (n). “  grande”

29 Eliminiamo termini di ordine inferiore e costanti. 50 n log n è O(n log n) 7n - 3 è O(n) 8n 2 log n + 5n 2 + n è O(n 2 log n) Nota: anche se (50 n log n) è O(n 5 ), ci aspettiamo di avere approssimazioni migliori !!!

30 “tight bound” = approssimazione stretta. f(n) =  (g(n)) se esistono c 1, c 2, e n 0, t.c. c 1 g(n)  f(n)  c 2 g(n) per n  n 0 f(n) =  (g(n)) se e solo se f(n) = O(g(n)) e f(n) =  (g(n)) O(f(n)) è spesso usato erroneamente al posto di  (f(n)) “”“”

31 c 2 g(n) c 1 g(n) f(n) Taglia dell’input = n Tempo di esecuzione n0n0 f(n) =  (g(n))

32 ”o piccolo” e ”  piccolo” f(n) = o(g(n)) Analogo stretto di O grande Per ogni c, esiste n 0, t.c. f(n)  cg(n) per n  n 0 Usato per confrontare tempi di esecuzione. Se f(n)=o(g(n)) diciamo che g(n) domina f(n). f(n)=  (g(n)) analogo stretto di   grande.

33 Notazione Asintotica Analogie con i numeri reali f(n) = O(g(n))f  g f(n) =  (g(n))f  g f(n) =  (g(n))f = g f(n) = o(g(n))f < g f(n) =  (g(n))f > g Abuso di notazione: f(n) = O(g(n)) Versione corretta: f(n) appartiene a O(g(n))

34 Non tutte le funzioni si possono confrontare !!! nn 1+sen(n) ??? 1+sen(n) oscilla tra 0 e 2

35 Esempi M = numero grande, es: 10000000;  = numero piccolo, es: 0,0000001; Mn =  (n  ) Log(n) = o(n) [Log(n)] M = o(n  ) n M =o(2 n  ) 2 nM = o(n!) Mn! = o(n n )

36 Limiti e notazione asintotica f(n)/g(n) ---> c allora f(n) =  (g(n)) f(n)/g(n) ---> 0 allora f(n) = o(g(n)) f(n)/g(n) ---> ∞ allora f(n) =  (g(n))

37 Limiti e notazione asintotica log[f(n)] = o(log[g(n)])allora f(n) = o(g(n)) log[f(n)] =  (log[g(n)])non è detto che f(n) = o(g(n)) esempio: log[n] =  (log[n 2 ]) ma n = o(n 2 )

38 Tecniche Algoritmiche: Divide et Impera

39 Divide et Impera Divide et impera: Dividi: Se l’istanza del problema da risolvere è troppo “complicata” per essere risolta direttamente, dividila in due o più “parti” Risolvi ricorsivamente: Usa la stessa tecnica divide et impera per risolvere le singole parti (sottoproblemi) Combina: Combina le soluzioni trovate per i sottoproblemi in una soluzione per il problema originario.

40 MergeSort: Algoritmo Dividi: se S contiene almeno due elementi (un solo elemento è banalmente già ordinato), rimuovi tutti gli elementi da S e inseriscili in due vettori, S 1 e S 2, ognuno dei quali contiene circa la metà degli elementi di S. (S 1 contiene i primi n/2 elementi e S 2 contiene i rimanenti n/2 elementi). Risolvi ricorsivamente: ordina gli elementi in S 1 e S 2 usando MergeSort (ricorsione). Combina: metti insieme gli elementi di S 1 e S 2 ottenendo un unico vettore S ordinato (merge)

41 Mergesort: esempio 24 36 10 87 83 21 39 58 24 36 10 87 24 36 2436 10 87 10 24 36 8783 21 39 5821 39 58 83 10 21 24 36 39 58 83 87 10 8724 36 2436

42 Merge Sort: Algoritmo Merge-sort(A,p,r) if p < r then q   p+r)/2 Merge-sort(A,p,q) Merge-sort(A,q+1,r) Merge(A,p,q,r) Rimuovi il più piccolo dei due elementi affioranti in A[p..q] e A[q+1..r] e inseriscilo nel vettore in costruzione. Continua fino a che i due vettori sono svuotati. Copia il risultato in A[p..r].

43 Merge 122736384754 123253268961 1 12 23 … e così via

44 Equazioni ricorsive: un esempio semplice T(n) = 1 se n = 1 T(n/2) + 1 se n > 1 Come si risolve ???

45 Equazioni ricorsive Risultati e tempi di esecuzione di algoritmi ricorsivi possono essere descritti usando equazioni ricorsive Un equazione ricorsiva esprime il valore di f(n) come combinazione di f(n 1 ),...,f(n k ) dove n i < n. Esempio: Merge Sort

46 Metodo iteratvo T(n) = T(n/2) + 1 T(n/4) + 1 + 1 T(n/8)+1 + 1 + 1 T(n/n)+1......................... + 1..................... 1 + 1......................... + 1 k Ci fermiamo quando 2 k =n k chiamate ricorsive

47 Dobbiamo valutare k. sappiamo che 2 k = n, quindi log 2 ( 2 k ) = log 2 (n), ovvero k = log 2 (n)

48 Induzione Dobbiamo dimostrare che una affermazione è vera per ogni n≥0 Teorema. Se 1. affermazione(0) è vera. 2. affermazione(n-1) vera implica affermazione(n) vera. Allora affermazione(n) vera per ogni n ≥ 0

49 Dimostrazione per induzione: esempio  n i=1 i = n(n+1)/2 affermazione(n):  1 i=1 i = 1(1+1)/2 = 1 OK affermazione(1): affermazione(n-1) “implica” affermazione(n):  n-1 i=1 i = (n-1)(n)/2  n i=1 i = n(n+1)/2 implica

50 Dimostrazione per induzione: esempio  n i=1 i =...ma  n-1 i=1 i + n = (n-1)(n)/2 + n = n(n+1)/2 L’uguaglianza tra questi due termini non è altro che affermazione(n-1) e quindi la assumiamo vera per ipotesi induttiva.

51 Metodo di sostituzione Primo passo: Ci buttiamo a “indovinare” una possibile soluzione: T(n) ≤ clog 2 (n) Secondo passo: la verifichiamo per induzione come segue: Assumiamo che T(n’) ≤ clog 2 (n’) per n’ < n e dimostriamo che T(n) ≤ clog 2 (n) c è una costante (indipendente da n) che determineremo strada facendo…

52 T(n) = T(n/2) + 1 ≤ clog 2 (n/2) + 1 = clog 2 (n) - clog 2 (2) + 1 = clog 2 (n) - c + 1 se c ≥ 1 allora ≤ clog 2 (n) Ipotesi induttiva !!! 

53 Equazioni ricorsive: un esempio più complicato T(n) =  (1) se n = 1 2T(n/2) +  (n) se n > 1 Soluzione T(n) =  (n log(n))

54 Albero di ricorsione Cn + 2T(n/2) C(n/2) + 2T(n/4) C(n/4) + 2T(n/8) = c n = c n + …… + = n(log(n))  (1) …… = c n Il fattore log(n) deriva dal fatto che l’albero ha un altezza log(n)

55 “Master Method” T(n) = aT(n/b) + f(n) a  1, b > 1, f(n) > 0 Poniamo x = log b a f(n) = O(n x-  ) con  >0 allora T(n) =  (n x ) f(n) =  (n x ) allora T(n) =  (n x log(n)) f(n) =  (n x+  ) con  >0 af(n/b) ≤ cf(n) con c n 0

56 … Merge sort T(n) =  (n log(n)) Insertion sortMerge sort Worst case Average case Best case  (n 2 )  (n log(n))  (n)  (n log(n))

57 Perchè ordinare è importante … velocizza molto la ricerca !!! Binary-search(A,x) i=0 j=length(A)-1 while i<j do k=  (i+j)/2  if A[k]=x then return true if A[k]>x then j=k-1 if A[k]<x then i=k+1 if A[i]=x then return true else return false

58 Analisi di Binary search Poniamo D(t)=j-i. D(t) è l’ampiezza del vettore sul quale ancora dobbiamo eseguire la ricerca dopo t confronti. Abbiamo D(0) = n-1 ……… D(t+1) = D(t)/2 Usciamo dal while quando D(t)<2 … ovvero se t ≥ log 2 n. Quindi T(n) =  (log 2 n)

59 Priority Queue (Code a Priorità) Dati: un insieme di elementi, ognuno dei quali ha una chiave (un intero per esempio). Operazioni: inserimento, trova il massimo, estrazione del massimo (massima chiave). Applicazioni delle PQ: Job scheduling Event-driven simulations

60 Implementazione (facile) usando vettori Prima soluzione: vettore ordinato. Ricerca massimo:  (1) operazioni estrazione massimo:  (1) operazioni inserimento:  (n) operazioni Seconda soluzione vettore non ordinato. Ricerca massimo:  (n) operazioni estrazione massimo:  (n) operazioni inserimento:  (1) operazioni Si può fare meglio ???

61 Grafi e Alberi G=(V,E) V={1,2,3,4,5,6,7,8} E={(1,2),(1,3),(1,4),(3,4),(6,7),(7,8)} 1 4 2 3 5 6 7 8 {1,3,4,1} è un ciclo. Un grafo senza cicli è aciclico.

62 Un albero è un grafo aciclico con un numero di nodi uguale al numero di archi più uno ( |V|=|E|+1 ) 1 4 2 3 5 6 7 8 Albero 1 4 2 3 5 6 7 8 Foresta

63 r x qy w Radice r è la radice x è il padre di y y è un figlio di x x e q sono avi di w w e q sono discendenti di x q è fratello di y Foglie h(a) altezza del nodo a: h(x)=1 h(y)=h(q)=2 h(w)=3

64 Heap 128 64 72 87 12 30 1 6 3 A={ 128, 64, 72, 8, 7, 12, 30, 1, 6, 3 } 1 2 3 4 5 6 7 8 9 10 A(6) = 12

65 Heap: definizione formale Un Heap è un albero binario quasi completo. Quasi significa che possono mancare alcune foglie consecutive a partire dall’ultima foglia di destra. Per ogni nodo i: Value(i) ≤ Value(Parent(i)) Nota 1: il massimo si trova nella radice Nota 2: non c’è nessuna relazione tra il valore di un nodo e quello di un suo fratello

66 Memorizzazione di un heap in un vettore 128 64 72 87 12 30 1 6 3

67 Memorizzazione di un heap in un vettore Radice posizione 1 Per ogni nodo in posizione i: left-child(i) posizione 2i right-child(i) posizione 2i+1 parent(i)  i/2 

68 i AB HeapsHeap i2i2i+1 parte del vettore già heapizzato elemento da aggiungere al sotto heap (verde) 4i4i +38i8i +7

69 i AB IDEA: facciamo scendere il nodo i nell’albero fino a trovare la sua posizione. ? AB i

70 Heapify(A,i) l=left(i) r=right(i) if l≤heap-size(A) and A[l]>A[i] then largest=l else largest=i if r≤heap-size(A) and A[r]>A[largest] then largest=r if largest  i then Exchange(A[i],A[largest]) Heapify(A,largest)

71 Heapify: costo computazionale Caso pessimo: il nodo si sposta fino ad arrivare alle foglie. Heapify impiega tempo costante ad ogni livello per sistemare A[i], A[left(i)] e A[right(i)]. Esegue aggiustamenti locali al massimo height(i) volte dove height(i) = O(log(n))

72 Build-heap(A) heap-size(A)=length(A) for i=  length(A)/2  downto 1 do heapify(A,i) Analisi “approssimativa”: ogni chiamata a heapify costa O(log(n)). Chiamiamo heapify O(n) volte, quindi build-heap = O(nlog(n)) Domanda (esercizio): build-heap =  (nlog(n)) ?

73 PQ implementate con Heap Extract-max(A) if heap-size(A)<1 then “error” max=A[1] A[1]=A[heapsize(A)] heapsize(A)=heapsize(A)-1 Heapify(A,1) return max O(log(n))

74 PQ implementate con Heap max = max = ?? max = Heapify( )

75 PQ implementate con Heap Insert(A,x) heap-size(A)=heap-size(A)+1 i=heap-size(A) while i>1 and A[parent(i)]<x do A[i]=A[parent(i)] i=parent(i) A[i]=x O(log(n))

76 Heap Sort: l’idea. Heap Heapify Heap Heapify... avanti così...

77 Heap Sort Heap-Sort(A) build-heap(A) for i=length(A) downto 2 do exchange(A[1],A[i]) heap-size(A)=heap-size(A)-1 heapify(A,1) O(nlog(n)) È un metodo “in place”

78 Quicksort: l’idea Dividi: Dividi il vettore in due parti non vuote. Conquista: ordina le due parti ricorsivamente Combina: fondi le due parti ottenendo un vettore ordinato. A={10,5,41,3,6,9,12,26} mergesort quicksort A metà A 1 ={10,5,41,3} A 2 ={6,9,12,26} Intorno a un Pivot, es 12 A 1 ={10,5,3,6,9,12} A 2 ={41,26} Dividi

79 Quicksort Quicksort(A,p,r) if p<r then q=partition(A,p,r) Quicksort(A,p,q) Quicksort(A,q+1,r) Nota: Mergesort lavora dopo la ricorsione Quicksort lavora prima della ricorsione Partition è cruciale !!!

80 5 3 2 6 4 1 3 7 3 3 2 6 4 1 5 7 3 3 2 1 4 6 5 7 ij i i i i i j j j j j A(p,r) < 5≥ 5 Pivot  (n) in place

81 Analisi di QS nel caso ottimo Caso ottimo: partizioni bilanciate T(n) = 2T(n/2) +  (n) quindi: T(n) =  (nlog(n))

82 Analisi di QS nel caso pessimo Caso pessimo: partizioni sbilanciate T(n) = T(n-1) +  (n) quindi: T(n) =  (n 2 ) ricorsione partition

83 Analisi di QS nel caso...... non buono ! 90% 10% T(n) ???

84 Albero di ricorsione n 1/10 n9/10 n 1/100 n9/100 n 81/100 n n +  (n log(n)) < n 81/1000 n729/1000 n log 10 n log 10/9 n

85 Analisi del caso medio di QS: una intuizione. Caso medio: a volte facciamo una buona partition a volte no... buona partition: cattiva partition

86 Caso medio le buone e le cattive partition si alternano... cattiva 1n-1 1(n-1)/2 dopo una cattiva e una buona partizione in successione siamo più o meno nella situazione in cui la cattiva partizione non è stata fatta ! buona

87 QS: distribuzione degli input Abbiamo assunto implicitamente che tutte le sequenze di numeri da ordinare fossero equiprobabili. Se ciò non fosse vero potremmo avere costi computazionali più alti. Possiamo “rendere gli input equiprobabili” ? mischiamo la sequenza casualmente prima di ordinare Scegliamo il pivot a caso. come procediamo

88 QS “randomizzato” QSR una una versione randomizzata della procedura Partition. Randomized-partition(A,p,r) i=random(p,r) exchange(A[p],A[i]) return partition(A,p,r) Un algoritmo randomizzato non ha un input pessimo, bensì ha una sequenza di scelte pessime di pivot.

89 Insertion sort Merge sort Heap sort Quick sort Caso pessimo n2n2 n log(n) n2n2 Caso medio n2n2 n log(n) Caso ottimo nn log(n) = in place

90 È possibile ordinare in meno di n log(n) ??? ovvero in o(n log(n))

91 Limite inferiore di complessità Insertion-sort Merge-sort Heap-sort Quick-sort “Comparison-sort” algoritmi basati su confronti Questi metodi calcolano una soluzione che dipende esclusivamentedall’esito di confronti fra numeri TEOREMA (Lower Bound per algoritmi Comparison-sort): Qualsiasi algoritmo “comparison-sort” deve effettuare nel caso pessimo  (n log(n)) confronti per ordinare una sequenza di n numeri.

92 lower bound per comparison sort IDEA: con n numeri ho n! possibili ordinamenti. Possiamo scegliere quello giusto tramite una sequenza di confronti. ≤> >> ≤ ≤ Ogni nodo rappresenta un confronto.

93 Esempio: n=3 {a 1,a 2,a 3 } a 1 :a 2 a 2 :a 3 a 1 :a 3 a 1,a 2,a 3 a 1 :a 3 a2,a1,a3a 2 :a 3 ≤> >> ≤ ≤ Ogni nodo bianco rappresenta un confronto. Ogni nodo rosso rappresenta una possibile soluzione. a 1,a 3,a 2 a 3,a 1,a 2 >≤ a 2,a 3,a 1 a 3,a 2,a 1 >≤ albero dei confronti

94 3! = 6 = numero di foglie dell’albero dei confronti. ogni (cammino dalla radice ad una) foglia rappresenta un ordinamento ci sono n! ordinamenti. quanto deve essere alto un albero binario per avere n! foglie ??? un albero binario alto h ha al massimo 2 h foglie dobbiamo avere 2 h ≥ n! Formula di Stirling: n! > (n/e) n e=2.17... h ≥ log[(n/e) n ] = nlog(n) - nlog(e) =  (nlog(n))

95 Il caso pessimo di un qualsiasi algoritmo comparison-sort eseguito su una sequenza di n numeri è dato dall’altezza dell’albero di decisione associato a quell’algoritmo. MA Un albero binario con n! foglie (ordinamenti) ha un altezza  (nlog(n)) QUINDI qualsiasi algoritmo comparison-sort, nel caso pessimo, esegue  (nlog(n)) confronti.

96 Counting sort: come ordinare in tempo lineare (!?!?) Ipotesi di lavoro: I numeri da ordinare appartengono all’intervallo [1,k] Risultato: counting sort ha un costo computazionale O(n + k) Se k=O(n) allora counting sort ha un costo computazionale O(n) e quindi “batte” tutti i comparison sort

97 Counting sort: un esempio A = {3,6,4,1,3,4,1,4} C’ = {2,0,2,3,0,1} C’’ = {2,2,4,7,7,8}. C’[3]=2 perché il numero 3 è contenuto 2 volte in A C’’[4]=7 perché ci sono 7 numeri minori o uguali a 4

98 Algoritmi di ordinamento stabili 4 7 2 3 2 7 A B C D E F 2 2 3 4 7 7 C E D A B F

99 Algoritmi di ordinamento NON stabili 4 7 2 3 2 7 A B C D E F 2 2 3 4 7 7 E C D A B F

100 Algoritmi di ordinamento stabili Un algoritmo di ordinamento è stabile se: Se A[i] = A[j] e i < j allora A[i] compare nell’ordinamento prima di A[j] ESERCIZIO: dimostrare che counting sort è stabile.

101 Counting-sort(A,B,k) 1.for i=1 to k do C[i]=0 2.for j=1 to length(A) do C[A[j]]=C[A[j]]+1 3.for i=2 to k do C[i]=C[i]+C[i-1] 4.for j=length(A) downto 1 do 5.B[C[A[j]]]=A[j] 6.C[A[j]]=C[A[j]]-1 1.costa  (k) 2.costa  (n) 3.costa  (k) 4.costa  (n) Quindi Counting sort =  (n + k)

102 Radix sort 310638237272182926310638237272182926 310272182926237638310272182926237638 310926237638272182310926237638272182 182237272310638926182237272310638926 vettore ordinato

103 Radix sort Radix-sort(A,d) for i=1 to d do usa un “stable sort” per ordinare A sulla cifra iesima Ogni cifra è compresa tra 1 e k. Usiamo counting sort (stabile). Costo computazionale: d  (n+k) =  (nd+dk). Counting sort non lavora in place !

104 Bucket sort Ipotesi: i numeri da ordinare sono uniformemente distribuiti nell’intervallo [0,1), ovvero Ci si aspetta che nell’intervallo [x,x+  ) ci siano tanti numeri quanti in [y,y+  ) per qualunque x,y, 

105 78 17 39 26 62 68 21 12 23 0 1 2 3 4 5 6 7 8 -- 2123 -- 26 12 -- 17 62 -- 68 -- 39 -- 78 --

106 Bucket-sort(A) n=length(A) for i=1 to n do inserisci A[i] nella lista B[  nA[i]  ] for i=0 to n-1 do ordina la lista B[i] usando insertion-sort Concatena le liste B[0],...,B[n-1] NOTA:  nA[i]  restituisce il “bucket” dove inserire A[i]

107 Variabile Aleatoria Discreta: variabile che può assumere un numero finito di valori con una certa distribuzione di probabilità. Esempio 1: X = (Testa, Croce) Pr(X=Testa) = Pr(X=Croce) = 1/2 Esempio 2: Y = (1,2,3,4) Pr(Y=1) = Pr(Y=2) = 1/3, Pr(Y=3) = Pr(Y=3) = 1/6.

108 Media di una VAD E[Y]= 1·1/3 + 2·1/3 + 3·1/6 + 4·1/6 = 13/6 Valori possibili di Y Probabilità

109 Vogliamo calcolare il costo computazionale medio di Bucket Sort. Bucket Sort usa Insertion Sort sulle singole liste. Assumendo che la lunghezza media di ogni lista sia n i, il costo della singola applicazione di Insertion Sort è (n i ) 2 (nel caso pessimo !!!) Dobbiamo quindi valutare E[(n i ) 2 ]. NOTA: E[X 2 ] diversa da E[X] 2

110 n i = variabile aleatoria = numero di elementi nel bucket i Pr(x ---> B i ) = 1/n Distribuzione binomiale Pr(n i = k) = ( ) (1/n) k (1-1/n) n-k E[n i ] = n 1/n = 1 E[n i 2 ] = Var[n i ] + E 2 [n i ] = 2 - 1/n =  (1) E[n i 2 ] = costo computazionale di insertion sort Costo computazionale di bucket sort = O(n) nknk

111 Selezione: esempio 12 3 8 1 7 6 100 91 1 3 6 7 8 12 91 100 minimo massimo quarto elemento nell’ordinamento Input Input ordinato

112 Selezione Ordinamento  (nlog(n)) minimo/massimo  (n) Selezione ???? Selezione: Calcolare l’iesimo elemento nell’ordinamento.

113 Selezione Input: un insieme A di n numeri distinti e un numero i tra 1 e n Output: l’elemento x in A maggiore di esattamente i-1 altri numeri in A Soluzione banale: ordino gli elementi di A e prendo l’iesimo elemento nell’ordinamento.  (nlog(n)) (analisi del caso pessimo)

114 A[1,...,n] 1 n q Supponiamo che A[i] ≤ A[j] per ogni 1 ≤ i ≤ q e ogni q < j ≤ n Domanda: il k-esimo elemento nell’ordinamento sta in L o in R ? LR Risposta: Facile. Se k ≤ q ---> L. Altrimenti R.

115 Selezione in tempo medio lineare Rand-select(A,p,r,i) if p=r then return A[p] q=rand-partition(A,p,r) k=q-p+1 if i≤k then return Rand-select(A,p,q,i) else return Rand-select(A,q+1,r,i-k) caso pessimo  (n 2 ) caso medio  (n) (senza dimostrazione)

116 Selezione in tempo lineare nel caso pessimo IDEA: dobbiamo progettare un buon algoritmo di partition  n (1-  )n Nota: basta che  sia maggiore di zero e indipendente da n !!!

117 Select(i) 1.Dividi i numeri in input in gruppi da 5 elementi ciascuno. 2.Ordina ogni gruppo (qualsiasi metodo va bene). Trova il mediano in ciascun gruppo. 3.Usa Select ricorsivamente per trovare il mediano dei mediani. Lo chiamiamo x. 4.Partiziona il vettore in ingresso usando x ottenendo due vettori A e B di lunghezza k e n-k. 5.Se i≤k allora Select(i) sul vettore A altrimenti Select(i-k) sul vettore B.

118 5 Mediano della terza colonna  n/5  = M Calcoliamo il mediano di M usando Select ricorsivamente !!!

119 Supponiamo di aver riordinato le colonne a seconda del valore del loro mediano. = mediano dei mediani. Maggiori o uguali di Minori o uguali di

120 più o meno 3 n/10 Se partizioniamo intorno a lasciamo almeno (circa) 3n/10 elementi da una parte e almeno (circa) 3n/10 elementi dall’altra !!! OK

121 Select: costo computazionale T(n) =  (1)se n < c  (n) + T(n/5) + T(7n/10 ) se n ≥ c Costo per ordinare le colonne Costo per calcolare il mediano dei mediani Costo per la chiamata ricorsiva di select T(n) ≤ k n, k costante opportuna Dim: Esercizio

122 Strutture Dati Elementari: Pile e Code

123 Pile (Stacks) Dati: un insieme S di elementi. Operazioni: PUSH, POP PUSH: inserisce un elemento in S POP: restituisce l’ultimo elemento inserito e lo rimuove da S Politica: Last-In-First-Out (LIFO)

124 Pila PUSH…

125 Pila POP…

126 Code (Queues) Dati: un insieme S di elementi. Operazioni: ENQUEUE, DEQUEUE ENQUEUE : inserisce un elemento in S DEQUEUE : restituisce l’elemento da più tempo presente (il più vecchio) e lo rimuove da S Politica: First-In-First-Out (FIFO)

127 Coda ENQUEUE…

128 Coda DEQUEUE…

129 Implementazione di Pile con Vettori STACK-EMPTY(S) If top[S] = 0 then return TRUE else return FALSE PUSH(S,x) top[S] = top[S] + 1 S[top[S]] = x POP(S) if STACK-EMPTY(S) then “error” else top[S] = top[S] - 1 return S[top[S] + 1]

130 Implementazione di code con Vettori ENQUEUE(Q,x) Q[tail[Q]] = x if tail[Q] = length[Q] then tail[Q] = 1 else tail[Q] = tail[Q] + 1 DEQUEUE(Q,x) x = Q[head[Q]] if head[Q] = length[Q] then head[Q] = 1 else head[Q] = head[Q] + 1

131 Problemi con i vettori Vettori Semplici, Veloci ma Bisogna specificare la lunghezza staticamente Legge di Murphy Se usi un vettore di lunghezza n = doppio di ciò che ti serve, domani avrai bisogno di un vettore lungo n+1 Esiste una struttura dati più flessibile?

132 Linked Lists Dato 1nextDato 2nextDato 3--- Head[L] Elemento della lista: Dato + puntatore all’ elemento successivo nella lista

133 Doubly Linked Lists Dato 1---- Head[L] next Dato 2prevnext Dato 3prev---- Elemento della lista: Dato + puntatore al predecessore + puntatore al successore

134 Ricerca e Inserimento LIST-SEARCH(L,k) x = head[L] while x nil and key[x] k do x = next[x] return x LIST-INSERT(L,x) next[x] = head[L] if head[L] nil then prev[head[L]] = x head[L] = x prev[x] = nil

135 Cancellazione LIST-DELETE(L,k) x = LIST-SEARCH[L,k] if prev[x] nil then next[prev[x]] = next[x] else head[L] = next[x] if next[x] nil then prev[next[x]] = prev[x]

136 Costi Computazionali InserimentoLIST-INSERT  1) CancellazioneLIST-DELETE  1) RicercaLIST-SEARCH  n)

137 Sentinelle ---prev nil[L] next Lista vuota usando le sentinelle

138 Sentinelle xlast nil[L] first Dato 1prevnext sentinella Dato 2prevnext Dato 3prevnext Dato 4prevnext

139 Cancellazione usando le sentinelle LIST-DELETE(L,k) x = LIST-SEARCH[L,k] if prev[x] nil then next[prev[x]] = next[x] else head[L] = next[x] if next[x] nil then prev[next[x]] = prev[x] LIST-DELETE-SENTINEL(L,k) x = LIST-SEARCH[L,k] next[prev[x]] = next[x] prev[next[x]] = prev[x]

140 Esercizio: usare le sentinelle per SEARCH e INSERT

141 Alberi binari rappresentati usando liste Padre Figlio sinistro Filgio destro

142 Alberi generali rappresentati usando liste Padre Primo figlio Primo fratello --- Lista dei fratelli

143 Costo computazionale delle operazioni sulle liste Singly linked non ordinata Singly linked ordinata Doubly linked non ordinata Doubly linked ordinata Ricerca Inserimento Cancellazione Successore Predecessore Massimo

144 Tabelle Hash

145 Elemento 1 Elemento 12 Elemento 2 Elemento 41 Elemento i 1 |S| Vettore V Ogni elemento ha una chiave K i tale che 0 < K i < n+1 Insieme S

146 Tabella con indirizzamento diretto i iElemento i-esimo j jElemento j-esimo Posizione nel vettore = valore della chiave

147 Tabella con indirizzamento diretto Search(V,k) return V[k] Insert(V,x) V[key[x]] = x Delete(V,x) V[key[x]] = nil Costo computazionale =  (1)

148 Problemi… Supponiamo che solo una parte S’ dello spazio S delle chiavi sia utilizzata/attiva. Cosa succede quando |S’| << |S| ? Si spreca spazio di memoria !!! Soluzioni?

149 Problemi… S’ S = Spazio sprecato

150 Una soluzione … Possiamo ridurre l’occupazione di spazio da  (|S|) a  (|S’|) Usando LINKED LISTS !!! PROBLEMA (non finiscono mai): Inserimento, Cancellazione e Ricerca costano  (|S’|) invece di  (1).

151 Vero Problema: compromesso tra TEMPO e SPAZIO Usando Hash tables possiamo raggiungere: Tempo di accesso:  (1) Spazio di memoria:  (|S’|) Ma … in media e non nel caso pessimo !

152 IDEA … i iElemento i-esimo h(i) iElemento i-esimo h = funzione hash

153 Funzione hash h restituisce un numero intero da 1 a M. Usiamo una tabella con M posizioni x viene memorizzato in posizione h(key[x])

154 Proprietà per una “buona” h  Deterministica ma deve sembrare “random” in modo da minimizzare le collisioni.  x e y generano una collisione se x ≠ y e h(x) = h(y)  h deve minimizzare il numero di collisioni

155 kiki kjkj kiki kjkj -- h(k i )=h(k j ) Risoluzione di collisioni con “chaining”

156 Chained-hash-insert(T,x) Inserisci x in testa alla lista: T[h(key[x])] Chained-hash-search(T,k) Ricerca l’elemento con chiave k nella lista: T[h(k)] Chained-hash-delete(T,x) cancella x dalla lista: T[h(key[x])]

157 Chaining: analisi Load factor  =n/m, n = numero di elementi memorizzati in T m = dimensione di T Caso pessimo: tutte le n chiavi finiscono nella stessa posizione. Ricerca =  (n) Caso medio: Simple uniform hashing: Pr(h(k)=i) = Pr(h(k)=j)

158 Simple uniform hashing: un esempio m=2 1 2 1 (1/2) 2 (1/8) 3 (1/8) 5 (1/16) 6 (1/8) 4 (1/16) U= in rosso è indicata la probabilità che una certa chiave debba essere inserita nella tabella h NON UNIFORME !!! PERCHE’ ???

159 Simple uniform hashing: un esempio m=2 1 2 1 (1/2) 2 (1/8) 3 (1/8) 5 (1/16) 6 (1/8) 4 (1/16) U= in rosso è indicata la probabilità che una certa chiave debba essere inserita nella tabella h UNIFORME !!! PERCHE’ ???

160 Simple uniform hashing Una funzione hash si dice uniforme quando rende uniforme il riempimento della tabella. Non quando la distribuzione delle chiavi è uniforme !!!

161 Teorema: Ipotesi: collisioni gestite con chaining simple uniform hashing caso medio Tesi: una ricerca ha costo computazionale  (1+  )

162 Dimostrazione: Caso di ricerca senza successo. Load factor  è la lunghezza media di una catena. In una ricerca senza successo il numero di elementi esaminati è uguale alla lunghezza media delle catene. Calcolare h() costa 1.

163 Dimostrazione: Caso di ricerca con successo. Assumiamo di inserire elementi in testa alla catena. Simple uniform hashing numero medio di elementi in una catena dopo i inserimenti = i/m l’elemento j ci si aspetta che venga inserito nella posizione 1 +(j-1)/m all’interno di una catena.

164 Un elemento generico finirà in media nella posizione data dalla formula: 1/n  ( 1+ (i-1)/m ) = 1/n (n + [n(n+1)]/[2m] - n/m) = = 1 +  /2 - 1/(2m) = =  (1+  ) i=1 n

165 Supponiamo che n=O(m). Ovvero che il numero di elementi inseriti nella tabella sia proporzionale alla dimensione della tabella. Abbiamo:  = n/m = O(m)/m = O(1) In questo caso la ricerca impiega tempo costante !!! Cosa succede se gli elementi vengono inseriti all’inizio delle liste ?

166 Riepiloghiamo... Se usiamo doubly linked lists per le catene e se inseriamo i nuovi elementi in testa alle liste abbiamo Ricerca Cancellazione Inserimento O(1) operazioni in media

167 Funzioni hash: progettazione Pr(k) = probabilità della chiave k S j ={ k  U tali che h(k)=j } Vogliamo uniform hashing ovvero  Pr(k) = 1/m (m=dimensione della tabella) kSjkSj

168 Esempio U = { x  R : 0≤x<1 } x preso a caso da U. Definiamo h(x)=  xm  Dimostrare per esercizio che h() è una buona hash function. (suggerimento: definire S j esplicitamente)

169 Se Pr() è sconosciuta Usiamo euristiche IDEA:  h deve dipendere da tutti i bit di k  deve essere indipendente da eventuali pattern che possono essere presenti nelle chiavi

170 Supponiamo per semplicità che le chiavi siano numeri naturali. Metodo della divisione h(k) = k mod m Esempio: m=12, k=100, h(100) = 100 mod 12 = 4 Per controllare se uno ha scelto un buon m è consigliabile usare un “benchmark” reale.

171 Metodo della moltiplicazione h(k) =  m(kA mod m)  Esempio: A = (5 1/2 -1)/2 = 0.618..., k = 123456, m = 10000 h(123456) = conti conti conti = 41

172 Risoluzione collisioni: open addressing 1 2 3 4 5 h( ,0) =1  h( ,0) =1h( ,1) =2  h(,0) =2h(,1) =4 h( ,0) =2h( ,1) =4h( ,2) =5 

173 Open addressing Nessun puntatore: spazio risparmiato!  ≤ 1 sempre. Nessuna lista per gestire le collisioni Hash function più complessa. deve essere una permutazione di

174 h(k,i) = posizione della tabella in cui inserire la chiave k quando tutte le posizioni h(k,0),..., h(k,i-1) sono già occupate.

175 Open addressing: uniform hashing Se gestiamo le collisioni con il metodo open addressing, la funzione hash restituisce una permutazione degli indici. Invece di simple uniform hashing parliamo di uniform hashing. Uniform hashing: tutte le permutazioni devono apparire con la stessa probabilità

176 Open addressing: inserimento Hash-insert(T,k) i=0 repeat j=h(k,i) if T[j]=nil then T[j]=k return j else i=i+1 until i=m error “hash table overflow”

177 Open addressing: ricerca Hash-search(T,k) i=0 repeat j=h(k,i) if T[j]=k then return j else i=i+1 until (T[j]=nil) or (i=m) return nil

178 Open addressing: cancellazione Hash-delete(T,k) i=Hash-search(T,k) if i  nil then T[i]=nil NON FUNZIONA

179 3 7 8 0 1 2 3 h(6,0)=0 h(6,1)=1 h(6,2)=2 h(6,3)=3 Inseriamo 6 3 7 8 6 0 1 2 3 Cancelliamo 8 3 7 6 0 1 2 3 ricerchiamo 6 Risposta: 6 non c’è

180 Esercizio: Modificare Hash-search e Hash-delete per risolvere il problema illustrato nel lucido precedente. 3 7 D 6 0 1 2 3 Suggerimento: usare un carattere con il quale contrassegnare gli elementi cancellati.

181 Open addressing: linear probing Sia h’ una funzione hash “ordinaria”. Definiamo h(k,i)=(h’(k) + i) mod m Esempio di linear probing: m=5, k=3, h’(3)=4 h(k,0) = 4 h(k,1) = 5 h(k,2) = 0 h(k,3) = 1 h(k,4) = 2 h(k,5) = 3 = probe

182 Linear probing: primary clustering Tempo medio di accesso per una ricerca senza successo: 1.5 Perche’? Tempo medio di accesso per una ricerca senza successo: 2.5 Perche’? Clustering primario

183 Usando linear probing il clustering primario si forma con alta probabilità. Pr = (i+1)/m i slot pieni i+1 slot pieni Pr = 1/m i slot vuoti

184 Quadratic probing h(k,i) = (h’(k) + c 1 i + c 2 i 2 ) mod m con c 2  0 Cosa si può dire sul clustering primario ?

185 Double hashing h(k,i) = (h 1 (k) + ih 2 (k)) mod m Cosa succede se MCD(m,h 2 (k)) = d > 1 ??? Quante permutazioni distinte produce il double hashing ???

186 Open addressing: ricerca Teorema: Data una hash table con open addressing e load factor  = n/m < 1 la lunghezza media di una “probe” in una ricerca senza successo è 1/(1-  ). (Ipotesi: uniform hashing)

187 1/(1-  ) = m  = (m-1)/m (valore massimo di  ) 1/(1-  ) = m/(m-1)  = 1/m (valore minimo di  ) 1/(1-  ) = 2  = 1/2

188 Dimostrazione: IDEA: cosa succede quando facciamo una ricerca senza successo ??? Empty X = lunghezza probe = quante volte devo calcolare h(k,i) prima di trovare uno slot vuoto = elemento non trovato Dobbiamo valutare E[X] = media di X

189 Lemma: X variabile aleatoria discreta X= 0 --> p 0 1 --> p 1 i --> p i E[X] =  i=0 ∞ ip i  i=0 ∞ iPr(X=i)  i=0 ∞ i ( Pr(X≥i) - Pr(X≥i+1) )  i=1 ∞ Pr(X ≥i) = ==.......... ESERCIZIO !!!

190 E[X] =  i=1 ∞ (n/m) i  i=1 ∞ ii  ∞ Pr(X≥i) ≤ ≤ Costo per la ricerca: 1 + E[X] = 1 +  i=1 ∞  i = 1 +  +  2 +  3 +.......... 1 / (1-  )

191 Open addressing: inserimento Teorema: Data una hash table con open addressing e load factor  = n/m < 1, la lunghezza media di una “probe” è 1/(1-  ). (Ipotesi: uniform hashing)

192 Dimostrazione: Nota:  deve essere < 1. Per inserire un elemento abbiamo bisogno di determinare la posizione nella tabella dove inserirlo. Ricerca: costo 1/(1-  ). Per inserire nella tabella nella posizione appena determinata:  (1).

193 Alberi Binari di Ricerca

194 Alberi di ricerca binari 8 185 615 179 16 ≤ >

195 Alberi di ricerca binari 8 185 615 179 16 ≤ >

196 BST: definizione formale Sia x un nodo dell’albero: Se y è un nodo nel sottoalbero sinistro di x allora key[y]≤key[x] Se y è un nodo nel sottoalbero destro di x allora key[y]>key[x] Nota che un BST può essere molto sbilanciato !!! bilanciato sbilanciato

197 8 185 715 179 16 6 Inorder-tree-walk(x) if x ≠ nil then Inorder-tree-walk(left[x]) print key[x] Inorder-tree-walk(right[x]) 5 6 7 8 9 15 17 16 18 ORDINAMENTO

198 8 185 715 179 16 6 Ricerca ricerchiamo il 16  (h) confronti h=altezza albero

199 Ricerca Tree-search(x,k) if x=nil or k=key[x] then return x if k<key[x] then return Tree-search(left[x],k) else return Tree-search(right[x],k) Esercizio: dimostrare che il costo computazionale di Tree-search è  (h)

200 15 186 73 42 9 13 Successore 2017 x ha il figlio destro. successore(x)=minimo nel sottoalbero di destra Dimostrazione: Esercizio.

201 15 186 73 42 9 13 Successore 2017 x non ha il figlio destro. successore(x) = il più basso avo di x il cui figlio sinistro è avo di x Dimostrazione: Esercizio.

202 Operazioni su BST Ricerca Minimo Massimo Predecessore Successore  (h) confronti

203 15 186 73 42 9 13 Inserimento 2017 5 99 5

204 Cancellazione 3 casi: x non ha figli: elimina x x ha un figlio: x ha 2 figli: Lemma: il successore di x sta nel sotto albero destro e ha al massimo 1 figlio. Dimostrazione: esercizio.

205 15 186 73 42 9 13 2017 eliminiamo successore di 15

206 17 186 73 42 9 13 20 17 ha preso il posto di 15

207 Cancellazione di un nodo x con 2 figli: 1. sia y = successore di x. y ha un solo figlio (al massimo) 2. sostituisci x con y. 3. rimuovi y.

208 Problema Tutte le operazioni su BST hanno un costo lineare nell’altezza dell’albero. Purtroppo, quando l’albero è sbilanciato, h = n-1 Le operazioni hanno un costo lineare invece che logaritmico come speravamo !!! Soluzione ???

209 Soluzione Introduciamo alcune proprietà addizionali sui BST per mantenerli bilanciati. Paghiamo in termini di una maggiore complessità delle operazioni dinamiche sull’albero. Tali operazioni devono infatti preservare le proprietà introdotte per mantenere il bilanciamento.

210 Red-Black Trees

211 Red Black Trees = BST + alcune proprietà aggiuntive 26 1741 14213047 2838 3539 2319 20 1610 12157 3

212 Proprietà A ogni nodo è rosso o nero 26 17 41 1421 30 47 2838 3539 2319 20 16 10 12 15 7 3

213 Proprietà B ogni foglia è nera (ne aggiungiamo un livello fittiziamente) 26 17 41 1421 30 47 2838 3539 2319 20 16 10 12 15 7 3

214 Proprietà C un nodo rosso ha figli neri 26 17 41 1421 30 47 2838 3539 2319 20 16 10 12 15 7 3

215 Proprietà D tutti i cammini da un nodo x alle foglie ha lo stesso numero di nodi neri 26 17 41 1421 30 47 2838 3539 2319 20 16 10 12 15 7 3 4 nodi neri

216 Idea di base Proprietà D: se un RB tree non ha nodi rossi è completo. Possiamo immaginarci un RB tree come un albero nero completo a cui abbiamo aggiunto “non troppi” nodi rossi (Proprietà C). Ciò rende l’albero “quasi bilanciato”

217 Black-height di un RB tree bh(x) = numero di nodi neri (senza contare x) nel cammino da x a una foglia) bh(root) = black-height dell’albero

218 bh(x) 26 17 41 1421 30 47 2838 3539 2319 20 16 10 12 15 7 3 3 2 2 3

219 Teorema: Un RBT con n nodi interni è alto al massimo 2log 2 (n+1) Lemma: Il numero di nodi interni di un sotto albero radicato in x è maggiore o uguale a 2 bh(x) -1

220 Dimostrazione del lemma. Per induzione sull’altezza di x. CASO BASE: h(x)=0. Se h(x)=0 allora bh(x)=0 inoltre x è una foglia quindi il numero di nodi interni è 0. INDUZIONE: h(x)>0. x ha 2 figli: L e R. Abbiamo 2 casi: L è rosso: bh(L) = bh(x) L è nero: bh(L) = bh(x) - 1 R viene trattato in modo analogo

221 Visto che h(L) < h(x) e h(R) < h(x) applichiamo l’ipotesi induttiva. Numero di nodi interni dell’albero radicato in L maggioreo o uguale a 2 bh(L) - 1. Stesso discorso per R. Inoltre 2 bh(L) -1  2 bh(x)-1 - 1 e 2 bh(R) - 1  2 bh(x)-1 - 1 Quindi: numero di nodi interni dell’albero radicato in x maggiore o uguale a 2 bh(x)-1 - 1 + 2 bh(x)-1 - 1 + 1 che è uguale a 2 bh(x) - 1.

222 Dimostrazione del teorema. Sia h l’altezza dell’albero. Qualsiasi cammino dalla radice --> foglia contiene almeno metà nodi neri. Il cammino radice --> foglia che determina l’altezza dell’albero contiene almeno h/2 nodi neri. bh(T) = bh(root)  h/2 Lemma --> n  2 bh(root) - 1 quindi: n  2 bh(root) - 1  2 h/2 - 1. concludiamo: log 2 (n+1)  log 2 (2 h/2 ) = h/2.

223 Rotazioni Operazioni di ristrutturazione locale dell’albero che mantengono soddisfatte le proprietà A,B,C,D Y X A CB Y X AC B destra sinistra esercizio: scrivere il pseudo codice per le rotazioni

224 Inserimento Idea: inseriamo x ---> T color[x]=red qualche rotazione + ricolorazione 41 30 47 2838 3539 32 nodo inserito ---> ---> !!!!!! il figlio di un nodo rosso deve essere nero

225 11 2 14 17 58 4 15 p

226 11 2 14 1 7 5 8 4 15 p rotazione sinistra --->

227 11 2 14 1 7 5 4 15 p 8 rotazione destra --->

228 7 14 15 8 2 15 4 11 FINE...

229 metodo generale: zio(x) è rosso C A x B D p yz ws rosso up A C x B p yz ws D

230 metodo generale: zio(x) è rosso C B x A D p y zws rosso up B C x A p y zws D

231 metodo generale: zio(x) è nero C A x B B p yz nero left(A) x A p y z D D C

232 ... e poi... B right(C) x A p y z D C C x A y zD B

233 Ci sono un certo numero di casi analoghi riconducibili a quelli esaminati: esercizio.

234 Cancellazione di un nodo da un RB-tree Come nel caso dei BST, possiamo sempre assumere di eliminare un nodo che ha al massimo un figlio. Infatti se dobbiamo cancellare un nodo x con due figli, spostiamo la chiave del successore y di x in x e poi rimuoviamo y dall’albero. Il successore di un nodo con due figli ha sempre al massimo un figlio.

235 Cancellazione di un nodo con ≤ 1 figlio Sia x il nodo da cancellare. Sia y il figlio di x e sia z il padre di x. 1. rimuoviamo x collegando z con y. 2. se x era rosso allora y e z sono neri e non dobbiamo fare altro. 3. se x era nero e y è rosso allora coloriamo y di nero. Se x era nero e y è nero allora ricoloriamo y con un colore nero “doppio”. Dopodichè svolgiamo alcune altre operazione descritte nel seguito.

236 Esempio z y x z y z y x z y ricolora doppio nero che va poi ridistribuito su un nodo rosso annerendolo. z y x

237 Cancellazione di un nodo con ≤ 1 figlio Idea: cercare di far salire il doppio nero nell’albero fino a trovare un nodo rosso sul quale scaricare una parte del nero del nodo “doppio nero” e riottenere una colorazione legale. Per far ciò operiamo operazioni di ristrutturazioni locali dell’albero e ricolorazioni che possono propagare il doppio nero due livelli più in alto

238 Caso 1: fratello nero con almeno un figlio rosso x y s t x y s t x y s t rotazione sinistra rotazione derstra e poi sinistra ab x ys t ab

239 Caso 2: fratello nero con figli neri x y s x y s x y s x y s

240 Caso 3: fratello rosso x y s ab x y s a b rotazione sinistra

241 Programmazione Dinamica

242 Divide et impera: si suddivide il problema in sotto problemi indipendenti, si calcola ricorsivamente una soluzione per i sottoproblemi e poi si fondono le soluzioni così trovate per calcolare la soluzione globale per il problema originale. Programmazione dinamica: simile all’approccio divide et impera, ma in questo caso si tiene traccia (in una tabella) delle soluzioni dei sottoproblemi perchè può capitare di dover risolvere il medesimo sottoproblema per più di una volta.

243 Prodotto di una sequenza di matrici Dobbiamo calcolare A=A 1 A 2 A m dove A i sono matrici di opportune dimensioni (righe x colonne). In che ordine conviene effettuare le moltiplicazioni ?

244 Moltiplicazioni di matrici Matrix-multiply(A,B) if columns(A)  rows(B) then “error” else for i=1 to rows(A) do for j=1 to columns(B) do C[i,j]=0 for k=1 to columns(A) do C[i,j]=C[i,j]+A[i,k]B[k,j]  (rows(A) columns(B) columns(C)) moltiplicazioni.

245 Esempio: A=MNQ M = 10 righe, 100 colonne N = 100 righe, 5 colonne Q = 5 righe, 50 colonne Primo metodo A=((MN)Q). Numero di moltiplicazioni: 101005 per calcolare A’=MN 101550 per calcolare A=A’Q Totale 7500 Secondo metodo A=(M(NQ)). Numero di moltiplicazioni: 100550 per calcolare A’=NQ 1010050 per calcolare A=MA’ Totale 75000

246 Numero di possibili parentesizzazioni Il numero di possibili parentesizzazioni P(n) può essere ottenuto come segue: P(n) = 1 n=1 n>1  P(k)P(n-k) k=1 n-1 numero di parentesizzazioni delle prime k matrici numero di parentesizzazioni delle altre n-k matrici

247 P(n) risulta essere  (4 n /n 3/2 ) Quindi esponenziale in n. L’algoritmo di enumerazione non può essere usato !!! Osservazione chiave: Supponiamo che la soluzione ottima sia ottenuta 1. moltiplicando le prime k matrici tra loro in qualche modo 2. moltiplicando le altre n-k matrici tra loro in qualche modo 3. moltiplicando le due matrici ottenute ai primi due passi Le parentesizzazioni dei passi 1 e 2 sono ottime

248 Soluzione ricorsiva m[i,j] = costo minimo per moltiplicare le matrici A i,...,A j m[i,j] = 0 i=j i<j min{ m[i,k] + m[k+1,j] + p i-1 p k p j } i≤k<j

249 Costo della soluzione ricorsiva Esercizio: Determinare il costo computazionale dell’algoritmo ricorsivo progettato a partire dall’equazione ricorsiva del lucido precedente. Esponenziale o polinomiale ???

250 Numero di sottoproblemi Quanti sottoproblemi abbiamo? uno per ogni coppia di indici i,j nel range 1,...n ovvero  (n 2 ) (pochi !). Quindi l’algoritmo ricorsivo deve risolvere più volte lo stesso sottoproblema altrimenti non si spiega il costo esponenziale !

251 7875 15750 00 2625 0 15125 11875 7125 9375 10500 5375 4375 0 750 2500 1000 3500 0 5000 0 m= A1A1 A2A2 A3A3 A4A4 A5A5 A6A6 A 1 30x35 A 2 35x15 A 3 15x5 A 4 5x10 A 5 10x20 A 6 20x25

252 3 1 2 1 3 3 3 33 3 4 3 3 5 5 s = s[i,j] contiene l’indice ottmo per spezzare la moltiplicazione A i A j in due: A i A s[i,j] e A s[i,j] +1 A j

253 Matrix-chain-order(p) n=length(p)-1 for i=1 to n do m[i,i]=0 for l=2 to n do for i=1 to n-l+1 do j=i+l-1 m[i,j]=∞ for k=i to j-1 do q=m[i,k]+m[k+1,j]+p i-1 p k p j if q<m[i,j] then m[i,j]=q s[i,j]=k return m,s Pseudo codice

254 Matrix-chain-multiply(A,s,i,j) if j>i then X = Matrix-chain-multiply(A,s,i,s[i,j]) Y = Matrix-chain-multiply(A,s,s[i,j]+1,j) return Matrix-multiply(X,Y) else return A i Parentesizzazione ottima dell’esempio = ((A 1 (A 2 A 3 ))((A 4 A 5 )A 6 )) Costo computazionale: Matrix-chain-order tempo  (n 3 )spazio  (n 2 ) Matrix-chain-multiply tempo esercizio spazio esercizio Pseudo codice

255 Passi fondamentali della programmazione dinamica 1.Caratterizzazione della struttura di una soluzione ottima 2.Definizione ricorsiva del valore di una soluzione ottima 3.Calcolo del valore di una soluzione ottima con strategia bottom-up 4.Costruzione di una soluzione ottima a partire dalle informazioni già calcolate.

256 nell’esempio della moltiplicazione di matrici..... 1.Una parentesizzazione ottima associata a una lista A 1,A 2,...,A m di matrici da moltiplicare può essere sempre suddivisa in due parentesizzazioni ottime associate alle liste A 1,...,A k e A k+1,...,A m per un opportuno valore di k. 2.Una parentesizzazione ottima costa quanto la somma dei costi delle due sotto parentesizzazioni ottime più il costo dovuto alla moltiplicazione delle matrici associate alle due sotto parentesizzazioni 3.vedi come procedere con la matrice m nei lucidi precedenti 4.vedi come procedere con la matrice s nei lucidi precedenti

257 Caratteristiche del problema per applicare la programmazione dinamica Sottostruttura ottima. Una soluzione ottima per il problema contiene al suo interno le soluzioni ottime dei sottoproblemi Sottoproblemi comuni. Un problema di ottimizzazione ha sottoproblemi comuni quando un algoritmo ricorsivo richiede di risolvere più di una volta lo stesso sottoproblema

258 Versione ricorsiva con memorizzazione dei risultati parziali in una tabella Mem-matrix-chain(p) n=length(p)-1 for i=1 to n do for j=1 to n do m[i,j]=∞ return Lookup-chain(p,1,n) Lookup-chain(p,i,j) if m[i,j]<∞ then return m[i,j] if i=j then m[i,j]=0 else for k=1 to j-1 do q=Lookup-chain(p,i,k)+ Lookupchain(p,k+1,j) + p i-1 p k p j if q<m[i,j] then m[i,j]=q return m[i,j]

259 Esercizi Determinare il costo computazionale di: Mem-matrix-chain ??? Lookup-chain ???

260 Sottosequenza comune più lunga Problema di ottimizzazione. Possiamo applicare la programmazione dinamica ?? X = ABCDBDAB Y = BDCABA Z = BCBA è LCS(X,Y)

261 Sotto struttura ottima Siano X= e Y= Sia Z= una qualunque LCS di X e Y. 1.Se x m =y n, e z k =x m =y n allora Z k-1 è LCS di X m-1 e Y n-1 2.Se x m  y n, e z k  x m allora Z k-1 è LCS di X m-1 e Y 3.Se x m  y n, e z k  y n allora Z k-1 è LCS di X e Y n-1

262 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 0 0 1 1 1 2 2 2 0 0 1 2 2 2 2 2 0 1 1 2 2 2 3 3 0 1 2 2 3 3 3 4 0 1 2 2 3 3 4 4 B D C A B A A B C B D A B 0 1 2 3 4 5 6 1 2 3 4 5 6 7 0

263 LCS-length(X,Y) m=length(X) n=length(Y) for i=1 to m do c[i,0]=0 for j=1 to n do c[0,j]=0 for i=1 to m do for j=1 to n do if x i =y j then c[i,j]=c[i-1,j-1] + 1 b[i,j]=  else if c[i-1,j]≥c[i,j-1] then c[i,j]=c[i,j-1] b[i,j]=  else c[i,j]=c[i,j-1] b[i,j]=  return b,c

264 Costruzione di una LCS Print-LCS(b,X,i,j) if i=0 or j=0 then return if b[i,j] =  then Print-LCS(b,X,i-1,j-1) print x i else if b[i,j] =  then Print-LCS(b,X,i-1,j) else Print-LCS(b,X,i,j-1)

265 Algoritmi Greedy

266 Selezione di attività S={1,...,n} insieme di attività. Ogni attività ha un tempo di inizio s i e un tempo di fine f i. Problema: Selezionare un sottoinsieme S’ di attività in modo tale che: 1.se i e j appartengono a S’ allora: s i ≥f j oppure s j ≥f i. 2.La cardinalita di S’ è massimizzato.

267 1 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 2 3 4 5 6 7 8 9 10 11 Attività ordinate in base a f i

268 1 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 2 3 4 5 6 7 8 9 10 11 1 4 8

269 Pseudocodice Greedy-activity-selector(s,f) n=length(s) A={1} j=1 for i=2 to n do if s i ≥f j then A=A  {i} j=i return A  (n)

270 Dimostrazione di correttezza Assumiamo che le attività siano ordinate per tempi di fine in modo crescente. Dimostriamo che esiste una soluzione ottima che contiene l’attività 1 che è quella che termina per prima (con il tempo di fine più piccolo). Supponiamo per assurdo che esista una soluzione S’’migliore di S’ (S’ contiene l’attività 1 ed è stata costruita con l’algoritmo greedy). Assumiamo che sia S’ che S’’ siano ordinate per tempi di fine crescenti. S’={1,.....} e S’’={k,......} dove 1  k. Sia T=S’’-{k}  {1}. Non è difficile dimostrare (esercizio) che T è una soluzione ottima e che ovviamente contiene l’attività 1. Adesso eliminiamo da S tutte le attività che sono incompatibili con l’attività 1 e ripetiamo la dimostrazione da capo.

271 A volte gli algoritmi greedy non trovano la soluzione ottima... Problema del commesso viaggiatore. Dato un insieme di n città occorre trovare la strada più breve per visitarle tutte una ed una sola volta e tornare alla città di partenza. Nessun algoritmo greedy può funzionare...

272 1 4 3 2 Algoritmo greedy: parto dalla città 1 e poi procedo visitando la città più vicina non ancora visitata. Quando tutte le città sono state visitate torno alla città 1. 1 4 3 2 Soluzione ottima Soluzione con algoritmo greedy

273 Non sempre una soluzione di ottimo “globale” si ottiene facendo una serie di scelte “localmente” ottime. Per il problema del commesso viaggiatore nessuna politica decisionale greedy fornisce una soluzione finale ottima.

274 Proprietà della scelta greedy. Ad ogni passo l’algoritmo compie una scelta in base ad una certa politica ed alle scelte compiute fino a quel momento. Cosi facendo ci si riduce ad un sottoproblema di dimensioni più piccole. Ad ogni passo si calcola un pezzo della soluzione. Sottostruttura ottima. Come nel caso della programmazione dinamica, anche per applicare un algoritmo greedy occorre che la soluzione ottima contenga le soluzioni ottime dei sottoproblemi.

275 Programmazione dinamica Vs Algoritmi greedy Non sempre è possibile risolvere un problema con programmazione dinamica o con algoritmi greedy (commesso viaggiatore). Non sempre i problemi che soddisfano la proprietà della sottostruttura ottima possono essere risolti con entrambi i metodi. Ci sono problemi che non possono essere risolti con algoritmi greedy ma possono essere risolti con programmazione dinamica Se un problema può essere risolto con algoritmi greedy è inutile scomodare la programmazione dinamica.

276 Problema dello zaino: 2 versioni Knapsack 0-1: Un ladro durante una rapina si trova davanti a n oggetti. Ogni oggetto ha un valore v i e un peso w i (numeri interi). Il ladro ha uno zaino che può contenere fino a W (numero intero) chilogrammi di refurtiva. Il ladro deve scegliere quali oggetti rubare per massimizzare il valore complessivo degli oggetti rubati. Knapsack: In questo caso il ladro può anche prendere una parte frazionaria degli oggetti. Non è costretto a “prendere o lasciare” un oggetto, può decidere di prenderne un pezzo grande a suo piacimento. Nota: Knapsack è una generalizzazione di Knapsack 0-1

277 Proprietà della sottostruttura ottima Entrambe le versioni del knapsack soddisfano tale proprietà. Supponiamo infatti che il ladro possa rubare refurtiva avente peso W’ non maggiore di W di valore massimo V. Se togliamo dallo zaino l’oggetto j otteniamo la soluzione ottima del sottoproblema in cui lo zaino può contenere al massimo W-w j chilogrammi ottenuti mettendo insieme oggetti da un insieme di n-1 (abbiamo eliminato l’oggetto j).

278 Knapsack: soluzione greedy Idea: il ladro prende la quantità più grande possibile dell’oggetto i tale per cui v i /w i (valore per unità di peso) è massimo. Dopodiche’, se nello zaino c’è ancora posto, ripete l’operazione. Esercizio. Dimostrare che l’algoritmo è greedy ed è corretto.

279 Knapsack: soluzione greedy 10 20 30 € 60 € 100 € 120 6 € al chilo 5 € al chilo 4 € al chilo Zaino da 50 chili Soluzione ottima di knapsack con algoritmo greedy € 240 102020 = 2/3 di 30

280 Knapsack 0-1: nessuna soluzione greedy 10 20 30 € 60 € 100 € 120 6 € al chilo 5 € al chilo 4 € al chilo Soluzione ottima di knapsack 0-1 Zaino da 50 chili 3020 € 220 Soluzione di knapsack 0-1 scegliendo per primo l’oggetto da 6 € al chilo (valore per unità di peso massimo) € 180 1030

281 Knapsack 0-1: Soluzione con programmazione dinamica Prima di decidere se inserire nello zaino l’oggetto i bisogna controllare il valore della sottosoluzione ottima di due altri sottoproblemi su n-1 oggetti: il sottoproblema nel quale l’oggetto i è inserito nello zaino e lo zaino ha capienza W-w i il sottoproblema nel quale l’oggetto i non è inserito nello zaino e lo zaino ha ancora peso W Esercizio: risolvere knapsack 0-1 con la programmazione dinamica


Scaricare ppt "Orario: Martedì 1013 Giovedì 1113 1416 Lab.Martedì 1416 RicevimentoGio.16 aula a."

Presentazioni simili


Annunci Google