Salta al contenuto
Note per Studenti Formulario · Dati e Algoritmi

FormularioDati e Algoritmi: definizioni, teoremi e formule delle note, in ordine di capitolo

In questa pagina 11

I. Nozioni fondamentali

Problemi computazionali e algoritmi

Definizione (problema computazionale). Un problema computazionaleDice quali risposte sono corrette per ogni possibile input, senza dire come trovarle. Esempio: per la somma, all'input (3, 4) corrisponde la risposta 7. Il modo di calcolare la risposta è l'algoritmo. Π\Pi è un insieme di coppie (i,s)(i, s) con i∈Ii \in I (istanza, dominio delle istanze) e s∈Ss \in S (soluzione, dominio delle soluzioni). Si richiede che ogni istanza abbia almeno una soluzione, cioè che per ogni i∈Ii \in I esista almeno un s∈Ss \in S con (i,s)∈Π(i, s) \in \Pi. Le coppie stanno in I×SI \times S, quindi Π⊆I×S\Pi \subseteq I \times S. Il simbolo I×SI \times S è il prodotto cartesianoInsieme di tutte le coppie ordinate (i, s) con i in I e s in S. dei due insiemi (vedi Operazioni tra insiemiUnione, intersezione, differenza, complementare e prodotto cartesiano, con le leggi di De Morgan e il legame con la logica.Operazioni tra insiemi →): Π\Pi ne è un sottoinsieme, cioè una relazione tra istanze e soluzioni. Se ogni istanza avesse una sola soluzione, Π\Pi sarebbe il grafico di una funzioneUna funzione associa a ogni elemento del dominio un solo valore: dominio naturale, grafico, immagine, antimmagine e metodo grafico.Funzioni - definizione, dominio e grafico → da II a SS; qui si ammette il caso più generale, con più soluzioni possibili per la stessa istanza.

Esempio. Per la somma di interi (gli insiemi numerici sono in Insiemi numerici (N, Z, Q, R)Naturali, interi, razionali e reali, perché la radice di 2 obbliga a passare da Q a R, e gli intervalli.Insiemi numerici (N, Z, Q, R) →) l'istanza (3,4)(3,4) ha come soluzione 77, e (1,9)(1,9) e (4,6)(4,6) hanno la stessa soluzione 1010.

Definizione (macchina RAM). Il modello usato è la macchina RAMRandom Access Machine: input, output, dati intermedi e programma stanno in memoria, e ogni accesso a una cella costa un passo. Non c'entra la memoria RAM del PC.: i passi elementari sono assegnamento, operazioni logiche e aritmetiche, indicizzazione di array, restituzione di un valore da un metodo; ciascuno costa Θ(1)\Theta(1) (la notazione è in Notazione asintoticaDefinizioni di O, Omega, Theta e o piccolo con le costanti c ed n0; esempi con costanti esplicite; proprietà (polinomi, esponenziali, logaritmi, somme, implicazioni tra notazioni); sommatorie notevoli; terminologia (logaritmica, lineare, polinomiale, esponenziale).Notazione asintotica →: significa "un numero di operazioni limitato da una costante che non dipende dalla taglia dell'input"). Il modello rispecchia un vero elaboratore, in cui processore e memoriaModello di von Neumann: CPU, memoria centrale, bus e periferiche; ciclo fetch-decode-execute e gerarchia di memoria.Struttura di un elaboratore → leggono o scrivono una cella con una sola istruzione e la memoria è indirizzabile per posizione; i casi in cui questa semplificazione non regge (accessi lenti fuori dalla cache) sono in Gerarchia di memoria e principio di localitàCaratteristiche delle memorie (posizione, capacità, unità di trasferimento, metodo di accesso, prestazioni, volatilità); compromesso costo-capacità-velocità e gerarchia a livelli; località temporale e spaziale con esempi; hit, miss e tempo medio di accesso con calcolo svolto.Gerarchia di memoria e principio di località →.

Esempio. A[i] <- A[i] + 1 è una indicizzazione, una addizione e un assegnamento: tre passi elementari, in tutto Θ(1)\Theta(1).

Definizione (taglia di un'istanza). La taglia è una funzione che associa a ogni istanza uno o più numeri che ne misurano la grandezza: la lunghezza di un array, il numero di nodi di un albero, il numero di vertici e archi di un grafo (nn ed mm). La complessità si esprime in funzione della taglia (vedi Complessità in tempo e caso pessimoPerché lo studio sperimentale non basta; complessità al caso pessimo come massimo sul numero di operazioni tra le istanze di una data taglia; stima con limiti superiore e inferiore senza trovare l'istanza peggiore; esempi arrayMax, prefixAverages e InsertionSort; efficienza asintotica e limiti dell'analisi.Complessità in tempo e caso pessimo →).

Esempio. La taglia di un array di nn interi è nn; quella di un grafo è la coppia (n,m)(n, m) di vertici e archi.

Proprietà (ricerca binaria: invariante e complessità)

Esempio. Su A=[2,5,8,12,16,23,38]A=[2,5,8,12,16,23,38] con x=23x=23 la ricerca binaria restituisce l'indice 55 dopo 22 iterazioni; con n=7n=7 ne servono al più ⌊log⁡27⌋+1=3\lfloor\log_27\rfloor+1=3.

Grafico interattivo: Passi al caso pessimo: ricerca lineare, n, contro ricerca binaria, floor(log2 n)+1 iterazioni; a n = 100 sono 100 contro 7

Complessità in tempo e caso pessimo

Definizione (complessità in tempo al caso pessimo). Sia tA,it_{A,i} il numero di operazioni eseguite da AA sull'istanza ii. La complessità in tempo al caso pessimo è

tA(n)=max⁡{ tA,i:i∈I, i ha taglia n }.t_A(n) = \max\{\, t_{A,i} : i \in I,\ i \text{ ha taglia } n \,\}.

Esempio. InsertionSort su n=4n=4 elementi fa 33 confronti sull'array ordinato [1,2,3,4][1,2,3,4] e 66 sull'array invertito [4,3,2,1][4,3,2,1]: il caso pessimo è il massimo, 66.

Proprietà (stima con limiti superiore e inferiore). Trovare l'istanza peggiore e contare i passi esatti è difficile e inutile. Si cercano due funzioni semplici:

  • limite superiore tA(n)∈O(g(n))t_A(n) \in O(g(n)): si argomenta che per ogni nn abbastanza grande, ogni istanza di taglia nn costa al più c g(n)c\,g(n) operazioni;
  • limite inferiore tA(n)∈Ω(g(n))t_A(n) \in \Omega(g(n)): si argomenta che per ogni nn abbastanza grande esiste un'istanza di taglia nn che costa almeno c g(n)c\,g(n) (a volte è comodo mostrare che tutte le istanze costano tanto).

Se coincidono si ha Θ(g(n))\Theta(g(n)) (stima stretta; le definizioni di OO, Ω\Omega e Θ\Theta con le costanti cc ed n0n_0 sono in Notazione asintoticaDefinizioni di O, Omega, Theta e o piccolo con le costanti c ed n0; esempi con costanti esplicite; proprietà (polinomi, esponenziali, logaritmi, somme, implicazioni tra notazioni); sommatorie notevoli; terminologia (logaritmica, lineare, polinomiale, esponenziale).Notazione asintotica →). La funzione gg deve essere la più vicina possibile al vero andamento (tight) e la più semplice (niente costanti, niente termini di ordine inferiore).

Esempio. Se ogni istanza di taglia nn costa al più c1n+c2c_1n+c_2 operazioni e una di esse ne costa almeno c3nc_3n, allora t(n)∈O(n)∩Ω(n)=Θ(n)t(n)\in O(n)\cap\Omega(n)=\Theta(n).

Notazione asintotica

Definizione (notazioni OO, Ω\Omega, Θ\Theta e oo)

Notazione Definizione Si legge
f∈O(g)f \in O(g) ∃ c>0, n0≥1\exists\, c > 0,\ n_0 \ge 1 : f(n)≤c g(n)f(n) \le c\,g(n) per ogni n≥n0n \ge n_0 ff cresce al più come gg
f∈Ω(g)f \in \Omega(g) ∃ c>0, n0≥1\exists\, c > 0,\ n_0 \ge 1 : f(n)≥c g(n)f(n) \ge c\,g(n) per ogni n≥n0n \ge n_0 almeno come gg
f∈Θ(g)f \in \Theta(g) f∈O(g)f \in O(g) e f∈Ω(g)f \in \Omega(g), cioè c′g≤f≤c′′gc' g \le f \le c'' g definitivamente come gg
f∈o(g)f \in o(g) ∀ c>0 ∃ n0\forall\, c > 0\ \exists\, n_0 : f(n)≤c g(n)f(n) \le c\,g(n) per n≥n0n \ge n_0; equivale a lim⁡f/g=0\lim f/g = 0 strettamente meno di gg

Esempio. 3n+4∈O(n)3n+4\in O(n) con c=4c=4 e n0=4n_0=4, perché 3n+4≤4n  ⟺  n≥43n+4\le4n\iff n\ge4 (si porta 3n3n a destra: 4≤n4 \le n).

Grafico interattivo: 3n+4 è O(n): con c = 4 la retta 4n sta sopra 3n+4 da n0 = 4 in poi (in n = 4 valgono entrambe 16)

Proprietà (polinomio contro esponenziale, logaritmo contro potenza)

  1. Polinomio contro esponenziale: nk∈o(an)n^k \in o(a^n) se k>0k > 0, a>1a > 1 (limite nk/an→0n^k/a^n \to 0; è la gerarchia degli infiniti di Limiti notevoli e gerarchia degli infinitiI limiti di base per sciogliere le forme indeterminate delle successioni: potenze, polinomi, il numero e, esponenziali, la gerarchia logaritmi < potenze < esponenziali < fattoriale < n^n e la razionalizzazione.Limiti notevoli e gerarchia degli infiniti →, che si può giustificare con il criterio del rapporto: (n+1)k/an+1nk/an=(1+1/n)ka→1a<1\dfrac{(n+1)^k/a^{n+1}}{n^k/a^n} = \dfrac{(1+1/n)^k}{a} \to \dfrac1a < 1, quindi il rapporto tende a 00, vedi Criteri del rapporto e della radice per successioniPer a_n > 0: se a_(n+1)/a_n oppure la radice n-esima di a_n tende a un limite minore di 1 la successione tende a 0, se maggiore di 1 tende a +∞; con limite 1 non si conclude.Criteri del rapporto e della radice per successioni →).
  2. Logaritmo contro potenza: (log⁡bn)k∈o(nh)(\log_b n)^k \in o(n^h) per b>1b > 1, k,h>0k, h > 0. Si pone m=log⁡bnm = \log_b n: nh=(bh)mn^h = (b^h)^m con a=bh>1a = b^h > 1 e mk∈o(am)m^k \in o(a^m) per il punto 2.

Esempio. Per n=100n=100 si ha n3=106n^3=10^6 mentre 2n≈1,3⋅10302^n\approx1{,}3\cdot10^{30}, coerente con n3∈o(2n)n^3\in o(2^n).

Proprietà (legami tra le notazioni)

  1. Legami tra notazioni:
    • f∈o(g)⇒f∈O(g)f \in o(g) \Rightarrow f \in O(g) (si prende c=1c = 1);
    • f∈O(g)  ⟺  g∈Ω(f)f \in O(g) \iff g \in \Omega(f) (si divide per cc);
    • f∈O(g)f \in O(g) e g∈O(f)⇒f∈Θ(g)g \in O(f) \Rightarrow f \in \Theta(g);
    • f∈O(g)⇒f+g∈Θ(g)f \in O(g) \Rightarrow f + g \in \Theta(g) (perché g≤f+g≤(c+1)gg \le f + g \le (c+1)g);
    • non valgono: O⇒oO \Rightarrow o (f=g=nf = g = n) né O⇒ΘO \Rightarrow \Theta (f=nf = n, g=n2g = n^2).

Esempio. Con f=nf=n e g=n2g=n^2: f∈O(g)f\in O(g) e f∈o(g)f\in o(g), ma f∉Θ(g)f\notin\Theta(g).

Formula (sommatorie notevoli)

∑i=0ni=n(n+1)2,∑i=0ni2=n(n+1)(2n+1)6∈Θ(n3),∑i=0nai=an+1−1a−1 (a≠1).\sum_{i=0}^{n} i = \frac{n(n+1)}{2}, \qquad \sum_{i=0}^{n} i^2 = \frac{n(n+1)(2n+1)}{6} \in \Theta(n^3), \qquad \sum_{i=0}^{n} a^i = \frac{a^{n+1} - 1}{a - 1}\ (a \ne 1).

Esempio. ∑i=010i=10⋅112=55\sum_{i=0}^{10}i=\frac{10\cdot11}{2}=55 e ∑i=032i=24−12−1=15\sum_{i=0}^{3}2^i=\frac{2^4-1}{2-1}=15.

Dimostrazioni, induzione e invarianti

Teorema (principio di induzione). Per provare che Q(n)Q(n) vale per ogni n≥n0n \ge n_0:

  1. si sceglie k≥0k \ge 0 (di solito k=0k = 0);
  2. base: si dimostra Q(n0),Q(n0+1),…,Q(n0+k)Q(n_0), Q(n_0+1), \dots, Q(n_0+k);
  3. passo: si fissa n≥n0+kn \ge n_0 + k arbitrario e, assumendo Q(m)Q(m) vera per ogni mm con n0≤m≤nn_0 \le m \le n (ipotesi induttiva), si dimostra Q(n+1)Q(n+1).

Esempio 1. ∑i=0ni=n(n+1)2\sum_{i=0}^{n} i = \frac{n(n+1)}{2} per n≥0n \ge 0 (sommatorie: SommatorieIl simbolo di sommatoria, le sue proprietà (linearità, additività, cambio di indice) e le somme notevoli di Gauss e geometrica.Sommatorie →). Base n=0n = 0: a sinistra c'è la somma del solo termine i=0i=0, cioè 00; a destra 0⋅12=0\frac{0\cdot1}{2}=0. Passo: si suppone vera la formula per nn e si scrive la somma fino a n+1n+1 isolando l'ultimo termine, ∑i=0n+1i=∑i=0ni⏟ipotesi induttiva+(n+1)=n(n+1)2+(n+1)=n(n+1)+2(n+1)2=(n+1)(n+2)2,\sum_{i=0}^{n+1} i = \underbrace{\sum_{i=0}^{n} i}_{\text{ipotesi induttiva}} + (n+1) = \frac{n(n+1)}{2} + (n+1) = \frac{n(n+1) + 2(n+1)}{2} = \frac{(n+1)(n+2)}{2}, dove si è messo a denominatore comune 22 e raccolto (n+1)(n+1). L'ultimo membro è proprio la formula con n+1n+1 al posto di nn. ✓

Definizione (invariante di ciclo). Un invarianteProprietà sulle variabili del ciclo che descrive lo stato a ogni iterazione. è una proprietà delle variabili usate nel ciclo che:

  1. vale all'inizio del ciclo, come conseguenza dello stato iniziale (inizializzazione);
  2. vale alla fine di ogni iterazione, assumendo che valesse all'inizio di quella iterazione (conservazione);
  3. alla fine dell'ultima iterazione, insieme alla condizione di uscita, implica la correttezza del ciclo (uso).

Esempio. In arrayMax l'invariante «alla fine dell'iterazione ii, currMax=max⁡{A[0],…,A[i]}currMax=\max\{A[0],\dots,A[i]\}» vale all'inizio (currMax=A[0]currMax=A[0]), si conserva a ogni passo e a fine ciclo (i=n−1i=n-1) dà il massimo dell'intero array.

Algoritmi ricorsivi

Definizione (algoritmo ricorsivo). Un algoritmo ricorsivo invoca se stesso su istanze sempre più piccole. La soluzione di un'istanza di taglia nn si ottiene:

  • direttamente per i casi base n=n0,…,n0+kn = n_0, \dots, n_0 + k;
  • altrimenti usando le soluzioni di una o più istanze di taglia <n< n.

Esempio. LinearSum(A, n) ha il caso base n=1n=1 e per n>1n>1 usa LinearSum(A, n-1): su A=[3,1,4]A=[3,1,4] con n=3n=3 restituisce 3+1+4=83+1+4=8.

Formula (potenza in tempo logaritmico). xn=1x^n = 1 se n=0n = 0; x⋅(x(n−1)/2)2x \cdot (x^{(n-1)/2})^2 se nn dispari; (xn/2)2(x^{n/2})^2 se nn pari e positivo.

Esempio. Power(2, 10) calcola 210=(25)22^{10}=(2^5)^2 con 55 chiamate (n=10,5,2,1,0n=10,5,2,1,0) e restituisce 10241024.

Formula (numero di chiamate di fib). Il numero di chiamate C(n)C(n) soddisfa C(0)=C(1)=1C(0) = C(1) = 1 e C(n)=1+C(n−1)+C(n−2)C(n) = 1 + C(n-1) + C(n-2), cioè C(n)=2F(n+1)−1C(n) = 2F(n+1) - 1: 1,1,3,5,9,15,25,41,…1, 1, 3, 5, 9, 15, 25, 41, \dots (n=0,1,2,3,4,5,6,7n = 0, 1, 2, 3, 4, 5, 6, 7; per fib(5)\mathrm{fib}(5) sono 1515 chiamate, verificato eseguendo il codice). Cresce come Φn\Phi^n: esponenziale. Lo spreco è nei sottoproblemi ripetuti (fib(3)\mathrm{fib}(3) è ricalcolato più volte).

Esempio. C(5)=2F(6)−1=2⋅8−1=15C(5)=2F(6)-1=2\cdot8-1=15 chiamate per fib(5).

{"tipo":"funzione","titolo":"Chiamate di fib(n): senza memoizzazione crescono come 2Φ^(n+1)/√5 − 1 (C(5) = 15, C(10) = 177), con memoizzazione sono 2n − 1 (9 per n = 5, 19 per n = 10)","curve":[{"espressione":"2*((1+sqrt(5))/2)^(x+1)/sqrt(5)-1","etichetta":"senza memoizzazione: C(n)"},{"espressione":"2*x-1","etichetta":"con memoizzazione: 2n−1"}],"x":[1,12],"y":[0,500],"punti":[{"x":5,"y":15,"etichetta":"15"},{"x":10,"y":177,"etichetta":"177"}],"assi":{"x":"n","y":"chiamate"}}
``` Con la **memoizzazione** (si salva ogni risultato in una tabella e lo si riusa) per fib(5)\mathrm{fib}(5) le chiamate scendono a 99 e in generale a Θ(n)\Theta(n) (ciascun fib(k)\mathrm{fib}(k) è calcolato una volta sola). Per fib(5)\mathrm{fib}(5): la discesa fib(5),fib(4),fib(3),fib(2),fib(1),fib(0)\mathrm{fib}(5),\mathrm{fib}(4),\mathrm{fib}(3),\mathrm{fib}(2),\mathrm{fib}(1),\mathrm{fib}(0) fa 66 chiamate e riempie la tabella; poi fib(3)\mathrm{fib}(3) chiama fib(1)\mathrm{fib}(1) (caso base, una chiamata), fib(4)\mathrm{fib}(4) chiama fib(2)\mathrm{fib}(2) (già in tabella) e fib(5)\mathrm{fib}(5) chiama fib(3)\mathrm{fib}(3) (già in tabella): 6+3=96+3=9. In generale n+1n+1 chiamate di discesa più n−2n-2 chiamate che trovano già il risultato, circa 2n2n. La memoizzazione usa Θ(n)\Theta(n) di memoria per la tabella: è lo scambio tempo contro spazio.

*Esempio.* C(5)=2F(6)−1=2⋅8−1=15C(5)=2F(6)-1=2\cdot8-1=15 chiamate per `fib(5)`.

## Correttezza per induzione

Sia nn la taglia. Si dimostra la correttezza per i casi base n∈[n0,n0+k]n \in [n_0, n_0 + k]; poi, assumendo che l'algoritmo risolva correttamente tutte le istanze di taglia m∈[n0,n]m \in [n_0, n], si dimostra che risolve quelle di taglia n+1n + 1.

**LinearSum.** Base n=1n = 1: restituisce A[0]A[0] ✓. Passo: per ipotesi `LinearSum(A, n)` =∑i=0n−1A[i]= \sum_{i=0}^{n-1} A[i], quindi `LinearSum(A, n+1)` =∑i=0n−1A[i]+A[n]=∑i=0nA[i]= \sum_{i=0}^{n-1} A[i] + A[n] = \sum_{i=0}^{n} A[i] ✓.

**Power.** Base n=0n = 0: x0=1x^0 = 1 ✓. Passo: si assume che `Power` sia corretta per **tutti** gli esponenti da 00 a nn (ipotesi forte: serve perché la chiamata ricorsiva è su circa la metà dell'esponente, non su nn). Se n+1n+1 è pari, la chiamata è su (n+1)/2≤n(n+1)/2 \le n (vero per n≥1n\ge1), quindi per ipotesi y=x(n+1)/2y = x^{(n+1)/2} e y⋅y=xn+1y\cdot y = x^{n+1}. Se n+1n+1 è dispari, la chiamata è su n/2≤nn/2 \le n, quindi y=xn/2y = x^{n/2} e x⋅y⋅y=x⋅xn=xn+1x \cdot y \cdot y = x \cdot x^{n} = x^{n+1} ✓ (si usa xaxb=xa+bx^a x^b = x^{a+b}, vedi <a class="wiki glossa" href="/analisi-1/appunti/funzioni-potenza/" aria-describedby="sp-17">Funzioni potenza<span class="spiega" role="tooltip"><span class="spiega-testo" id="sp-17">Le potenze x^alfa al variare dell&#39;esponente: domini, grafici, radici n-esime e proprietà delle potenze con base positiva.<span class="spiega-vai">Funzioni potenza →</span></span><span class="graffa" aria-hidden="true"><i class="g-l"></i><i class="g-linea"></i><i class="g-punta"></i><i class="g-linea"></i><i class="g-r"></i></span></span><!--/spiega--></a>).

## Errori comuni

- Dimenticare un caso base, o una chiamata che non riduce la taglia (non termina): in `ReverseArray` serve i<ji < j.
- Contare il costo di un nodo **includendo** le chiamate figlie (le conta due volte).
- Ricalcolare più volte la stessa sottoistanza (`Power` con due chiamate, `fib` senza memoizzazione).
- Dimenticare che la ricorsione consuma stack: profondità dd ⇒ spazio Θ(d)\Theta(d).

II. Il linguaggio C

Puntatori, struct e memoria dinamica in C

Definizione (puntatore). Un puntatore è una variabile che contiene un indirizzo di memoria. &x è l'indirizzo di x, *p è il valore puntato da p (dereferenziazione), NULL è il puntatore che non punta a nulla.

Esempio. Con int x = 5; int *p = &x; *p = 7; si ottiene x == 7. Passo per passo, supponendo che x sia all'indirizzo 10001000 (valore indicativo):

Proprietà (passaggio dei parametri per valore). C passa i parametri per valore: la funzione riceve una copia. Per modificare una variabile del chiamante si passa il suo indirizzo.

Esempio. Con void incrementa(int *p) { (*p)++; } e int n = 5; incrementa(&n); si ha n == 6.

Proprietà (array e aritmetica dei puntatori). Un array è un blocco contiguo; il suo nome si comporta come puntatore al primo elemento, quindi v[i] equivale a *(v + i) (la somma avanza di i elementi, non di i byte). Un array passato a una funzione arriva come puntatore: la lunghezza non si conserva e va passata a parte (int n). Nessun controllo sui limiti: v[n] è un errore silenzioso.

Esempio. Con int v[3] = {10, 20, 30}; si ha *(v + 2) == v[2] == 30. Se v fosse all'indirizzo 10001000 e sizeof(int) fosse 44, gli elementi occuperebbero i byte 10001000–10031003, 10041004–10071007, 10081008–10111011 e v + 2 varrebbe 1000+2⋅4=10081000 + 2 \cdot 4 = 1008: il compilatore moltiplica i per la dimensione del tipo puntato. Per questo v[i] costa Θ(1)\Theta(1) (un'addizione e una lettura), la stessa assunzione del modello RAMProblema computazionale come insieme di coppie (istanza, soluzione); algoritmo e modello di calcolo RAM; pseudocodice; taglia di un'istanza; ADT e struttura dati concreta; esempio svolto con ricerca lineare e binaria in un array ordinato.Problemi computazionali e algoritmi →. Altri dettagli sugli array sono in Array e stringhe in CArray di dimensione fissa in memoria contigua, inizializzazione, nessun controllo sugli indici, passaggio a funzioni con la lunghezza, matrici; stringhe come array di char terminati da '\0' e funzioni di string.h.Array e stringhe in C →.

Definizione (stringa in C). Una stringa è un array di char terminato da '\0': char nome[64] contiene al più 63 caratteri più il terminatore. Si copia con strcpy, si confronta con strcmp, mai con = o ==.

Esempio. strlen("ciao") vale 4, ma la stringa occupa sizeof("ciao") == 5 byte per il terminatore '\0': in memoria ci sono i cinque char 'c', 'i', 'a', 'o', '\0'. I caratteri sono memorizzati come interi piccoli secondo una codifica (vedi Codifiche binarie e informazione non numericaBit, byte e multipli (potenze di 2 e di 10); codici BCD e Gray; caratteri ASCII, Unicode e UTF-8; ordine dei byte (little e big endian); bit di parità e codice di Hamming per rilevare e correggere errori.Codifiche binarie e informazione non numerica →) e '\0' è il valore 00. strlen conta i caratteri fino al primo '\0', quindi costa Θ(lunghezza)\Theta(\text{lunghezza}) e non Θ(1)\Theta(1). == tra due stringhe confronterebbe i due indirizzi, non il contenuto.

Definizione (operatori . e ->). . si usa su una struct, -> su un puntatore a struct. Una struct che contiene un puntatore a se stessa permette le strutture collegate:

typedef struct nodo {
    int val;
    struct nodo *next;   /* dentro la definizione serve il nome "struct nodo" */
} Nodo;

Esempio. Con Studente s; s.voto = 28; Studente *ps = &s; ps->voto++; si ottiene s.voto == 29.

Definizione (memoria dinamica con malloc e free). Le variabili locali vivono nello stack e spariscono quando la funzione termina; malloc alloca nello heap un blocco che dura finché non lo si libera con free.

Esempio. int *a = malloc(4 * sizeof(int)); riserva 16 byte nello heap; dopo a[i] = i * i si ha a[3] == 9, e free(a); li libera.

Proprietà (puntatore a puntatore). Un puntatore passato per valore è una copia: assegnarlo dentro la funzione non cambia quello del chiamante. Per modificarlo (tipicamente la testa di una lista o la radice di un albero) si passa il suo indirizzo, cioè un Nodo **.

Esempio. Con void azzeraGiusto(int **p) { *p = NULL; }, la chiamata azzeraGiusto(&a) lascia a == NULL.

Liste concatenate in C

Definizione (lista concatenata). Una lista concatenata è una sequenza di nodi allocati nello heap, ciascuno con un valore e un puntatore al successivo; la lista è identificata dal puntatore alla testa (NULL = lista vuota). Premessa: Puntatori, struct e memoria dinamica in CPuntatori e passaggio per riferimento in C, array e aritmetica dei puntatori, stringhe, struct e typedef con l'operatore ->, malloc e free, puntatore a puntatore per modificare una testa; compilazione con Makefile; errori tipici (puntatori pendenti, perdite di memoria, off-by-one).Puntatori, struct e memoria dinamica in C →. È l'implementazione concreta degli ADT di Liste, pile e codeRipasso degli ADT elementari: lista index-based (array) e position-based (lista doppiamente concatenata con sentinelle), pila LIFO, coda FIFO, deque, iteratori; interfacce, implementazioni e costi; array estendibile e coda circolare.Liste, pile e code →. La stessa struttura è presentata, in versione più elementare, in Liste concatenateLista concatenata semplice: nodi allocati dinamicamente collegati da puntatori; inserimento e rimozione in testa, scorrimento, ricerca, inserimento ordinato, deallocazione; confronto dei costi con l'array; versione in Python.Liste concatenate → e Pila e codaPila (LIFO) e coda (FIFO) come ADT: operazioni e costi; realizzazione in Python con list e collections.deque; realizzazione in C con array e indici, coda circolare; applicazioni (parentesi bilanciate, stack delle chiamate, visite).Pila e coda → (Fondamenti di Informatica); i puntatori usati qui sono quelli di Puntatori in CUn puntatore contiene un indirizzo di memoria; operatori & e *, NULL, puntatori come parametri per modificare variabili del chiamante, aritmetica dei puntatori, legame tra array e puntatori, const, puntatori a puntatori.Puntatori in C → e i blocchi di malloc vivono nello heap (Gestione della memoria in CSegmenti di memoria di un processo (codice, dati statici, stack, heap); durata delle variabili; allocazione dinamica con malloc, calloc, realloc e free; errori classici: memory leak, dangling pointer, double free, buffer overflow; strumenti di controllo.Gestione della memoria in C →).

Esempio. Dopo addHead di 11, 22, 33 in una lista vuota la lista è 3 2 1, con la testa che punta a 3. Stato dopo ciascuna chiamata (la freccia è il campo next):

Proprietà (testa passata per riferimento). Ogni funzione che può cambiare la testa riceve un Nodo **l (puntatore alla variabile testa del chiamante).

Esempio. Con Nodo *l = NULL; la chiamata addHead(&l, 5) rende l != NULL: senza il doppio puntatore la testa del chiamante non cambierebbe.

Definizione (inversione in loco). Inversione in loco, O(n)O(n) e senza nodi nuovi: si ritorcono i puntatori scorrendo la lista.

Esempio. La lista 3 2 1 10 20 diventa 20 10 1 2 3 dopo inverti. Traccia su una lista più corta, 1 2 3 (ogni riga è lo stato alla fine dell'iterazione del while):

Definizione (coda con puntatori a testa e coda). Per rendere O(1)O(1) anche l'inserimento in fondo si tengono due puntatori, in una struct:

typedef struct { Nodo *testa; Nodo *coda; } Coda;     /* estrae da testa, inserisce in coda */

void enqueue(Coda *q, int v) {
    Nodo *n = malloc(sizeof(Nodo));
    n->val = v; n->next = NULL;
    if (q->coda == NULL) q->testa = n;     /* coda vuota */
    else q->coda->next = n;
    q->coda = n;
}

int dequeue(Coda *q) {                     /* precondizione: non vuota */
    Nodo *n = q->testa;
    int v = n->val;
    q->testa = n->next;
    if (q->testa == NULL) q->coda = NULL;  /* si e' svuotata */
    free(n);
    return v;
}

int isEmpty(const Coda *q) { return q->testa == NULL; }

Esempio. Inseriti 10,20,30,4010,20,30,40 con enqueue, due dequeue danno 1010 e 2020 (ordine FIFO).

Proprietà (costi delle operazioni sulle liste concatenate)

Operazione Lista semplice Con puntatore alla coda
addHead, pop Θ(1)\Theta(1) Θ(1)\Theta(1)
addTail Θ(n)\Theta(n) Θ(1)\Theta(1)
ricerca di un valore Θ(n)\Theta(n) Θ(n)\Theta(n)
accesso al nodo ii-esimo Θ(i)\Theta(i) Θ(i)\Theta(i)

Esempio. Con n=1000n=1000 nodi addTail ne scorre 10001000 in una lista semplice e nessuno con il puntatore alla coda. Costruire una lista di nn elementi con nn addTail su lista semplice costa ∑k=0n−1k=n(n−1)2∈Θ(n2)\sum_{k=0}^{n-1} k = \frac{n(n-1)}{2} \in \Theta(n^2) (somma di SommatorieIl simbolo di sommatoria, le sue proprietà (linearità, additività, cambio di indice) e le somme notevoli di Gauss e geometrica.Sommatorie →, come in Complessità in tempo e caso pessimoPerché lo studio sperimentale non basta; complessità al caso pessimo come massimo sul numero di operazioni tra le istanze di una data taglia; stima con limiti superiore e inferiore senza trovare l'istanza peggiore; esempi arrayMax, prefixAverages e InsertionSort; efficienza asintotica e limiti dell'analisi.Complessità in tempo e caso pessimo →), contro Θ(n)\Theta(n) con addHead o con il puntatore alla coda.

III. ADT elementari

Liste, pile e code

Definizione (lista index-based). L'elemento si identifica con l'indice i≥0i \ge 0 = numero di elementi che lo precedono.

Metodo Effetto
size(), isEmpty() numero di elementi, lista vuota
get(i) elemento in posizione ii, senza toglierlo
set(i, e) sostituisce l'elemento in ii con e e restituisce il vecchio
add(i, e) inserisce e all'indice ii spostando a destra i successivi
remove(i) toglie e restituisce l'elemento in ii, spostando a sinistra i successivi

Esempio. Su [7, 8, 9] si ha get(2) = 9, e add(1, 5) produce [7, 5, 8, 9].

Proprietà (array estendibile con raddoppio). Si realizza con un array: get, set, size in O(1)O(1); add e remove in O(n)O(n) per gli spostamenti. Se l'array è pieno si alloca un array di taglia doppia e si copiano gli elementi: il costo Θ(n)\Theta(n) della copia è ammortizzato dai nn inserimenti precedenti, quindi l'inserimento in fondo costa O(1)O(1) ammortizzato.

Esempio. Partendo da capacità 11 e raddoppiando, 88 inserimenti in fondo copiano in tutto 1+2+4=71+2+4=7 elementi: meno di una copia per inserimento.

Grafico interattivo: Elementi copiati in totale dopo n inserimenti in fondo: con raddoppio (2^ceil(log2 n) − 1, sempre sotto 2n) contro allungamento di una cella alla volta (n(n−1)/2); a n = 30 sono 31 contro 435

Definizione (lista position-based con doppia lista e sentinelle). Si realizza con una lista doppiamente concatenata: ogni nodo punta a predecessore e successore, e due nodi sentinella (senza elemento) delimitano inizio e fine, così nessun inserimento o rimozione ha casi particolari. Tutti i metodi costano O(1)O(1), ma la lista si scandisce solo in sequenza (arrivare al nodo ii costa Θ(i)\Theta(i)). L'implementazione in C con puntatori è in Liste concatenate in CLista singolarmente concatenata in C con nodo struct e testa passata per riferimento (Nodo **); addHead, addTail ricorsiva e iterativa, pop, inversione in loco, liberazione; coda con puntatori a testa e coda; ricorsione sulle liste; costi.Liste concatenate in C → e i puntatori in Puntatori in CUn puntatore contiene un indirizzo di memoria; operatori & e *, NULL, puntatori come parametri per modificare variabili del chiamante, aritmetica dei puntatori, legame tra array e puntatori, const, puntatori a puntatori.Puntatori in C →.

Esempio. Dato un nodo pp, addAfter(p, e) collega il nuovo nodo ai suoi due vicini in O(1)O(1) passi, senza scorrere la lista.

Definizione (pila LIFO). Inserimenti e rimozioni secondo LIFO (last in, first out): push(e) inserisce in cima, top() legge la cima senza togliere, pop() toglie e restituisce la cima, più size() e isEmpty(). Con un array e un indice t di cima, o con una lista concatenata con inserimento e rimozione in testa, tutti i metodi sono O(1)O(1) (con array estendibile, ammortizzato). Esempi: stack delle chiamate di una funzione (i record di attivazione di Algoritmi ricorsiviAlgoritmi ricorsivi come induzione eseguita; albero della ricorsione e record di attivazione nello stack; esempi ReverseArray, LinearSum, Power in tempo logaritmico, Fibonacci ricorsivo (esponenziale) e con memoizzazione; analisi della complessità con l'albero della ricorsione e correttezza per induzione.Algoritmi ricorsivi → e Stack e chiamata di funzioni in ARMChiamata con BL e ritorno con BX LR; convenzione di chiamata AAPCS (argomenti in r0-r3, risultato in r0, registri da preservare r4-r11); stack discendente pieno con PUSH e POP; record di attivazione; funzioni foglia e non foglia; esempio ricorsivo del fattoriale con evoluzione dello stack.Stack e chiamata di funzioni in ARM →), annulla/ripeti, controllo delle parentesi.

Esempio. Dopo push(1), push(2), push(3) si ha top() = 3; un pop() restituisce 3 e la nuova cima è 2.

Definizione (coda FIFO e buffer circolare). Secondo FIFO (first in, first out): enqueue(e) inserisce in fondo, first() legge il primo, dequeue() toglie e restituisce il primo. Con una lista concatenata con puntatori a testa e coda, tutto in O(1)O(1). Con un array si usa un buffer circolare: indice f del primo e size; enqueue scrive in (f + size) mod N, dequeue avanza f = (f + 1) mod N. Usata dalla visita in ampiezza dei grafi (vedi Visite di grafi - BFS e DFSVisite in ampiezza (BFS) e in profondità (DFS) come design pattern; etichette discovery, cross e back edge; BFS tree e distanze; complessità Theta(n+m) con liste di adiacenza; applicazioni (connettività, componenti, spanning tree, cammini minimi non pesati, cicli, vertici a distanza al più d); esempio svolto su un grafo di 7 vertici.Visite di grafi - BFS e DFS →).

Esempio. Con N=5N=5, primo in f = 3 e size = 2, enqueue scrive in (3+2) mod 5=0(3+2)\bmod5=0; dopo un dequeue si ha f = 4. Il resto della divisione per NN fa "tornare a capo" l'indice: le celle 33 e 44 sono occupate, la prossima libera è 5 mod 5=05 \bmod 5 = 0. Traccia con N=5N=5:

Definizione (deque). La deque (double-ended queue) permette inserimento e rimozione sia in testa sia in coda: unisce pila e coda.

Esempio. Dopo addFirst(1), addLast(2), addFirst(3) la deque contiene 3, 1, 2.

IV. Alberi

Alberi

Definizione (albero radicato). Un albero radicato TT è una collezione di nodi che, se non vuota, soddisfa:

  • esiste un nodo speciale rr, la radice;
  • ogni v≠rv \ne r ha un unico padre uu (vv è figlio di uu);
  • risalendo di padre in padre da ogni nodo si arriva a rr.

Definizione ricorsiva: T={r}∪T1∪⋯∪TkT = \{r\} \cup T_1 \cup \dots \cup T_k con k≥0k \ge 0, dove T1,…,TkT_1, \dots, T_k sono alberi non vuoti le cui radici sono figlie di rr.

Esempio. Un albero con radice rr, figli aa e bb e un nodo cc figlio di aa ha n=4n=4 nodi; le foglie sono bb e cc.

Definizione (terminologia degli alberi)

Termine Significato
antenato di yy x=yx = y oppure xx antenato del padre di yy
discendente di yy xx tale che yy è antenato di xx
nodo interno nodo con almeno un figlio
nodo esterno o foglia nodo senza figli
sottoalbero TvT_v vv con tutti i suoi discendenti
albero ordinato per ogni nodo interno è fissato un ordine lineare tra i figli

Esempio. Nello stesso albero rr è antenato di cc, il sottoalbero TaT_a è {a,c}\{a,c\}, aa è un nodo interno e bb, cc sono foglie.

Definizione (profondità, livello e altezza)

  • Profondità depth(v)\text{depth}(v) = numero di antenati di vv meno 1; ricorsivamente: depth(r)=0\text{depth}(r) = 0, altrimenti 1+depth(padre(v))1 + \text{depth}(\text{padre}(v)).
  • Livello ii = insieme dei nodi a profondità ii.
  • Altezza di un nodo: height(v)=0\text{height}(v) = 0 se vv è foglia, altrimenti 1+max⁡w figlio di vheight(w)1 + \max_{w \text{ figlio di } v} \text{height}(w). Altezza dell'albero: height(T)=height(r)\text{height}(T) = \text{height}(r).

Esempio. Nello stesso albero depth(c)=2\text{depth}(c)=2, height(a)=1\text{height}(a)=1 e height(T)=height(r)=2\text{height}(T)=\text{height}(r)=2.

Proposizione (altezza e profondità delle foglie). Se TT non è vuoto, height(T)=max⁡{depth(v):v foglia}\text{height}(T) = \max\{\text{depth}(v) : v \text{ foglia}\}.

Esempio. Le foglie bb e cc hanno profondità 11 e 22, quindi height(T)=max⁡{1,2}=2\text{height}(T)=\max\{1,2\}=2. Sull'albero di riferimento le foglie b,c,eb,c,e hanno profondità 1,2,31,2,3 e height(T)=3\text{height}(T)=3.

Visite di alberi

Definizione (visite in preorder e postorder). Chiamata iniziale: preorder(T, T.root()). Preorder: prima il padre, poi i sottoalberi dei figli. Postorder: prima i sottoalberi dei figli, poi il padre.

Esempio. Sull'albero con radice rr, figli aa, bb e cc figlio di aa: preorder r,a,c,br,a,c,b e postorder c,a,b,rc,a,b,r.

Formula (complessità di una visita). L'albero della ricorsione (vedi Algoritmi ricorsiviAlgoritmi ricorsivi come induzione eseguita; albero della ricorsione e record di attivazione nello stack; esempi ReverseArray, LinearSum, Power in tempo logaritmico, Fibonacci ricorsivo (esponenziale) e con memoizzazione; analisi della complessità con l'albero della ricorsione e correttezza per induzione.Algoritmi ricorsivi →) ha un nodo per ogni nodo di TT. Sia tut_u il costo della visita di uu e cuc_u il numero di figli. La chiamata su uu costa Θ(1+cu)+tu\Theta(1 + c_u) + t_u (il ciclo sui figli, escluse le chiamate ricorsive, più la visita). Poiché ∑ucu=n−1\sum_u c_u = n - 1 (vedi AlberiAlbero radicato (definizione per padre e ricorsiva), terminologia (antenati, discendenti, nodi interni ed esterni, sottoalbero, albero ordinato), profondità, livello, altezza; altezza = massima profondità delle foglie; algoritmi depth e height con costo; somma dei figli = n-1; esempio di algoritmo Omega(n^2) (heightBad).Alberi →: ogni nodo non radice ha un solo padre, quindi gli archi sono n−1n-1):

t(n)=∑u∈T(Θ(1+cu)+tu)=Θ(n+∑u∈Ttu).t(n) = \sum_{u \in T} \big(\Theta(1 + c_u) + t_u\big) = \Theta\Big(n + \sum_{u \in T} t_u\Big).

Se ogni visita costa Θ(1)\Theta(1) si ha Θ(n)\Theta(n); se tu∈Θ(log⁡n)t_u \in \Theta(\log n) per ogni uu si ha Θ(nlog⁡n)\Theta(n \log n).

Esempio. In un albero con n=7n=7 nodi ∑ucu=n−1=6\sum_uc_u=n-1=6, quindi ∑u(1+cu)=2n−1=13\sum_u(1+c_u)=2n-1=13.

Definizione (antenato comune più basso, LCA). Dati v,wv, w in TT, LCA(v,w)\text{LCA}(v, w) è l'antenato comune più profondo. Algoritmo: si calcolano dv=depth(v)d_v = \text{depth}(v) e dw=depth(w)d_w = \text{depth}(w) risalendo; si porta il nodo più profondo a salire di ∣dv−dw∣|d_v - d_w| passi, poi si salgono insieme vv e ww finché coincidono. Costo O(depth(v)+depth(w))=O(h)O(\text{depth}(v) + \text{depth}(w)) = O(h), al caso pessimo O(n)O(n) e senza strutture di appoggio. La differenza di profondità è un valore assolutoIl valore assoluto è la distanza di un numero da zero: definizione, proprietà, disuguaglianza triangolare e disequazioni con i moduli.Valore assoluto (modulo) → perché non si sa quale dei due nodi sia più profondo. Perché funziona: dopo aver portato i due nodi alla stessa profondità, i loro antenati alla stessa profondità sono confrontabili e il primo in comune che si incontra salendo è, per definizione, il più profondo tra quelli comuni.

Esempio. Nell'albero con radice rr, figli aa, bb e cc figlio di aa: LCA(c,b)=r\text{LCA}(c,b)=r e LCA(c,a)=a\text{LCA}(c,a)=a.

Alberi binari

Definizione (albero binario e albero binario proprio). Un albero binario è un albero ordinato (vedi AlberiAlbero radicato (definizione per padre e ricorsiva), terminologia (antenati, discendenti, nodi interni ed esterni, sottoalbero, albero ordinato), profondità, livello, altezza; altezza = massima profondità delle foglie; algoritmi depth e height con costo; somma dei figli = n-1; esempio di algoritmo Omega(n^2) (heightBad).Alberi →) in cui ogni nodo interno ha al più 2 figli, ogni nodo non radice è etichettato come figlio sinistro o destro del padre, e il figlio sinistro precede il destro. Se c'è un solo figlio si assume sia il sinistro. Il sottoalbero sinistro (destro) di vv è quello radicato nel suo figlio sinistro (destro).

Un albero binario è proprio (o pieno, full) se ogni nodo interno ha esattamente 2 figli. Un albero binario è un caso particolare di albero (AlberiAlbero radicato (definizione per padre e ricorsiva), terminologia (antenati, discendenti, nodi interni ed esterni, sottoalbero, albero ordinato), profondità, livello, altezza; altezza = massima profondità delle foglie; algoritmi depth e height con costo; somma dei figli = n-1; esempio di algoritmo Omega(n^2) (heightBad).Alberi →) e di grafo senza cicli (Grafi - definizioni e proprietàGrafo G=(V,E) diretto e non diretto, grafo semplice e pesato; incidenza, adiacenza, grado; cammini, cicli, sottografi, grafi connessi e componenti connesse; alberi liberi, foreste, spanning tree e spanning forest; proprietà con dimostrazioni (somma dei gradi = 2m, m <= n(n-1)/2, alberi m = n-1, connessi m >= n-1, foreste m <= n-1).Grafi - definizioni e proprietà →); le interfacce sono ADT (Tipi di dato astrattiUn ADT è definito dalle operazioni e dal loro comportamento, non dalla rappresentazione; interfaccia e implementazione; realizzazione in Python con le classi e in C con header e tipo opaco; esempio di un ADT Frazione.Tipi di dato astratti →).

Esempio. Un albero con la radice, un figlio sinistro con due foglie e un figlio destro foglia è binario proprio: ogni nodo interno ha 22 figli e n=5n=5.

Teorema (relazioni in un albero binario proprio)

# Relazione
1 m=(n−m)+1m = (n - m) + 1
2 h+1≤m≤2hh + 1 \le m \le 2^h
3 h≤n−m≤2h−1h \le n - m \le 2^h - 1
4 2h+1≤n≤2h+1−12h + 1 \le n \le 2^{h+1} - 1
5 log⁡2(n+1)−1≤h≤n−12\log_2(n+1) - 1 \le h \le \frac{n-1}{2}

Esempio. Albero perfetto di altezza h=2h=2: n=23−1=7n=2^3-1=7, m=22=4m=2^2=4 foglie e n−m=3=m−1n-m=3=m-1.

Grafico interattivo: Altezza di un albero binario proprio con n nodi: limite inferiore log2(n+1) − 1 (albero perfetto) e limite superiore (n − 1)/2 (albero a pettine); per n = 31 l'altezza è tra 4 e 15

Definizione (visita inorder). Oltre a preorder e postorder (vedi Visite di alberiVisite in preorder e postorder come schemi generali (template) da adattare; complessità Theta(n + somma dei costi di visita) perché la somma dei figli è n-1; esempi (indice di un libro, spazio occupato in un file system); profondità con il preorder, altezza con il postorder; antenato comune più basso.Visite di alberi →), negli alberi binari si ha la visita inorder: prima il figlio sinistro, poi il padre, poi il figlio destro.

Esempio. Sull'albero con radice ++ e foglie 33 e 44 la visita inorder dà 3,+,43,+,4.

Definizione (parse tree di un'espressione). Un'espressione fully parenthesized in notazione infissa è una costante/variabile aa oppure (E1 Op E2)(E_1\ \text{Op}\ E_2) con E1,E2E_1, E_2 fully parenthesized e Op binario. Il parse tree è l'albero binario proprio che ha nelle foglie costanti e variabili, nei nodi interni gli operatori, con sottoalbero sinistro e destro uguali ai parse tree di E1E_1 ed E2E_2; le parentesi sono implicite nella struttura. Con operatori unari l'albero non è più proprio. Le espressioni aritmetiche e la precedenza degli operatori sono quelle di Tipi numerici ed espressioniint, float, complex e bool; operatori aritmetici con divisione intera e modulo; precedenza; conversioni di tipo; modulo math e arrotondamenti.Tipi numerici ed espressioni → (Fondamenti di Informatica); la costruzione del parse tree è una fase della compilazione (Paradigmi di programmazione e linguaggiAlgoritmo e programma; paradigmi imperativo, procedurale, a oggetti, funzionale e dichiarativo; compilazione e interpretazione; confronto tra Python e C.Paradigmi di programmazione e linguaggi →, Dal programma C all'eseguibile ARMCatena di traduzione compilatore, assemblatore, linker e loader; file oggetto, tabella dei simboli e rilocazione; collegamento statico e dinamico; organizzazione della memoria di un processo; chiamare funzioni assembly da C e viceversa, con esempio.Dal programma C all'eseguibile ARM →).

Esempio. ((15+x)⋅(7−(9÷3)))((15+x)\cdot(7-(9\div3))) con x=5x=5 vale 20⋅4=8020\cdot4=80.

Definizione (heightsum). La heightsum di un albero binario proprio è la somma delle altezze di tutti i suoi nodi. Con la sola somma restituita da ogni sottoalbero non si potrebbe calcolare l'altezza del padre; si restituisce quindi una coppia (somma, altezza):

Algoritmo heightSum(T, v)
Input: v in T      Output: (somma delle altezze dei nodi di T_v, altezza di v)
if T.isExternal(v) then return (0, 0)
(sL, hL) <- heightSum(T, T.left(v))
(sR, hR) <- heightSum(T, T.right(v))
h <- max(hL, hR) + 1
return (sL + sR + h, h)

Esempio. In un albero con la radice, un figlio sinistro con due foglie e un figlio destro con una foglia e un nodo con due foglie, le altezze dei nodi interni sono 3,1,2,13,1,2,1 e la heightsum vale 77.

V. Code con priorità e heap

Code con priorità

Definizione (coda con priorità). Una coda con priorità (priority queue) è una collezione di entry le cui chiavi, non necessariamente distinte, rappresentano priorità e provengono da un universo totalmente ordinato (due chiavi qualunque sono sempre confrontabili con ≤\le, come per i numeri reali: Campi ordinati (Q e R)Gli assiomi di somma, prodotto e ordine comuni a Q e R, da cui derivano le regole di equazioni e disequazioni.Campi ordinati (Q e R) →). Ingressi e uscite si alternano in modo arbitrario, le priorità in ingresso non sono ordinate, le uscite avvengono in ordine di priorità.

interfaccia PriorityQueue<K,V>:
    size(), isEmpty()
    insert(k, v)   inserisce e restituisce la nuova entry (k, v)
    min()          restituisce una entry con chiave minima, senza toglierla
    removeMin()    restituisce e toglie una entry con chiave minima

Per convenzione chiave minima = priorità massima. Una coda basata sul massimo ha max() e removeMax() al posto di min() e removeMin().

Esempio. Dopo insert(5,a), insert(2,b), insert(7,c), min() restituisce la entry con chiave 22 e removeMin() la toglie.

Proprietà (code con priorità su liste: costi). Lista non ordinata. insert: si aggiunge in coda, Θ(1)\Theta(1). min: si scorre tutta la lista tenendo la chiave minore, Θ(n)\Theta(n). removeMin: si trova il minimo e lo si toglie, Θ(n)\Theta(n).

Lista ordinata (per chiave crescente). min e removeMin: si guarda/toglie il primo nodo, Θ(1)\Theta(1). insert: si scorre fino alla prima chiave ≥k\ge k e si inserisce prima di essa, Θ(n)\Theta(n).

Esempio. Con n=1000n=1000 entry, removeMin scorre fino a 10001000 entry in una lista non ordinata e ne guarda una sola in una lista ordinata. Nella lista ordinata insert(6, E) sul contenuto (3,B) (5,A) (7,D) (9,C)(3,B)\ (5,A)\ (7,D)\ (9,C) confronta 66 con 33, 55, 77 e si ferma davanti a 77 (prima chiave ≥6\ge6): il risultato è (3,B) (5,A) (6,E) (7,D) (9,C)(3,B)\ (5,A)\ (6,E)\ (7,D)\ (9,C), con fino a nn confronti nel caso peggiore (chiave maggiore di tutte).

Proprietà (ordinamento con una coda con priorità). Per ordinare nn chiavi: si fanno nn insert e poi nn removeMin, che restituiscono le chiavi in ordine crescente. Il costo dipende dall'implementazione:

  • lista non ordinata: n⋅Θ(1)+n⋅Θ(n)=Θ(n2)n \cdot \Theta(1) + n \cdot \Theta(n) = \Theta(n^2) (è il selection sort);
  • lista ordinata: n⋅Θ(n)+n⋅Θ(1)=Θ(n2)n \cdot \Theta(n) + n \cdot \Theta(1) = \Theta(n^2) (è l'insertion sort);
  • heap: n⋅Θ(log⁡n)+n⋅Θ(log⁡n)=Θ(nlog⁡n)n \cdot \Theta(\log n) + n \cdot \Theta(\log n) = \Theta(n \log n) (è l'heap sort).

Esempio. Con le chiavi [5,2,7,1][5,2,7,1], quattro insert seguiti da quattro removeMin restituiscono 1,2,5,71,2,5,7.

Grafico interattivo: Confronti di ordinamento con coda con priorità: liste (selection e insertion sort) n(n+1)/2 contro heap (heap sort) 2·n·log2 n; a n = 100 sono 5050 contro 1329, e il divario cresce con n

Heap

Definizione (albero binario completo). Un albero binario di altezza hh è completo se:

  • per ogni livello ii con 0≤i≤h−10 \le i \le h - 1 ci sono 2i2^i nodi (livello pieno);
  • al livello hh i nodi sono tutti a sinistra delle eventuali mancanze: nessun buco, e al più l'ultimo nodo interno ha solo il figlio sinistro.

Esempio. Con h=2h=2 i livelli 00 e 11 sono pieni (1+21+2 nodi) e il livello 22 ha da 11 a 44 nodi tutti a sinistra: nn va da 44 a 77.

Proposizione (altezza di un albero binario completo). Un albero binario completo con nn nodi ha altezza h=⌊log⁡2n⌋h = \lfloor \log_2 n \rfloor.

Esempio. n=8n=8 dà h=⌊log⁡28⌋=3h=\lfloor\log_28\rfloor=3, n=15n=15 dà h=3h=3 e n=16n=16 dà h=4h=4.

Grafico interattivo: Altezza di un albero binario completo, floor(log2 n): 3 per n = 11 (gli heap degli esempi), 5 per n = 63; ogni volta che n raddoppia l'altezza cresce di 1

Definizione (heap). Uno heap è un albero binario completo i cui nodi memorizzano entry e che soddisfa la heap-order property: per ogni nodo vv diverso dalla radice, key(v)≥key(padre(v))\text{key}(v) \ge \text{key}(\text{padre}(v)). Equivale a: ogni nodo ha chiave ≤\le quella dei figli. (Un max-heap inverte la disuguaglianza.) Il nodo last è il più a destra del livello hh.

Esempio. L'array [4,6,9,15,7,16,12][4,6,9,15,7,16,12] è uno heap: 6,9≥46,9\ge4, 15,7≥615,7\ge6 e 16,12≥916,12\ge9.

Proprietà (heap con nn entry). Proprietà (heap con nn entry):

  1. la radice contiene una entry con chiave minima;
  2. le chiavi lungo un cammino dalla radice a una foglia formano una sequenza non decrescente;
  3. con chiavi distinte, se eie_i è l'entry con ii-esima chiave più piccola (e1e_1 = radice) e sta a profondità dd, allora d<id < i (gli dd antenati di eie_i sono altrettante entry con chiave minore, più eie_i stessa);
  4. con chiavi distinte, l'entry con chiave massima sta in una foglia (per assurdo: se fosse in un nodo interno, un suo figlio avrebbe chiave maggiore);
  5. per ogni nodo vv e ogni nodo uu nel sottoalbero TvT_v, key(u)≥key(v)\text{key}(u) \ge \text{key}(v) (per transitività lungo il cammino).

Esempio. Nello heap [4,6,9,15,7,16,12][4,6,9,15,7,16,12] la radice 44 è il minimo e il cammino 4,6,154,6,15 è non decrescente.

Formula (level numbering di uno heap su array). Un heap sta in un array P[1..n]P[1..n] numerando i nodi per livelli, da sinistra a destra: la radice è P[1]P[1], i figli di P[i]P[i] sono P[2i]P[2i] e P[2i+1]P[2i+1], il padre di P[i]P[i] è P[⌊i/2⌋]P[\lfloor i/2 \rfloor] e last=n\text{last} = n. I collegamenti padre-figlio sono impliciti (struttura dati implicita): nessun puntatore, spazio Θ(n)\Theta(n). (Nel testo adottato la numerazione parte da 0: figli 2i+12i+1, 2i+22i+2, padre ⌊(i−1)/2⌋\lfloor (i-1)/2 \rfloor.)

Esempio. In P[1..11]P[1..11] i figli di P[5]P[5] sono P[10]P[10] e P[11]P[11], e il padre di P[11]P[11] è P[⌊11/2⌋]=P[5]P[\lfloor11/2\rfloor]=P[5].

Proprietà (insert e up-heap bubbling). Si pone la nuova entry in posizione last+1\text{last} + 1 (la completezza è salva) e si ripristina l'heap-order risalendo.

Esempio. Su P=[4,6,9,15,7,16,12,20,18,8,11]P=[4,6,9,15,7,16,12,20,18,8,11], insert(5) mette 55 in posizione 1212 e lo fa salire fino alla 33: P=[4,6,5,15,7,9,12,20,18,8,11,16]P=[4,6,5,15,7,9,12,20,18,8,11,16].

Proprietà (removeMin e down-heap bubbling). Si toglie la radice, si porta in radice l'ultima entry e si ripristina scendendo, scambiando sempre con il figlio di chiave minore.

Esempio. Su P=[4,6,9,15,7,16,12,20,18,8,11]P=[4,6,9,15,7,16,12,20,18,8,11], removeMin() restituisce 44 e lo heap diventa [6,7,9,15,8,16,12,20,18,11][6,7,9,15,8,16,12,20,18,11].

Proprietà (rimozione di una entry qualsiasi). removeEntry(P, j): si pone P[j]←P[last]P[j] \leftarrow P[\text{last}] e si decrementa last\text{last}. Sia e=(k,v)e = (k, v) l'entry spostata, e0e_0 il nuovo padre, e1,e2e_1, e_2 i figli. Tre casi:

  1. k0≤k≤min⁡(k1,k2)k_0 \le k \le \min(k_1, k_2): l'heap è già valido;
  2. k<k0k < k_0: up-heap bubbling a partire da P[j]P[j];
  3. k>min⁡(k1,k2)k > \min(k_1, k_2): down-heap bubbling a partire da P[j]P[j].

Esempio. Su P=[4,6,9,15,7,16,12,20,18,8,11]P=[4,6,9,15,7,16,12,20,18,8,11], removeEntry(P, 2) porta 1111 in posizione 22 e lo fa scendere (down-heap): P=[4,7,9,15,8,16,12,20,18,11]P=[4,7,9,15,8,16,12,20,18,11].

Costruzione di uno heap

Definizione (costruzione top-down con inserimenti successivi). Si considera P[1]P[1] uno heap di un elemento e si inserisce P[2],P[3],…,P[n]P[2], P[3], \dots, P[n] con l'up-heap bubbling.

Esempio. Con P=[3,2,1]P=[3,2,1]: P[1]=3P[1]=3 è uno heap; insert di 22 lo fa salire sopra 33 ([2,3][2,3]); insert di 11 sale fino alla radice ([1,3,2][1,3,2]).

Formula (costo della costruzione top-down). Costo. L'iterazione jj costa O(log⁡j)O(\log j), quindi O(∑j=2nlog⁡j)=O(nlog⁡n)O\big(\sum_{j=2}^{n} \log j\big) = O(n \log n). È anche Ω(nlog⁡n)\Omega(n \log n): se PP è in ordine decrescente ogni nuova chiave è minore di tutte le precedenti e sale fino alla radice, e

∑j=⌊n/2⌋nlog⁡2j ≥ n2log⁡2⌊n2⌋∈Ω(nlog⁡n).\sum_{j=\lfloor n/2 \rfloor}^{n} \log_2 j \ \ge\ \frac n2 \log_2 \Big\lfloor \frac n2 \Big\rfloor \in \Omega(n \log n).

Quindi Θ(nlog⁡n)\Theta(n \log n) (anche nn insert consecutive in uno heap vuoto, vedi Esercizio 13 · costruzione di uno heap top-down e bottom-up).

Esempio. P=[2,10,27,9,11,13,5]P = [2, 10, 27, 9, 11, 13, 5]: dopo j=2,3j = 2,3: [2,10,27][2, 10, 27]; j=4j = 4 (99 sale sopra 1010): [2,9,27,10][2, 9, 27, 10]; j=5j = 5: [2,9,27,10,11][2, 9, 27, 10, 11]; j=6j = 6 (1313 sopra 2727): [2,9,13,10,11,27][2, 9, 13, 10, 11, 27]; j=7j = 7 (55 sopra 1313, poi 5>25 > 2 stop): [2,9,5,10,11,27,13][2, 9, 5, 10, 11, 27, 13]. Gli scambi sono stati 0,0,1,0,1,10,0,1,0,1,1, in tutto 33.

Definizione (costruzione bottom-up). Le posizioni ⌊n/2⌋+1,…,n\lfloor n/2 \rfloor + 1, \dots, n sono foglie e quindi già heap da un elemento. Si procede a ritroso sui nodi interni, "sistemando" ciascuno con il down-heap bubbling: ogni nodo ha come sottoalberi due heap già costruiti.

Esempio. Con n=6n=6 si ha ⌊n/2⌋=3\lfloor n/2\rfloor=3: P[4..6]P[4..6] sono foglie e si sistemano j=3,2,1j=3,2,1 con il down-heap bubbling.

Formula (costo della costruzione bottom-up). Costo. Sia h=⌊log⁡2n⌋h = \lfloor \log_2 n \rfloor. Il down-heap da un nodo al livello ii (0≤i≤h−10 \le i \le h-1) costa O(h−i)O(h - i) (al più la distanza dalle foglie), e al livello ii ci sono al più 2i2^i nodi. Il costo totale è

O(∑i=0h−12i(h−i))=O(2h∑i=0h−1h−i2h−i)=O(2h∑ℓ=1hℓ2ℓ).O\Big( \sum_{i=0}^{h-1} 2^i (h - i) \Big) = O\Big( 2^h \sum_{i=0}^{h-1} \frac{h - i}{2^{h-i}} \Big) = O\Big( 2^h \sum_{\ell=1}^{h} \frac{\ell}{2^\ell} \Big).

Si dimostra per induzione che ∑ℓ=1hℓ2ℓ=2−h+22h<2\sum_{\ell=1}^{h} \frac{\ell}{2^\ell} = 2 - \frac{h+2}{2^h} < 2: per h=1h = 1 è 12=2−32\frac12 = 2 - \frac32; passando da hh a h+1h+1 si somma h+12h+1\frac{h+1}{2^{h+1}} e si ottiene 2−2(h+2)−(h+1)2h+1=2−h+32h+12 - \frac{2(h+2) - (h+1)}{2^{h+1}} = 2 - \frac{h+3}{2^{h+1}}. (Nel corso si dimostra la limitazione più debole ∑ℓ(1/2)ℓ<3\sum \ell (1/2)^\ell < 3, che basta.) Dunque il costo è O(2h)=O(n)O(2^h) = O(n), perché 2h≤n2^h \le n.

Esempio. P=[2,10,27,9,11,13,5,30,18,1,14,15,6]P = [2, 10, 27, 9, 11, 13, 5, 30, 18, 1, 14, 15, 6] (n=13n = 13, ⌊n/2⌋=6\lfloor n/2 \rfloor = 6):

Grafico interattivo: Scambi al caso peggiore nella costruzione di uno heap con n elementi: top-down su array decrescente, (n+1)·⌊log₂n⌋ − 2^(⌊log₂n⌋+1) + 2 (34 per n = 15, 258 per n = 63, 1538 per n = 255), bottom-up al più n − 1 (somma delle altezze dei nodi: 11, 57 e 247 se n = 2^k − 1)

Definizione (heap sort). Costruzione bottom-up Θ(n)\Theta(n) e poi nn removeMin da Θ(log⁡n)\Theta(\log n): Θ(nlog⁡n)\Theta(n \log n) (vedi Algoritmi di ordinamentoSelection sort e insertion sort (quadratici, con invarianti), merge sort (divide et impera, Theta(n log n)), quick sort (caso pessimo quadratico, medio n log n), heap sort (in loco, Theta(n log n)), ordinamento senza confronti per chiavi intere in un intervallo piccolo, limite inferiore Omega(n log n) per gli algoritmi basati su confronti.Algoritmi di ordinamento →).

Esempio. Con n=1000n=1000 chiavi: costruzione bottom-up Θ(n)\Theta(n) e poi 10001000 removeMin, ciascuno con circa log⁡21000≈10\log_21000\approx10 scambi al più.

VI. Mappe e tabelle hash

Mappe e dizionari

Definizione (ADT mappa). Mappa: entry con chiavi distinte provenienti da un dominio su cui è definito =; concetto analogo a un indice (associative array: la chiave è l'indice di accesso).

Metodo Effetto
get(k) se esiste (k,x)(k, x) restituisce xx, altrimenti null
put(k, v) se esiste (k,x)(k, x) lo sostituisce con vv e restituisce xx; altrimenti inserisce (k,v)(k, v) e restituisce null
remove(k) se esiste (k,x)(k, x) lo toglie e restituisce xx, altrimenti null
keySet(), values(), entrySet() iteratori sulle chiavi, sui valori, sulle entry

Esempio. Su una mappa vuota: put("a", 1) restituisce null, poi put("a", 2) restituisce 1, get("a") restituisce 2 e remove("a") restituisce 2.

Definizione (dizionario o multimap). Ammette più entry con la stessa chiave. Metodi caratteristici:

  • get(k): restituisce una collezione (anche vuota) con tutti i valori associati a kk;
  • put(k, x): inserisce sempre una nuova entry (k,x)(k, x), senza toccare le altre con la stessa chiave; non restituisce nulla;
  • remove(k, v): toglie una entry con chiave kk e valore vv, se esiste; non restituisce nulla.

Esempio. Dopo put(k, 1) e put(k, 2), get(k) restituisce la collezione {1,2}\{1,2\}; remove(k, 1) lascia {2}\{2\}.

Tabelle hash

Definizione (tabella hash). La tabella hash è un'implementazione efficiente della mappa (vedi Mappe e dizionariADT mappa (chiavi distinte) con get, put, remove, keySet/values/entrySet; famiglia mappa/mappa ordinata/dizionario (multimappa); applicazioni; implementazioni semplici ma poco efficienti (lista, array indicizzato dalle chiavi); dizionario realizzato con una mappa di liste.Mappe e dizionari →) con tempo medio costante. Ha tre componenti:

  1. una funzione hash h:U→[0,N−1]h : U \to [0, N-1] (UU universo delle chiavi), composta da hash code k↦Zk \mapsto \mathbb{Z} e compression function Z→[0,N−1]\mathbb{Z} \to [0, N-1];
  2. un bucket array AA di capacità NN: A[i]A[i] contiene le entry ⟨k,v⟩\langle k, v \rangle con h(k)=ih(k) = i;
  3. un metodo di gestione delle collisioni: ⟨k1,v1⟩,⟨k2,v2⟩\langle k_1, v_1 \rangle, \langle k_2, v_2 \rangle con k1≠k2k_1 \ne k_2 e h(k1)=h(k2)h(k_1) = h(k_2).

Esempio. Con N=11N=11 e h(k)=(3k+5) mod 11h(k)=(3k+5)\bmod11 la chiave 1212 va nel bucket 41 mod 11=841\bmod11=8: 3⋅12+5=413\cdot12+5=41 e 41=3⋅11+841=3\cdot11+8, quindi il resto è 88.

Definizione (hash code per stringhe)

  • Stringhe S=s0s1…sk−1S = s_0 s_1 \dots s_{k-1}:
    • somma dei caratteri ∑si\sum s_i: non è buona: stop, spot, tops hanno lo stesso hash code (le permutazioni collidono);
    • polinomiale: ∑i=0k−1si ak−1−i\sum_{i=0}^{k-1} s_i \, a^{k-1-i} (con la regola di Horner); per parole inglesi vanno bene a=31,33,37,39,41a = 31, 33, 37, 39, 41 (a=31a = 31 nella classe String di Java);
    • cyclic shift: si somma ogni carattere dopo uno shift ciclico di 55 posizioni della somma parziale: h <- s0; for i <- 1 to k-1: h <- (h << 5) | (h >> 27); h <- h + s_i.

Esempio. Con a=31a=31, "ab" ha hash code polinomiale 97⋅31+98=310597\cdot31+98=3105 e "ba" ha 98⋅31+97=313598\cdot31+97=3135, mentre con la somma dei caratteri entrambe valgono 195195.

Definizione (compression function: divisione e MAD)

  • Metodo della divisione: i↦i mod Ni \mapsto i \bmod N. Se NN non è primo le correlazioni tra gli hash code tendono a conservarsi; ad esempio con N=2pN = 2^p conta solo gli ultimi pp bit e con N=10pN = 10^p solo le ultime pp cifre decimali. In pratica si sceglie NN primo e lontano da potenze di 22.
  • MAD (multiply-add-divide): i↦[(ai+b) mod p] mod Ni \mapsto [(a i + b) \bmod p] \bmod N con p>Np > N primo e a,b∈[0,p−1]a, b \in [0, p-1] scelti a caso, a>0a > 0.

Esempio. Con N=11N=11 l'hash code 137137 dà 137 mod 11=5137\bmod11=5; con MAD (a=3a=3, b=5b=5, p=13p=13) l'hash code 77 dà [(3⋅7+5) mod 13] mod 11=0[(3\cdot7+5)\bmod13]\bmod11=0.

Definizione (load factor e separate chaining). Ogni bucket è una mappa più piccola, realizzata con una lista. Il load factor è λ=n/N\lambda = n/N (nn entry, NN bucket): la lunghezza media di un bucket.

Esempio. Con n=9n=9 entry e N=11N=11 bucket: λ=9/11≈0,82\lambda=9/11\approx0{,}82. Perché λ\lambda è la lunghezza media: con nn entry distribuite sui NN bucket in modo uniforme, il numero di entry nel bucket ii ha Valore attesoIl valore atteso E[X] = Σ x p_X(x) è la media dei valori di X pesata con le loro probabilità (esiste se la serie converge assolutamente); per una funzione g vale E[g(X)] = Σ g(x) p_X(x) senza trovare la legge di g(X), ed E è lineare: E[aX + bY + c] = aE[X] + bE[Y] + c.Valore atteso → n⋅1N=λn\cdot\frac1N=\lambda (ogni entry cade nel bucket ii con probabilità 1/N1/N, e il valore atteso di una somma è la somma dei valori attesi).

Proprietà (costo di una tabella hash: caso pessimo e medio). Per una tabella con nn entry get, put, remove costano Θ(n)\Theta(n), dominate dalla ricerca della chiave nel bucket: O(n)O(n) banale; Ω(n)\Omega(n) con tutte le entry nello stesso bucket e chiave cercata assente.

Caso medio. Sotto l'ipotesi di uniform hashing la complessità media è O(1+λ)O(1 + \lambda), nel senso: ricerca senza successo, media su tutti i valori di h(k)h(k) equiprobabili; ricerca con successo, media su tutte le chiavi presenti equiprobabili inserite una dopo l'altra senza cancellazioni. In pratica si impone λ<0,9\lambda < 0{,}9. Il load factor è il compromesso tra spazio e velocità: λ\lambda piccolo = tempo basso e spazio elevato; λ\lambda grande = tempo elevato e spazio ridotto. La qualità di hh determina quanto ci si avvicina all'ipotesi ideale.

Esempio. Con λ≈0,82\lambda\approx0{,}82 una ricerca senza successo costa in media circa 1+λ≈1,81+\lambda\approx1{,}8 confronti, mentre nel caso pessimo, con tutte le entry in un bucket, costa nn.

Grafico interattivo: Confronti medi per una ricerca in una tabella hash con separate chaining in funzione del load factor: senza successo 1 + λ, con successo circa 1 + λ/2; a λ = 0,75 (soglia di rehashing) sono 1,75 e 1,375

Definizione (rehashing). Quando λ\lambda supera una soglia (ad esempio λ>0,75\lambda > 0{,}75):

  1. si crea un nuovo bucket array di capacità N′≥2NN' \ge 2N (se serve, si cerca un primo ≥2N\ge 2N);
  2. si sceglie una nuova funzione hash;
  3. si reinseriscono tutte le entry.

Esempio. Con N=11N=11 e soglia 0,750{,}75 il nono inserimento (9/11>0,759/11>0{,}75) causa il rehashing verso N′≥22N'\ge22, per esempio il primo 2323.

VII. Alberi di ricerca

Alberi binari di ricerca

Definizione (albero binario di ricerca). Un albero binario di ricerca (ABR, binary search tree) implementa una mappa ordinata (vedi Mappe e dizionariADT mappa (chiavi distinte) con get, put, remove, keySet/values/entrySet; famiglia mappa/mappa ordinata/dizionario (multimappa); applicazioni; implementazioni semplici ma poco efficienti (lista, array indicizzato dalle chiavi); dizionario realizzato con una mappa di liste.Mappe e dizionari →) su chiavi di un universo ordinato (Campi ordinati (Q e R)Gli assiomi di somma, prodotto e ordine comuni a Q e R, da cui derivano le regole di equazioni e disequazioni.Campi ordinati (Q e R) →). È l'analogo ad albero della ricerca binaria su array (Ricerca lineare e binariaRicerca lineare su sequenze qualsiasi in O(n); ricerca binaria su sequenze ordinate in O(log n), versione iterativa e ricorsiva, invariante e errori di indice; modulo bisect.Ricerca lineare e binaria →): a ogni confronto si scarta un intero sottoalbero come là si scartava metà intervallo, con il vantaggio che inserire e togliere non richiede di spostare gli elementi. È un albero binario proprio (vedi Alberi binariAlbero binario e albero binario proprio; interfaccia; relazioni tra nodi, foglie e altezza (m = n-m+1, h+1 <= m <= 2^h, 2h+1 <= n <= 2^(h+1)-1) con dimostrazioni; visita inorder; parse tree e valutazione di espressioni; heightSum come esempio di calcolo di un'informazione più ricca.Alberi binari →) i cui nodi interni memorizzano entry e le cui foglie sono vuote (sentinelle esterne), tale che per ogni nodo interno vv con chiave kk:

  • le chiavi del sottoalbero sinistro di vv sono <k< k;
  • le chiavi del sottoalbero destro di vv sono >k> k.

Esempio. Inserendo 30,40,2430,40,24 in un ABR vuoto si ha radice 3030, figlio destro 4040 e figlio sinistro 2424; l'inorder è 24,30,4024,30,40.

Definizione (get, put e remove su un ABR)

  • get(k): w←w \leftarrow TreeSearch(k, root); se ww è esterno restituisce null, altrimenti il valore di ww. Θ(h)\Theta(h).

  • put(k, x): w←w \leftarrow TreeSearch. Se ww è interno si sostituisce il valore e si restituisce il vecchio; altrimenti ww è una foglia e si espande: diventa nodo interno con la entry (k,x)(k, x) e due nuove foglie come figli; si incrementa il numero di entry e si restituisce null. Θ(h)\Theta(h).

  • remove(k): w←w \leftarrow TreeSearch; se è esterno restituisce null. Altrimenti due casi:

    1. ww ha almeno un figlio foglia: si elimina ww e la foglia e si collega l'altro figlio uu al padre di ww (se ww era la radice, uu diventa radice);
    2. ww ha due figli interni: si prende yy = predecessore inorder di ww (il nodo più a destra del sottoalbero sinistro: entry con la chiave immediatamente minore) e si copia la sua entry in ww; yy non ha figlio destro interno, quindi si elimina yy con il caso 1. L'albero resta di ricerca: la nuova chiave in ww è maggiore di tutte le chiavi a sinistra e minore di quelle a destra.

    Costo: Θ(h)\Theta(h) per la ricerca, O(h)O(h) per trovare il predecessore, O(1)O(1) per aggiornare i nodi: Θ(h)\Theta(h).

Esempio. In un ABR con inorder 11,13,24,30,40,48,5811,13,24,30,40,48,58, remove(30) con due figli interni sostituisce la entry con il predecessore inorder, cioè 2424.

Proprietà (altezza e costo delle operazioni su un ABR). Le tre operazioni costano Θ(h)\Theta(h) e hh può arrivare a n−1n - 1 (chiavi inserite in ordine crescente o decrescente: l'albero degenera in una lista), per cui al caso pessimo sono Θ(n)\Theta(n); in un albero bilanciato h=Θ(log⁡n)h = \Theta(\log n). Per ottenere Θ(log⁡n)\Theta(\log n) garantito: alberi che si ristrutturano (AVL, rosso-neri) oppure nodi più capienti, gli alberi (2,4) (vedi Multi-way search tree e alberi (2,4)Multi-way search tree (nodi con più entry, d figli e d-1 chiavi ordinate), ricerca in O(d_max h), un MWS tree con n entry ha n+1 foglie; albero (2,4): nodi con 2-4 figli e tutte le foglie alla stessa profondità, altezza Theta(log n) con dimostrazione, operazioni in Theta(log n); idea di overflow e underflow; tabella di confronto con gli alberi binari di ricerca.Multi-way search tree e alberi (2,4) →).

Esempio. Inserendo 1,2,3,4,51,2,3,4,5 in ordine crescente l'albero diventa un cammino: h=n−1=4h=n-1=4 e ogni operazione costa Θ(n)\Theta(n). Il grafico confronta l'altezza dell'albero degenere con quella minima possibile (albero completo, ⌊log⁡2n⌋\lfloor\log_2 n\rfloor, vedi HeapAlbero binario completo e sua altezza floor(log2 n); heap = albero completo con heap-order property; proprietà (radice minima, cammini non decrescenti); rappresentazione su array con level numbering; insert con up-heap bubbling, removeMin con down-heap bubbling, rimozione di una entry qualsiasi; invarianti e costi Theta(log n); esempio svolto.Heap →): già con n=1000n=1000 chiavi sono 999999 contro 99 passi di ricerca.

Grafico interattivo: Altezza di un albero binario di ricerca con n chiavi: da floor(log2 n) (albero bilanciato) a n − 1 (chiavi inserite in ordine); per n = 64 sono 6 contro 63

Multi-way search tree e alberi (2,4)

Definizione (multi-way search tree). Un multi-way search tree (MWS tree) è un albero ordinato (vedi AlberiAlbero radicato (definizione per padre e ricorsiva), terminologia (antenati, discendenti, nodi interni ed esterni, sottoalbero, albero ordinato), profondità, livello, altezza; altezza = massima profondità delle foglie; algoritmi depth e height con costo; somma dei figli = n-1; esempio di algoritmo Omega(n^2) (heightBad).Alberi →) in cui:

  • ogni nodo interno ha ≥2\ge 2 figli;
  • ogni nodo interno con d≥2d \ge 2 figli v1,…,vdv_1, \dots, v_d (dd-nodo) memorizza d−1d - 1 entry (k1,x1),…,(kd−1,xd−1)(k_1, x_1), \dots, (k_{d-1}, x_{d-1}) con k1<k2<⋯<kd−1k_1 < k_2 < \dots < k_{d-1};
  • per 1≤i≤d1 \le i \le d ogni chiave kk del sottoalbero TviT_{v_i} soddisfa ki−1<k<kik_{i-1} < k < k_i (con k0=−∞k_0 = -\infty, kd=+∞k_d = +\infty).

Esempio. Un 33-nodo con chiavi 10<2010<20 ha 33 figli, con chiavi <10<10, in (10,20)(10,20) e >20>20. Il caso d=2d=2 è il nodo dell'ABR (Alberi binari di ricercaAlbero binario di ricerca come albero binario proprio con entry nei nodi interni e foglie vuote; inorder crescente; TreeSearch; get, put e remove (due casi, con predecessore inorder) in Theta(h); altezza fino a n-1; esempi di inserimenti; alberi aumentati con campi size e max e algoritmi di conteggio e interrogazione in O(h).Alberi binari di ricerca →): una chiave e due sottoalberi, uno con chiavi minori e uno con chiavi maggiori. Gli intervalli aperti (ki−1,ki)(k_{i-1},k_i) sono quelli dei intervalli di numeri realiNaturali, interi, razionali e reali, perché la radice di 2 obbliga a passare da Q a R, e gli intervalli.Insiemi numerici (N, Z, Q, R) →: gli dd intervalli (−∞,k1),(k1,k2),…,(kd−1,+∞)(-\infty,k_1),(k_1,k_2),\dots,(k_{d-1},+\infty) coprono tutta la retta tranne le chiavi del nodo, quindi ogni chiave cercata ha esattamente un posto dove andare.

Proposizione (numero di foglie di un MWS tree). Un MWS tree con nn entry ha n+1n + 1 foglie.

Esempio. Con radice (10,20)(10,20) e figli (3,7)(3,7), (14,17)(14,17), (25,30,35)(25,30,35) le entry sono 99 e le foglie 3+3+4=10=9+13+3+4=10=9+1.

Definizione (albero (2,4) e altezza). Un albero (2,4) è un MWS tree tale che:

  1. ogni nodo interno è un dd-nodo con 2≤d≤42 \le d \le 4 (cioè ha 11, 22 o 33 entry);
  2. tutte le foglie hanno la stessa profondità.

Proposizione. Un (2,4) con n>0n > 0 entry ha altezza Θ(log⁡n)\Theta(\log n).

Esempio. Con n=1000n=1000 entry l'altezza è tra log⁡41001≈5\log_4 1001\approx5 e log⁡21001≈10\log_2 1001\approx10, cioè Θ(log⁡n)\Theta(\log n).

Grafico interattivo: Altezza di un albero (2,4) con n entry: compresa tra metà di log2(n+1) (tutti 4-nodi) e log2(n+1) (tutti 2-nodi); per n = 1000 tra 5 e 10

VIII. Alberi e heap in C

Alberi e heap in C

Definizione (albero binario in C come puntatore alla radice). L'albero è il puntatore alla radice (NULL = albero vuoto). Gli algoritmi ricorsivi seguono lo schema delle visite: caso base r == NULL, poi i due sottoalberi.

Esempio. Con Nodo *r = nuovoNodo(5); r->left = nuovoNodo(3); si ha contaNodi(r) == 2, altezza(r) == 1 e inorder(r) stampa 3 5.

Definizione (inserimento in un albero binario di ricerca con doppio puntatore). La radice può cambiare (primo inserimento), quindi si passa Nodo **:

void addBST(Nodo **r, int v) {
    if (*r == NULL)
        *r = nuovoNodo(v);
    else if (v < (*r)->val)
        addBST(&(*r)->left, v);
    else if (v > (*r)->val)
        addBST(&(*r)->right, v);          /* chiave uguale: ignorata */
}

Esempio. Con addBST(&r, 30); addBST(&r, 40); addBST(&r, 24); la visita inorder stampa 24 30 40. Passo per passo: r vale NULL, quindi addBST(&r, 30) crea la radice 3030 (e la scrive in r: per questo serve Nodo **, come nelle liste, Liste concatenate in CLista singolarmente concatenata in C con nodo struct e testa passata per riferimento (Nodo **); addHead, addTail ricorsiva e iterativa, pop, inversione in loco, liberazione; coda con puntatori a testa e coda; ricorsione sulle liste; costi.Liste concatenate in C →). Per 4040: *r non è NULL, 40>3040>30, si chiama addBST(&(*r)->right, 40), dove *r ora è il campo right della radice, ancora NULL: si crea lì il nodo. Per 2424: 24<3024<30, si scende a sinistra e si crea il nodo nel campo left. È la ricerca di TreeSearch (Alberi binari di ricercaAlbero binario di ricerca come albero binario proprio con entry nei nodi interni e foglie vuote; inorder crescente; TreeSearch; get, put e remove (due casi, con predecessore inorder) in Theta(h); altezza fino a n-1; esempi di inserimenti; alberi aumentati con campi size e max e algoritmi di conteggio e interrogazione in O(h).Alberi binari di ricerca →) con l'espansione della foglia, dove la "foglia" è un campo NULL.

Definizione (min-heap su array con indici da 1). Un min-heap (HeapAlbero binario completo e sua altezza floor(log2 n); heap = albero completo con heap-order property; proprietà (radice minima, cammini non decrescenti); rappresentazione su array con level numbering; insert con up-heap bubbling, removeMin con down-heap bubbling, rimozione di una entry qualsiasi; invarianti e costi Theta(log n); esempio svolto.Heap →) è un array h con indici da 1 (h[0] non si usa): figli di i in 2i e 2i+1, padre in i/2. last è il numero di entry.

Proprietà (costi di insert, removeMin e bottomUp in C). insert e removeMin costano Θ(log⁡n)\Theta(\log n), bottomUp Θ(n)\Theta(n) (vedi Costruzione di uno heapCostruire uno heap da un array di n entry: approccio top-down con n-1 insert, Theta(n log n); approccio bottom-up con down-heap dalle foglie verso la radice, Theta(n), con dimostrazione della somma; esempi svolti, in loco; unione di due alberi con heap-order.Costruzione di uno heap →). Prova: da 11 9 7 13 3 4, bottomUp produce 3 9 4 13 11 7; dopo insert(2) i due removeMin restituiscono 22 e 33, e resta 4 9 7 13 11.

Esempio. Da 11 9 7 13 3 4, bottomUp produce 3 9 4 13 11 7; dopo insert(2), due removeMin restituiscono 2 e 3 e resta 4 9 7 13 11.

IX. Algoritmi di ordinamento

Algoritmi di ordinamento

Proprietà (selection sort: invariante e costo). Invariante all'inizio dell'iterazione ii: S[0..i−1]S[0..i-1] contiene gli ii elementi minori, già in ordine crescente (e ciascuno ≤\le tutti gli elementi di S[i..n−1]S[i..n-1]). Alla fine i=n−1i = n-1 e tutto è ordinato. Costo: il ciclo interno fa n−1−in - 1 - i confronti, in totale ∑i=0n−2(n−1−i)=n(n−1)2∈Θ(n2)\sum_{i=0}^{n-2}(n-1-i) = \frac{n(n-1)}{2} \in \Theta(n^2) per ogni istanza, anche già ordinata. Corrisponde alla coda con priorità su lista non ordinata (vedi Code con prioritàEntry chiave-valore; ADT coda con priorità (insert, min, removeMin) con chiave minima = priorità massima; esempio di esecuzione; applicazioni; implementazioni con lista non ordinata e ordinata e relativi costi; ordinamento tramite coda con priorità.Code con priorità →).

Esempio. Con n=5n=5 il selection sort fa 5⋅4/2=105\cdot4/2=10 confronti su qualunque array, anche già ordinato.

Proprietà (insertion sort: caso pessimo e caso migliore). Si mantiene ordinato un prefisso e si inserisce S[i]S[i] al posto giusto spostando a destra i maggiori (pseudocodice e analisi in Complessità in tempo e caso pessimoPerché lo studio sperimentale non basta; complessità al caso pessimo come massimo sul numero di operazioni tra le istanze di una data taglia; stima con limiti superiore e inferiore senza trovare l'istanza peggiore; esempi arrayMax, prefixAverages e InsertionSort; efficienza asintotica e limiti dell'analisi.Complessità in tempo e caso pessimo →). Invariante: all'inizio dell'iterazione ii, S[0..i−1]S[0..i-1] è ordinato (e contiene gli elementi originali di S[0..i−1]S[0..i-1]). Caso pessimo Θ(n2)\Theta(n^2) (array decrescente), caso migliore Θ(n)\Theta(n) (array ordinato), veloce su sequenze quasi ordinate.

Esempio. Con n=5n=5 servono 44 confronti su [1,2,3,4,5][1,2,3,4,5] e 1010 su [5,4,3,2,1][5,4,3,2,1]. Traccia su S=[5,2,9,1,6]S=[5,2,9,1,6]:

Formula (costo del merge sort). merge(L, R) confronta le due teste e sposta la minore nell'output: Θ(∣L∣+∣R∣)\Theta(\lvert L \rvert + \lvert R \rvert). Costo: l'albero della ricorsione ha ⌈log⁡2n⌉+1\lceil \log_2 n \rceil + 1 livelli e al livello ii le 2i2^i chiamate lavorano su sequenze di taglia ≈n/2i\approx n/2^i, con costo totale per livello Θ(n)\Theta(n) (il costo di un nodo è la fusione): Θ(nlog⁡n)\Theta(n \log n) per ogni istanza. Richiede Θ(n)\Theta(n) di spazio ausiliario. È stabile se in caso di parità si prende l'elemento di sinistra (verificato: per n=1024n = 1024 già ordinato i confronti sono 51205120).

Esempio. Per n=8n=8 l'albero ha log⁡28+1=4\log_28+1=4 livelli e nel caso pessimo i confronti sono nlog⁡2n−n+1=17n\log_2n-n+1=17.

Proprietà (costo del quick sort). S[lo..i−1]<p≤S[i..j−1]S[lo..i-1] < p \le S[i..j-1]. Costo: Θ(n)\Theta(n) per partizione. Se il pivot divide in parti bilanciate l'albero ha altezza Θ(log⁡n)\Theta(\log n) e il costo è Θ(nlog⁡n)\Theta(n \log n); se è sempre il minimo o il massimo (array già ordinato con pivot ultimo) il costo è ∑(n−i)=Θ(n2)\sum (n - i) = \Theta(n^2). Con pivot scelto a caso il tempo atteso è O(nlog⁡n)O(n \log n): è l'esempio classico di caso pessimo patologico (vedi Complessità in tempo e caso pessimoPerché lo studio sperimentale non basta; complessità al caso pessimo come massimo sul numero di operazioni tra le istanze di una data taglia; stima con limiti superiore e inferiore senza trovare l'istanza peggiore; esempi arrayMax, prefixAverages e InsertionSort; efficienza asintotica e limiti dell'analisi.Complessità in tempo e caso pessimo →). Spazio ausiliario O(log⁡n)O(\log n) attesi (stack).

Esempio. Con pivot ultimo su un array già ordinato di n=6n=6 elementi i confronti sono 5+4+3+2+1=155+4+3+2+1=15: Θ(n2)\Theta(n^2). Il pivot è il massimo: tutti gli altri n−1n-1 elementi vanno a sinistra, la parte destra è vuota e il problema scende solo di 11, quindi T(n)=T(n−1)+n−1T(n)=T(n-1)+n-1, che dà ∑k=1n−1k=n(n−1)2\sum_{k=1}^{n-1}k=\frac{n(n-1)}{2}.

Definizione (heap sort). Si costruisce uno heap sull'array e si estrae ripetutamente il massimo (heap sort con max-heap) mettendolo in fondo all'array: in loco.

  1. bottom-up heap-construction: Θ(n)\Theta(n) (vedi Costruzione di uno heapCostruire uno heap da un array di n entry: approccio top-down con n-1 insert, Theta(n log n); approccio bottom-up con down-heap dalle foglie verso la radice, Theta(n), con dimostrazione della somma; esempi svolti, in loco; unione di due alberi con heap-order.Costruzione di uno heap →);
  2. per end=n−1\text{end} = n-1 fino a 11: scambia S[0]S[0] con S[end]S[\text{end}] e fai down-heap su S[0..end−1]S[0..\text{end}-1]: n−1n-1 passi da O(log⁡n)O(\log n).

Esempio. Per n=1024n=1024: costruzione bottom-up Θ(n)\Theta(n) e 10231023 down-heap da al più log⁡21024=10\log_21024=10 passi, circa 10410^4 operazioni. Perché l'ordine è crescente con un max-heap: il massimo in radice viene scambiato con l'ultima posizione utile dell'array, che è la sua posizione definitiva; poi la parte ancora da sistemare si accorcia di 11 e un down-heap ripristina l'heap (HeapAlbero binario completo e sua altezza floor(log2 n); heap = albero completo con heap-order property; proprietà (radice minima, cammini non decrescenti); rappresentazione su array con level numbering; insert con up-heap bubbling, removeMin con down-heap bubbling, rimozione di una entry qualsiasi; invarianti e costi Theta(log n); esempio svolto.Heap →). L'array è così riempito dal fondo, dal massimo al minimo, e a fine algoritmo risulta crescente senza memoria aggiuntiva.

Formula (ordinamento per conteggio). Se le chiavi sono interi in [0,K][0, K] si può ordinare (o contare) in Θ(n+K)\Theta(n + K) con un array di contatori B[0..K]B[0..K]: una scansione che fa B[S[i]]←B[S[i]]+1B[S[i]] \leftarrow B[S[i]] + 1 e una scansione di BB. Non usa confronti, quindi non è soggetto al limite sotto. Con K=O(n)K = O(n) è lineare (si usa nell'esercizio della mediana di nn chiavi in [0,10n][0, 10n]). L'array dei contatori è una mappa con chiavi intere (Mappe e dizionariADT mappa (chiavi distinte) con get, put, remove, keySet/values/entrySet; famiglia mappa/mappa ordinata/dizionario (multimappa); applicazioni; implementazioni semplici ma poco efficienti (lista, array indicizzato dalle chiavi); dizionario realizzato con una mappa di liste.Mappe e dizionari →, array di taglia ∣U∣|U|) e il costo Θ(n+K)\Theta(n+K) è la somma dei due passaggi: nn incrementi e una scansione di K+1K+1 contatori.

Esempio. Con S=[3,1,3,0]S=[3,1,3,0] e K=3K=3 si ottiene B=[1,1,0,2]B=[1,1,0,2] e quindi l'ordine 0,1,3,30,1,3,3: B[0]=1B[0]=1 (00 compare una volta), B[1]=1B[1]=1, B[2]=0B[2]=0, B[3]=2B[3]=2; leggendo BB da sinistra si scrive 00 una volta, 11 una volta, nessun 22, 33 due volte.

Teorema (limite inferiore per l'ordinamento per confronti). Ogni algoritmo di ordinamento basato su confronti richiede Ω(nlog⁡n)\Omega(n \log n) confronti al caso pessimo.

Esempio. Per n=3n=3 ci sono 3!=63!=6 permutazioni, quindi servono almeno ⌈log⁡26⌉=3\lceil\log_26\rceil=3 confronti; per n=1024n=1024 almeno log⁡21024!≈8769\log_21024!\approx8769.

Grafico interattivo: Limite inferiore log2(n!) per l'ordinamento per confronti (Stirling) e confronti del merge sort al caso pessimo n·log2 n − n + 1: a n = 64 sono 296 contro 321, quindi il merge sort è quasi ottimo

X. Grafi: definizioni e visite

Grafi - definizioni e proprietà

Definizione (grafo). Un grafo G=(V,E)G = (V, E) ha un insieme VV di vertici (o nodi) e una collezione EE di archi (coppie di vertici). È diretto se ogni arco (u,v)(u, v) è una coppia ordinata (u→vu \to v), non diretto se la coppia è non ordinata (u−vu - v). EE è una collezione e non un insieme (Insiemi e insieme delle partiCome si descrive un insieme, sottoinsiemi, uguaglianza, insieme delle parti e insiemi finiti e infiniti.Insiemi e insieme delle parti →, Operazioni tra insiemiUnione, intersezione, differenza, complementare e prodotto cartesiano, con le leggi di De Morgan e il legame con la logica.Operazioni tra insiemi →) perché si ammettono archi multipli tra due vertici; un self loop è un arco (u,u)(u, u). Un grafo semplice non ha archi multipli né self loop; un grafo pesato associa pesi a archi e/o vertici.

Esempio. Il grafo non diretto con V={1,2,3}V=\{1,2,3\} e E={(1,2),(2,3)}E=\{(1,2),(2,3)\} ha n=3n=3 vertici e m=2m=2 archi.

Definizione (grado, cammino e componenti connesse)

  • Un arco e=(u,v)e = (u, v) è incidente su uu e vv, che sono adiacenti; i vicini di vv sono i vertici uu con (v,u)∈E(v, u) \in E.
  • Il grado degree(v)\text{degree}(v) è il numero di archi incidenti su vv. Un vertice di grado 00 è isolato.
  • Un cammino è una sequenza di vertici u1,…,uku_1, \dots, u_k con (ui,ui+1)∈E(u_i, u_{i+1}) \in E; la lunghezza è il numero di archi, k−1k - 1 (nei grafi pesati, la somma dei pesi). Un ciclo è un cammino con uk=u1u_k = u_1; è semplice se i vertici sono tutti distinti (a parte primo e ultimo).
  • Un sottografo G′=(V′,E′)G' = (V', E') ha V′⊆VV' \subseteq V, E′⊆EE' \subseteq E e gli archi di E′E' incidono solo su V′V'. È di copertura (spanning) se V′=VV' = V.
  • GG è connesso se per ogni coppia u,vu, v esiste un cammino da uu a vv; altrimenti è disconnesso.
  • Le componenti connesse sono una partizione di GG in sottografi G1,…,GkG_1, \dots, G_k (Gi=(Vi,Ei)G_i = (V_i, E_i)) con: ogni GiG_i connesso; V=V1∪⋯∪VkV = V_1 \cup \dots \cup V_k e E=E1∪⋯∪EkE = E_1 \cup \dots \cup E_k (partizioni); nessun arco tra ViV_i e VjV_j per i≠ji \ne j. Sono i sottografi connessi massimali; la partizione è unica; GG connesso ⇔\Leftrightarrow k=1k = 1. Esempio: vertici u,v,w,x,y,zu, v, w, x, y, z con archi uvuv, xyxy, ywyw, wxwx hanno tre componenti: {u,v}\{u, v\} con 1 arco, {x,y,w}\{x, y, w\} con 3 archi, {z}\{z\} senza archi. Gradi: u,vu,v hanno grado 11; x,y,wx,y,w hanno grado 22 ciascuno (ognuno è su due archi del triangolo x,y,wx,y,w); zz ha grado 00. Somma dei gradi 1+1+2+2+2+0=8=2⋅41+1+2+2+2+0=8=2\cdot4, con m=4m=4 archi.

Esempio. Con V={1,2,3,4}V=\{1,2,3,4\} e archi (1,2),(2,3)(1,2),(2,3): degree(2)=2\text{degree}(2)=2, il vertice 44 è isolato e il grafo ha 22 componenti connesse, {1,2,3}\{1,2,3\} e {4}\{4\}.

Proprietà (gradi, numero di archi e alberi). P1. ∑v∈Vdegree(v)=2m\sum_{v \in V} \text{degree}(v) = 2m. Dimostrazione: ogni arco è contato esattamente due volte, una per estremo (nella somma ∑vdegree(v)\sum_v\text{degree}(v) l'arco (u,v)(u,v) compare una volta nel grado di uu e una volta in quello di vv). Conseguenza: la somma dei gradi è pari, quindi il numero di vertici di grado dispari è pari (se ne avessi un numero dispari la somma sarebbe dispari).

P2. m≤(n2)=n(n−1)2m \le \binom{n}{2} = \frac{n(n-1)}{2}, quindi m∈O(n2)m \in O(n^2). Dimostrazione: GG è semplice, perciò EE è un sottoinsieme delle coppie non ordinate di vertici distinti. Tali coppie sono (n2)=n(n−1)2\binom n2=\frac{n(n-1)}{2}: si scelgono 22 vertici su nn senza badare all'ordine (Fattoriale e coefficienti binomialiFattoriale, permutazioni, disposizioni, combinazioni e coefficiente binomiale n su k, con il triangolo di Tartaglia.Fattoriale e coefficienti binomiali →); equivalentemente, ogni vertice può essere unito a n−1n-1 altri, e ogni coppia è stata contata due volte, da cui il fattore 12\frac12. Con n=5n=5 sono 1010, con n=6n=6 sono 1515 (grafi completi K5K_5, K6K_6).

P3. Se GG è un albero, m=n−1m = n - 1. Dimostrazione: si vede GG come albero radicato; EE coincide con le relazioni padre-figlio, che sono n−1n - 1 (ogni vertice non radice ha un solo padre).

P4. Se GG è connesso, m≥n−1m \ge n - 1. Dimostrazione: finché esiste un ciclo si elimina un suo arco: GG resta connesso, perché ogni cammino che usava quell'arco si può rifare percorrendo il resto del ciclo (il ciclo offre un percorso alternativo tra gli estremi dell'arco). Alla fine è connesso e senza cicli, cioè un albero libero con m′=n−1≤mm' = n - 1 \le m archi.

P5. Se GG è una foresta, m≤n−1m \le n - 1. Dimostrazione: finché GG non è connesso si aggiunge un arco tra due componenti diverse G1,G2G_1, G_2: non può creare cicli, perché un ciclo che passasse per il nuovo arco (a,b)(a,b) richiederebbe un altro cammino da aa a bb, ma aa e bb stanno in componenti diverse e quindi non ce n'è nessuno. Alla fine GG è un albero con m′=n−1≥mm' = n - 1 \ge m archi.

Esempio. Con n=5n=5 vertici si ha m≤5⋅4/2=10m\le5\cdot4/2=10; un albero libero ha m=4m=4 archi e la somma dei gradi è 2m=82m=8.

Grafico interattivo: Numero di archi di un grafo semplice connesso con n vertici: da n − 1 (albero) a n(n−1)/2 (grafo completo); a n = 20 sono 19 contro 190

Corollario (spanning forest). Un grafo con kk componenti connesse di n1,…,nkn_1, \dots, n_k vertici ha spanning forest con ∑(ni−1)=n−k\sum (n_i - 1) = n - k archi (ogni componente contribuisce con uno spanning tree di ni−1n_i - 1 archi, per P3). Esempio d'esame: n1=5n_1 = 5, m1=10m_1 = 10; n2=6n_2 = 6, m2=15m_2 = 15; n3=8n_3 = 8, m3=8m_3 = 8: il numero massimo di archi in una spanning forest è (5−1)+(6−1)+(8−1)=16(5-1) + (6-1) + (8-1) = 16, indipendentemente dai valori mim_i (le prime due componenti sono grafi completi K5K_5 e K6K_6, la terza ha 88 archi su 88 vertici e quindi contiene almeno un ciclo). Ogni spanning forest che non si può più estendere ha esattamente n−kn - k archi.

Esempio. Con componenti di 55, 66 e 88 vertici lo spanning forest ha (5−1)+(6−1)+(8−1)=16=19−3(5-1)+(6-1)+(8-1)=16=19-3 archi.

Rappresentazione dei grafi

Definizione (liste di adiacenza). Per ogni vertice vv una lista I(v)I(v) di puntatori agli archi di LEL_E incidenti su vv (in alternativa, ai vicini; al posto delle liste si possono usare mappe, ad esempio Tabelle hashTabella hash per implementare una mappa: funzione hash = hash code + compression function, bucket array, separate chaining; hash code per numeri e stringhe (polynomial, cyclic shift), division e MAD; load factor, complessità al caso pessimo Theta(n) e medio O(1+lambda), rehashing; esempio svolto con inserimenti e collisioni.Tabelle hash →: con una mappa areAdjacent costerebbe Θ(1)\Theta(1) in media). È la rappresentazione più usata, e quella assunta se non detto altrimenti, perché:

  • occupa spazio lineare nella taglia del grafo, Θ(n+m)\Theta(n + m) (in un grafo non diretto ogni arco compare nelle liste dei suoi due estremi);
  • permette di scorrere i vicini di vv in tempo lineare nel grado di vv.

Esempio. Con archi (1,2),(1,3),(2,3),(3,4)(1,2),(1,3),(2,3),(3,4) e il vertice 55 isolato: I(1)={2,3}I(1)=\{2,3\}, I(3)={1,2,4}I(3)=\{1,2,4\} e I(5)=∅I(5)=\emptyset.

Definizione (matrice di adiacenza). Matrice AA di dimensione n×nn \times n, con righe e colonne in corrispondenza 1-1 con i vertici (che devono essere rappresentati da interi):

A[i1,i2]={puntatore a e=(i1,i2)∈LEse l’arco esistenullaltrimenti.A[i_1, i_2] = \begin{cases} \text{puntatore a } e = (i_1, i_2) \in L_E & \text{se l'arco esiste} \\ \text{null} & \text{altrimenti.} \end{cases}

Dà accesso a un arco in tempo costante, ma occupa Θ(n2)\Theta(n^2), che può essere superlineare nella taglia del grafo: conviene per grafi densi (m=Θ(n2)m = \Theta(n^2)).

Esempio. Con n=5n=5 la matrice ha 2525 celle; A[1,3]A[1,3] punta all'arco (1,3)(1,3) se esiste, altrimenti vale null.

Proprietà (spazio e tempi delle rappresentazioni di un grafo)

Operazione / risorsa Solo LEL_E Liste di adiacenza Matrice di adiacenza
spazio Θ(n+m)\Theta(n + m) Θ(n+m)\Theta(n + m) Θ(n2)\Theta(n^2)
incidentEdges(v) Θ(m)\Theta(m) Θ(degree(v))\Theta(\text{degree}(v)) Θ(n)\Theta(n)
opposite(v, e) Θ(1)\Theta(1) Θ(1)\Theta(1) Θ(1)\Theta(1)
areAdjacent(u, v) Θ(m)\Theta(m) Θ(min⁡(degree(u),degree(v)))\Theta(\min(\text{degree}(u), \text{degree}(v))) Θ(1)\Theta(1)
scansione di tutti i vicini di tutti i vertici Θ(nm)\Theta(nm) Θ(n+m)\Theta(n + m) Θ(n2)\Theta(n^2)

Esempio. Con n=1000n=1000 vertici e m=3000m=3000 archi le liste di adiacenza usano Θ(n+m)=4000\Theta(n+m)=4000 celle (più precisamente n+2m=7000n+2m=7000 riferimenti, perché ogni arco è in due liste), la matrice n2=106n^2=10^6.

Grafico interattivo: Memoria per un grafo con m = 3n archi: liste di adiacenza n + 2m = 7n riferimenti, matrice di adiacenza n² celle; a n = 100 sono 700 contro 10000, e il rapporto cresce con n

Visite di grafi - BFS e DFS

Definizione (visita in ampiezza, BFS). La BFS da ss visita la componente connessa CsC_s di ss, etichetta gli archi di CsC_s come DISCOVERY EDGE o CROSS EDGE e divide i vertici in livelli LiL_i secondo la distanza ii da ss (distanza d(x,y)d(x, y) = lunghezza minima di un cammino; +∞+\infty se in componenti diverse).

Esempio. Sul grafo con liste 1:2,41:2,4; 2:1,3,42:1,3,4; 3:2,5,63:2,5,6; 4:1,2,64:1,2,6; 5:3,65:3,6; 6:3,4,5,76:3,4,5,7; 7:67:6, la BFS da 11 ha livelli L0={1}L_0=\{1\}, L1={2,4}L_1=\{2,4\}, L2={3,6}L_2=\{3,6\}, L3={5,7}L_3=\{5,7\}.

Proprietà (BFS e spanning tree). Dopo BFS(G, s): tutti i vertici di CsC_s sono visitati e tutti i suoi archi etichettati; i discovery edge formano uno spanning tree di CsC_s radicato in ss (il BFS tree); per ogni v∈Liv \in L_i il cammino nel BFS tree da ss a vv ha i=d(s,v)i = d(s, v) archi; se (u,v)(u, v) è un cross edge i livelli di uu e vv differiscono al più di 11.

Esempio. Nello stesso grafo d(1,6)=2d(1,6)=2 (cammino 1,4,61,4,6) e il BFS tree ha n−1=6n-1=6 discovery edge.

Formula (complessità della BFS). Ogni vertice di CsC_s entra in una lista una sola volta (poi ID ≠0\ne 0) e ogni arco di CsC_s viene etichettato una volta; dalla lista di adiacenza di ciascun vertice ogni arco è esaminato due volte (una per estremo). Con nsn_s vertici e msm_s archi in CsC_s il costo è Θ(ns+ms)=Θ(ms)\Theta(n_s + m_s) = \Theta(m_s) (se ss non è isolato, ms≥ns−1m_s \ge n_s - 1). Se GG è connesso, Θ(n+m)\Theta(n + m). Il conto: ogni vertice di CsC_s è estratto da una lista una volta e per ciascuno si percorre la sua lista di adiacenza, di lunghezza degree(v)\text{degree}(v) (Rappresentazione dei grafiStrutture di base (lista dei vertici LV, lista degli archi LE), liste di adiacenza, matrice di adiacenza; operazioni incidentEdges, opposite, areAdjacent; spazio e tempi a confronto; esempio con un grafo di 5 vertici.Rappresentazione dei grafi →); il costo totale è ∑v(1+degree(v))=ns+2ms\sum_{v}\big(1+\text{degree}(v)\big)=n_s+2m_s perché la somma dei gradi è 2ms2m_s (Grafi - definizioni e proprietàGrafo G=(V,E) diretto e non diretto, grafo semplice e pesato; incidenza, adiacenza, grado; cammini, cicli, sottografi, grafi connessi e componenti connesse; alberi liberi, foreste, spanning tree e spanning forest; proprietà con dimostrazioni (somma dei gradi = 2m, m <= n(n-1)/2, alberi m = n-1, connessi m >= n-1, foreste m <= n-1).Grafi - definizioni e proprietà →), e ns+2ms∈Θ(ns+ms)n_s+2m_s\in\Theta(n_s+m_s). Con la matrice di adiacenza lo scorrimento dei vicini di ogni vertice costerebbe Θ(n)\Theta(n) e il totale Θ(n2)\Theta(n^2).

Esempio. Con ns=7n_s=7 vertici e ms=9m_s=9 archi il costo è Θ(ns+ms)=Θ(16)\Theta(n_s+m_s)=\Theta(16).

Proprietà (DFS). Dopo DFS(G, s): tutti i vertici di CsC_s sono visitati, tutti gli archi di CsC_s sono etichettati DISCOVERY o BACK, e i discovery edge formano uno spanning tree di CsC_s radicato in ss. Complessità Θ(ms)\Theta(m_s) con liste di adiacenza (Θ(n+m)\Theta(n + m) se connesso), con lo stesso argomento della BFS. Un vertice uu si dice discoverable da vv se esiste un cammino da vv a uu fatto di vertici non ancora visitati.

Esempio. La DFS da 11 nello stesso grafo visita 1,2,3,5,6,4,71,2,3,5,6,4,7 con 66 discovery edge e 33 back edge, (3,6)(3,6), (1,4)(1,4) e (2,4)(2,4).

Proprietà (confronto tra BFS e DFS)

BFS DFS
struttura di appoggio liste di livelli (coda) ricorsione (pila)
archi non-tree cross edge, tra livelli adiacenti back edge
cammini nell'albero minimi in numero di archi non necessariamente minimi
costo (liste di adiacenza) Θ(n+m)\Theta(n + m) Θ(n+m)\Theta(n + m)

Esempio. La BFS trova d(1,6)=2d(1,6)=2, mentre il cammino da 11 a 66 nell'albero DFS (1,2,3,5,61,2,3,5,6) ha lunghezza 44.

XI. Cammini minimi, alberi di copertura e ordinamento topologico

Cammini minimi e algoritmo di Dijkstra

Definizione (lunghezza di un cammino e distanza). Sia G=(V,E,w)G = (V, E, w) un grafo non diretto e pesato, con w:E→Rw : E \to \mathbb{R} (esempio: rete di aeroporti con distanze in km; R\mathbb{R} sono i numeri reali: Insiemi numerici (N, Z, Q, R)Naturali, interi, razionali e reali, perché la radice di 2 obbliga a passare da Q a R, e gli intervalli.Insiemi numerici (N, Z, Q, R) →). Il problema è lo stesso dell'instradamento dei pacchetti in Internet, dove i pesi sono costi dei collegamenti e l'algoritmo è detto link state (Algoritmi di instradamento - link state e distance vectorL'instradamento (routing) trova il percorso di costo minimo in un grafo pesato in cui i router sono nodi e le reti tra due router sono archi. Link state: ogni router diffonde con un flooding i pacchetti LSP sui propri collegamenti, ricostruisce tutto il grafo e applica Dijkstra (nodi con stato (distanza, permanente o temporaneo)). Distance vector: ogni router conosce solo i vicini e scambia con loro il proprio vettore delle distanze, aggiornato con Bellman-Ford $D_{A,w}=\min{D_{A,w},,D_{A,Y}+d_{Y,w}}$; converge in al più $n-1$ giri ma può soffrire del conteggio all'infinito (limite a 16, hold down, aggiornamenti immediati, split horizon con poison reverse). Path vector: ogni annuncio porta l'intero cammino, si sceglie per politica e non per costo, e i cicli si scoprono trovando sé stessi nel cammino. Dijkstra è corretto se i costi sono non negativi (invariante: un nodo permanente ha già la distanza vera); Bellman-Ford è corretto perché dopo $k$ giri conosce i cammini minimi con al più $k+1$ archi.Algoritmi di instradamento - link state e distance vector →, Instradamento e inoltroL'inoltro (forwarding) mette il pacchetto sulla strada verso la destinazione, un salto alla volta (hop by hop). Se la destinazione è nella stessa rete del mittente l'inoltro è diretto (si usa l'ARP per il MAC del destinatario), altrimenti è indiretto: il pacchetto va al router successivo (next hop) indicato dalla tabella di instradamento, o al default gateway. Con le netmask: l'inoltro è diretto attraverso l'interfaccia $x$ se $\text{IP(dst)}\ \text{AND}\ \text{NM}(x)=\text{IP}(x)\ \text{AND}\ \text{NM}(x)$; altrimenti si scorre la tabella dalla maschera più lunga (longest prefix match) e si usa il primo match. La riga con rete $0.0.0.0$ e maschera $0.0.0.0$ (default route) corrisponde sempre. L'aggregazione di rotte (route aggregation) riduce la tabella, e nell'inoltro con etichette (MPLS) la tabella si consulta per indice.Instradamento e inoltro →). La lunghezza di un cammino u1,…,uku_1, \dots, u_k è ∑i=1k−1w(ui,ui+1)\sum_{i=1}^{k-1} w(u_i, u_{i+1}); se w(e)=1w(e) = 1 per ogni arco è il numero di archi. La distanza d(u,v)d(u, v) è la minima lunghezza di un cammino da uu a vv e il cammino che la realizza è un cammino minimo.

Esempio. Nel grafo dell'esempio svolto il cammino A,C,F,EA,C,F,E ha lunghezza 9+2+9=209+2+9=20 e d(A,E)=20d(A,E)=20.

Proposizione (sottostruttura ottima dei cammini minimi). Se u1,…,uku_1, \dots, u_k è un cammino minimo da u1u_1 a uku_k, allora ui,…,uju_i, \dots, u_j è un cammino minimo da uiu_i a uju_j per ogni 1≤i<j≤k1 \le i < j \le k. Dimostrazione (per assurdo: Tecniche di dimostrazioneDimostrazione diretta, per contronominale e per assurdo di un teorema del tipo ipotesi implica tesi.Tecniche di dimostrazione →): se esistesse un cammino più corto da uiu_i a uju_j, sostituendolo nel cammino originale si otterrebbe un cammino da u1u_1 a uku_k più corto, contro l'ipotesi. In formule: la lunghezza del cammino originale è ℓ(u1..ui)+ℓ(ui..uj)+ℓ(uj..uk)\ell(u_1..u_i)+\ell(u_i..u_j)+\ell(u_j..u_k); sostituendo il termine centrale con uno strettamente minore si ottiene una somma strettamente minore.

Esempio. Se A,C,F,EA,C,F,E è un cammino minimo da AA a EE, allora C,FC,F è un cammino minimo da CC a FF (d(C,F)=2d(C,F)=2).

Definizione (algoritmo di Dijkstra). Sviluppato nel 1956, richiede pesi non negativi. Generalizza la BFS: a partire da ss fa crescere una cloud di vertici di cui si conoscono già distanza da ss e cammino minimo.

Esempio. Sul grafo dell'esempio svolto Dijkstra da AA estrae i vertici in ordine di distanza: A (0)A\,(0), B (7)B\,(7), C (9)C\,(9), F (11)F\,(11), D (20)D\,(20), E (20)E\,(20).

Teorema (complessità dell'algoritmo di Dijkstra). La complessità di ShortestPaths(G, s) è O(min⁡{n2,(n+m)log⁡n})O(\min\{n^2, (n + m) \log n\}): con la scelta migliore dell'implementazione. Per grafi sparsi (m=O(n)m = O(n)) conviene lo heap (O(nlog⁡n)O(n \log n)); per grafi densi (m=Θ(n2)m = \Theta(n^2)) la lista (Θ(n2)\Theta(n^2) contro Θ(n2log⁡n)\Theta(n^2 \log n)).

Esempio. Con n=1000n=1000 e m=3000m=3000 conviene lo heap: (n+m)log⁡2n≈4⋅104(n+m)\log_2n\approx4\cdot10^4 contro n2=106n^2=10^6 (log⁡21000≈9,97\log_2 1000\approx 9{,}97, quindi 4000⋅9,97≈39 9004000\cdot9{,}97\approx39\,900).

Grafico interattivo: Costo di Dijkstra con n = 1000 in funzione di m: lista non ordinata n² = 10⁶ (costante), heap (n+m)·log₂n; si incrociano a m ≈ 99000, sotto il quale lo heap vince

Minimum spanning tree

Definizione (minimum spanning tree). Dato un grafo G=(V,E,w)G = (V, E, w) non diretto, pesato e connesso, un minimum spanning tree (MST, albero di copertura di peso minimo) è uno spanning tree TT (vedi Grafi - definizioni e proprietàGrafo G=(V,E) diretto e non diretto, grafo semplice e pesato; incidenza, adiacenza, grado; cammini, cicli, sottografi, grafi connessi e componenti connesse; alberi liberi, foreste, spanning tree e spanning forest; proprietà con dimostrazioni (somma dei gradi = 2m, m <= n(n-1)/2, alberi m = n-1, connessi m >= n-1, foreste m <= n-1).Grafi - definizioni e proprietà →) che minimizza il peso totale w(T)=∑e∈Tw(e)w(T) = \sum_{e \in T} w(e). Ha n−1n - 1 archi. Applicazione tipica: collegare tutti i nodi di una rete (cavi, broadcast efficiente) con il costo minimo. Perché n−1n-1 archi: uno spanning tree è un albero libero con nn vertici, quindi ha m=n−1m=n-1 archi (proprietà P3 di Grafi - definizioni e proprietàGrafo G=(V,E) diretto e non diretto, grafo semplice e pesato; incidenza, adiacenza, grado; cammini, cicli, sottografi, grafi connessi e componenti connesse; alberi liberi, foreste, spanning tree e spanning forest; proprietà con dimostrazioni (somma dei gradi = 2m, m <= n(n-1)/2, alberi m = n-1, connessi m >= n-1, foreste m <= n-1).Grafi - definizioni e proprietà →); con meno archi non sarebbe connesso, con di più avrebbe un ciclo, che si potrebbe spezzare togliendo un arco e quindi abbassando il peso (se i pesi sono positivi). Un albero di copertura compare anche nell'analisi dei circuiti (Grafo di una rete ed equazioni indipendentiIl grafo di una rete tiene solo i collegamenti (nodi e lati). Scelto un albero (n−1 lati, connesso, senza maglie), ogni lato di coalbero dà una LKT indipendente e ogni lato d'albero una LKC indipendente: in totale ℓ − n + 1 LKT e n − 1 LKC, che con le ℓ equazioni dei bipoli fanno 2ℓ equazioni per 2ℓ incognite.Grafo di una rete ed equazioni indipendenti →), dove però non si cerca di minimizzare un peso ma di scegliere le equazioni indipendenti. Se i pesi sono tutti distinti l'MST è unico; altrimenti ce ne può essere più d'uno.

Esempio. Nel grafo AB 7AB\,7, AC 9AC\,9, AF 14AF\,14, BC 10BC\,10, BD 15BD\,15, CD 11CD\,11, CF 2CF\,2, DE 6DE\,6, EF 9EF\,9 l'MST ha n−1=5n-1=5 archi e peso 3333.

Proprietà (del taglio). Sia FF un sottoinsieme degli archi di un qualche MST e sia (A,B)(A, B) un taglio che nessun arco di FF attraversa. Se ee è un arco di peso minimo tra quelli che attraversano il taglio, allora F∪{e}F \cup \{e\} è ancora contenuto in un MST.

Esempio. Con il taglio {A}\{A\} contro il resto gli archi che lo attraversano pesano 7,9,147,9,14: l'arco AB (7)AB\,(7) appartiene a un MST. Un secondo taglio, con F={AB}F=\{AB\} già scelto e A′={A,B}A'=\{A,B\}, B′={C,D,E,F}B'=\{C,D,E,F\}: gli archi che attraversano sono AC 9AC\,9, AF 14AF\,14, BC 10BC\,10, BD 15BD\,15; il minimo è AC 9AC\,9, quindi F∪{AC}F\cup\{AC\} è ancora contenuto in un MST.

Definizione (algoritmo di Kruskal). Si scorrono gli archi per peso crescente e si aggiunge l'arco se non crea un ciclo con quelli già scelti, cioè se collega due componenti diverse.

Esempio. Nello stesso grafo Kruskal sceglie CF 2CF\,2, DE 6DE\,6, AB 7AB\,7, AC 9AC\,9, EF 9EF\,9: peso 2+6+7+9+9=332+6+7+9+9=33.

Definizione (algoritmo di Prim). Si parte da un vertice ss e si fa crescere un unico albero, aggiungendo sempre l'arco di peso minimo che collega l'albero a un vertice esterno. Si realizza come Dijkstra (vedi Cammini minimi e algoritmo di DijkstraGrafi pesati, lunghezza di un cammino e distanza; sottocammini di un cammino minimo; problema SSSP; algoritmo di Dijkstra con cloud e priority queue, rilassamento degli archi, esempio svolto; correttezza (due lemmi) e complessità O(min(n^2, (n+m) log n)) con lista non ordinata o heap; pesi non negativi.Cammini minimi e algoritmo di Dijkstra →) con una coda con priorità in cui la chiave di un vertice esterno vv è il peso del miglior arco che lo collega all'albero.

Esempio. Da AA Prim aggiunge AB 7AB\,7, AC 9AC\,9, CF 2CF\,2, FE 9FE\,9, ED 6ED\,6: peso 3333, come con Kruskal. L'albero cresce da un solo vertice, mentre Kruskal fa crescere una foresta di più alberi che si fondono; l'invariante di Prim è che l'insieme degli archi scelti forma un albero che contiene ss, e il taglio della proprietà è "albero contro resto".

Grafi diretti e ordinamento topologico

Definizione (grafo diretto, grado entrante e uscente). In un grafo diretto G=(V,E)G = (V, E) ogni arco è una coppia ordinata (u→v)(u \to v): uu è la coda (origine) e vv la testa (destinazione). Per ogni vertice si distinguono il grado uscente out(v)\text{out}(v) (archi che partono da vv) e il grado entrante in(v)\text{in}(v) (archi che arrivano in vv); vale ∑vin(v)=∑vout(v)=m\sum_v \text{in}(v) = \sum_v \text{out}(v) = m (ogni arco ha una sola coda e una sola testa, quindi contribuisce 11 a una somma di uscenti e 11 a una somma di entranti: a differenza dei grafi non diretti, dove la somma dei gradi è 2m2m, Grafi - definizioni e proprietàGrafo G=(V,E) diretto e non diretto, grafo semplice e pesato; incidenza, adiacenza, grado; cammini, cicli, sottografi, grafi connessi e componenti connesse; alberi liberi, foreste, spanning tree e spanning forest; proprietà con dimostrazioni (somma dei gradi = 2m, m <= n(n-1)/2, alberi m = n-1, connessi m >= n-1, foreste m <= n-1).Grafi - definizioni e proprietà →). Nei circuiti elettrici il grafo ha archi orientati secondo il verso di riferimento delle correnti (Grafo di una rete ed equazioni indipendentiIl grafo di una rete tiene solo i collegamenti (nodi e lati). Scelto un albero (n−1 lati, connesso, senza maglie), ogni lato di coalbero dà una LKT indipendente e ogni lato d'albero una LKC indipendente: in totale ℓ − n + 1 LKT e n − 1 LKC, che con le ℓ equazioni dei bipoli fanno 2ℓ equazioni per 2ℓ incognite.Grafo di una rete ed equazioni indipendenti →).

Esempio. Con archi A→BA\to B, A→CA\to C, C→BC\to B: out(A)=2\text{out}(A)=2 e in(B)=2\text{in}(B)=2.

Definizione (DAG). Un DAG (directed acyclic graph) è un grafo diretto senza cicli diretti. Modella dipendenze: prerequisiti di corsi, compiti di un progetto, dipendenze tra moduli di un programma. Anche una rete combinatoria di porte logiche è un DAG (le uscite dipendono dagli ingressi senza retroazione: Reti combinatorie e mappe di KarnaughRete combinatoria (uscite funzione dei soli ingressi attuali); mintermini e maxtermini, forme canoniche SOP e POS; mappe di Karnaugh a 3 e 4 variabili con esempi svolti; condizioni di indifferenza; costo e ritardo di una rete a due livelli.Reti combinatorie e mappe di Karnaugh →), e per calcolare l'uscita si valutano le porte proprio in ordine topologico; una retroazione crea un ciclo e dà una rete sequenziale (Reti sequenziali e automi a stati finitiModello di una rete sequenziale sincrona (stato in flip-flop, logica di stato prossimo e di uscita); automi di Moore e di Mealy; procedimento di sintesi con esempio svolto di un riconoscitore della sequenza 11.Reti sequenziali e automi a stati finiti →). Una relazione "uu deve precedere vv" senza cicli è un ordine parziale: un ordine topologico ne è un'estensione a ordine totale.

Esempio. Gli archi A→BA\to B e B→CB\to C formano un DAG; aggiungendo C→AC\to A si crea un ciclo diretto e il grafo non è più un DAG.

Definizione (ordinamento topologico). Un ordinamento topologico di un DAG è una sequenza di tutti i vertici tale che per ogni arco (u→v)(u \to v) il vertice uu precede vv. Esiste se e solo se il grafo è un DAG (con un ciclo, ogni vertice del ciclo dovrebbe precedere il successivo: impossibile). In generale non è unico.

Esempio. Per il DAG con archi A→BA\to B e A→CA\to C sono ordini topologici sia A,B,CA,B,C sia A,C,BA,C,B.

Proprietà (cammini minimi in un DAG pesato). Applicazione: cammini minimi in un DAG pesato in Θ(n+m)\Theta(n + m), anche con pesi negativi: si rilassano gli archi uscenti dei vertici nell'ordine topologico a partire dalla sorgente (quando si arriva a vv tutti i predecessori sono già definitivi).

Esempio. Con archi s→a (2)s\to a\,(2), s→b (3)s\to b\,(3), b→a (−2)b\to a\,(-2) l'ordine topologico s,b,as,b,a dà d(a)=min⁡(2, 3−2)=1d(a)=\min(2,\,3-2)=1. Passo per passo: d(s)=0d(s)=0; si rilassano gli archi uscenti di ss: d(a)=2d(a)=2, d(b)=3d(b)=3; si passa a bb (secondo nell'ordine): l'arco b→ab\to a dà 3+(−2)=1<23+(-2)=1<2, quindi d(a)=1d(a)=1; si passa ad aa e non ci sono archi uscenti. Qui non c'è il problema di Dijkstra con pesi negativi (Cammini minimi e algoritmo di DijkstraGrafi pesati, lunghezza di un cammino e distanza; sottocammini di un cammino minimo; problema SSSP; algoritmo di Dijkstra con cloud e priority queue, rilassamento degli archi, esempio svolto; correttezza (due lemmi) e complessità O(min(n^2, (n+m) log n)) con lista non ordinata o heap; pesi non negativi.Cammini minimi e algoritmo di Dijkstra →) perché bb è elaborato prima di aa: l'ordine topologico garantisce che quando si arriva a un vertice tutti i suoi predecessori sono già definitivi.