Esercizi · Algoritmi ricorsivi¶
Esercizi · Capitolo 17 · ogni soluzione si apre con un clic · Capitolo 17: Algoritmi ricorsivi · Eserciziario, volume 5 (PDF)
Negli esercizi di questo capitolo i passi si contano come nel capitolo «Algoritmi ricorsivi»: ogni riga di pseudocodice eseguita conta come un passo, e una riga che contiene una chiamata conta un passo più i passi della funzione chiamata. Si usano i seguenti risultati del capitolo.
Numero di passi su un array (o sotto-array) di \(n\) elementi:
SumIterative\((a,n)\) |
\(3n+4\) |
RecursiveSum\((a,1,n)\) |
\(7n-5\), con \(2n-1\) chiamate e profondità \(\lceil\log_2 n\rceil+1\) |
RecursiveSumLinear\((a,n)\) |
\(3n-1\), con \(n\) chiamate e profondità \(n\) |
Merge\((a,l,m,r)\), \(n=r-l+1\) |
\(6n+t+11\), dove \(t\) è il numero di confronti e \(\min\{n_1,n_2\}\le t\le n-1\) |
RecursiveBinarySearch\((a,1,n,m)\) |
\(S(0)=2\), \(S(n)=S(\lfloor n/2\rfloor)+5\) per \(n\ge 1\) |
BruteForceMaximumSubarray\((a,n)\) |
\(2n^2+7n+6+3U\), dove \(U\) è il numero di aggiornamenti di \(best\) |
Master theorem. Sia \(S(n)=a\,S(n/b)+f(n)\), con \(a\ge 1\) e \(b>1\) costanti e \(f(n)\) asintoticamente positiva.
-
Se \(f(n)=O\big(n^{\log_b a-\varepsilon}\big)\) per una qualche costante \(\varepsilon>0\), allora \(S(n)=\Theta\big(n^{\log_b a}\big)\).
-
Se \(f(n)=\Theta\big(n^{\log_b a}\log^k n\big)\) per una qualche costante \(k\ge 0\), allora \(S(n)=\Theta\big(n^{\log_b a}\log^{k+1} n\big)\).
-
Se \(f(n)=\Omega\big(n^{\log_b a+\varepsilon}\big)\) per una qualche costante \(\varepsilon>0\), e \(a\,f(n/b)\le c\,f(n)\) per una qualche costante \(c<1\) e per ogni \(n\) sufficientemente grande, allora \(S(n)=\Theta\big(f(n)\big)\).
Esercizio 17.1: Algoritmi ricorsivi (E1)
Consideriamo l'algoritmo ricorsivo RecursiveSum:
e l'array di \(n=9\) elementi
Domande
-
Disegnare l'albero delle chiamate di
RecursiveSum\((a,1,9)\), indicando per ogni chiamata gli indici \((l,r)\) e il valore restituito al padre. -
Elencare le chiamate nell'ordine in cui terminano.
-
Quante chiamate vengono eseguite in totale? Quante addizioni? Quanti passi (contando ogni riga eseguita come un passo)? Confrontare con le formule generali \(2n-1\), \(n-1\) e \(7n-5\).
-
Qual è il massimo numero di chiamate contemporaneamente attive (la profondità della ricorsione)? Quali chiamate sono attive in quel momento? Confrontare con la formula \(\lceil \log_2 n\rceil+1\).
Soluzione
-
La radice
RS\((1,9)\) calcola \(m=\lfloor 10/2\rfloor=5\); la metà sinistra \(a[1..5]\) ha \(\lceil 9/2\rceil=5\) elementi e la metà destra \(a[6..9]\) ha \(\lfloor 9/2\rfloor=4\) elementi. Procedendo allo stesso modo per ogni chiamata, otteniamo il seguente albero delle chiamate (in blu gli elementi restituiti dai casi base, in rosso i valori restituiti ai padri):I valori di \(m\) calcolati dalle chiamate interne sono:
chiamata \((1,9)\) \((1,5)\) \((1,3)\) \((1,2)\) \((4,5)\) \((6,9)\) \((6,7)\) \((8,9)\) \(m\) 5 3 2 1 4 7 6 8 valore restituito 45 24 12 11 12 21 8 13 L'algoritmo restituisce \(45=4+7+1+9+3+6+2+8+5\).
-
Le chiamate vengono eseguite in profondità, e una chiamata termina solo dopo che sono terminati entrambi i suoi figli. L'ordine di terminazione è:
\[ \begin{array}{l} (1,1),\ (2,2),\ (1,2),\ (3,3),\ (1,3),\ (4,4),\ (5,5),\ (4,5),\ (1,5),\\ (6,6),\ (7,7),\ (6,7),\ (8,8),\ (9,9),\ (8,9),\ (6,9),\ (1,9). \end{array} \] -
L'albero delle chiamate ha \(9\) foglie (una per ogni elemento) e \(8\) nodi interni:
-
numero di chiamate: \(17 = 2\cdot 9-1\);
-
numero di addizioni: una per ogni nodo interno, cioè \(8=9-1\);
-
numero di passi: ogni foglia esegue \(2\) righe e ogni nodo interno \(5\) righe, quindi il totale è \(9\cdot 2+8\cdot 5 = 58 = 7\cdot 9-5\).
-
-
L'albero delle chiamate ha altezza \(4\) (le foglie
RS\((1,1)\) eRS\((2,2)\) sono al livello \(4\)), quindi il massimo numero di chiamate contemporaneamente attive è \(5\). Per esempio, quando viene eseguitaRS\((1,1)\), le chiamate attive sono\[ \texttt{RS}(1,9),\quad \texttt{RS}(1,5),\quad \texttt{RS}(1,3),\quad \texttt{RS}(1,2),\quad \texttt{RS}(1,1). \]Questo è in accordo con la formula: \(\lceil \log_2 9\rceil+1 = 4+1 = 5\), poiché \(2^3=8<9\le 16=2^4\).
Si noti che le foglie non sono tutte allo stesso livello:
RS\((3,3)\), per esempio, è al livello \(3\). Le foglie più profonde si raggiungono seguendo sempre la metà più grande (quella sinistra): \(9\to 5\to 3\to 2\to 1\).
Esercizio 17.2: Algoritmi ricorsivi (E2)
Consideriamo i tre algoritmi che calcolano la somma degli elementi di un array \(a[1..n]\), \(n\ge 1\), studiati nel capitolo «Algoritmi ricorsivi»:
-
SumIterative\((a,n)\) (un ciclo); -
RecursiveSum\((a,1,n)\) (divide et impera, \(m=\lfloor (l+r)/2\rfloor\), caso base \(l=r\)); -
RecursiveSumLinear\((a,n)\) (decremento di uno: restituisceRecursiveSumLinear\((a,n-1)+a[n]\), caso base \(n=1\)).
Il loro numero di passi è richiamato nell'introduzione agli esercizi.
Domande
-
Per
RecursiveSum, sia \(A(n)\) il numero di addizioni eseguite su un array di \(n\) elementi. Scrivere un'equazione di ricorrenza per \(A(n)\) e dimostrare per induzione forte che \(A(n)=n-1\). -
Completare la seguente tabella, dove chiamate è il numero totale di chiamate (per
SumIterativescrivere \(-\)) e profondità è il massimo numero di chiamate contemporaneamente attive:\(n=10\) \(n=100\) \(n=1000\) passi chiamate profondità passi chiamate profondità passi chiamate profondità SumIterativeRecursiveSumRecursiveSumLinear -
Consideriamo la seguente variante di
RecursiveSum, che tratta come caso base anche i sotto-array di due elementi: se \(r-l+1\le 2\) restituisce \(a[l]\) (se \(l=r\)) oppure \(a[l]+a[r]\) (se \(r=l+1\)); altrimenti procede comeRecursiveSum. Quante chiamate e quante addizioni esegue per \(n=8\)? E per \(n=2^k\), \(k\ge 1\)? -
Spiegare perché ogni algoritmo che calcola la somma di \(n\) numeri sommandoli due alla volta esegue esattamente \(n-1\) addizioni.
Soluzione
-
Una chiamata su un elemento non esegue alcuna addizione; una chiamata su \(n\ge 2\) elementi esegue un'addizione (\(s_1+s_2\)) più le addizioni delle sue due chiamate ricorsive:
\[ A(n)= \begin{cases} 0 & \text{se } n=1,\\ A(\lceil n/2\rceil)+A(\lfloor n/2\rfloor)+1 & \text{se } n\ge 2. \end{cases} \]Caso base (\(n=1\)): \(A(1)=0=1-1\).
Passo induttivo (\(n\ge 2\)): supponiamo \(A(k)=k-1\) per ogni \(1\le k<n\). Poiché \(1\le\lfloor n/2\rfloor\le\lceil n/2\rceil<n\),
\[ A(n) = \big(\lceil n/2\rceil-1\big)+\big(\lfloor n/2\rfloor-1\big)+1 = \lceil n/2\rceil+\lfloor n/2\rfloor-1 = n-1. \] -
Usiamo le formule del capitolo «Algoritmi ricorsivi» (si veda anche l'introduzione agli esercizi):
-
SumIterative: \(3n+4\) passi, nessuna chiamata ricorsiva; -
RecursiveSum: \(7n-5\) passi, \(2n-1\) chiamate, profondità \(\lceil\log_2 n\rceil+1\); -
RecursiveSumLinear: \(3n-1\) passi, \(n\) chiamate (su \(n, n-1,\dots,1\) elementi), profondità \(n\) (tutte le chiamate sono attive quando si raggiunge il caso base).
Poiché \(2^3<10\le 2^4\), \(2^6<100\le 2^7\) e \(2^9<1000\le 2^{10}\), si ha \(\lceil\log_2 10\rceil=4\), \(\lceil\log_2 100\rceil=7\) e \(\lceil\log_2 1000\rceil=10\). Quindi:
\(n=10\) \(n=100\) \(n=1000\) passi chiamate profondità passi chiamate profondità passi chiamate profondità SumIterative34 \(-\) \(-\) 304 \(-\) \(-\) 3004 \(-\) \(-\) RecursiveSum65 19 5 695 199 8 6995 1999 11 RecursiveSumLinear29 10 10 299 100 100 2999 1000 1000 Tutti e tre gli algoritmi eseguono \(\Theta(n)\) passi, ma la memoria aggiuntiva (proporzionale alla profondità) è rispettivamente \(\Theta(1)\), \(\Theta(\log_2 n)\) e \(\Theta(n)\).
-
-
Per \(n=8\) i sotto-array hanno dimensioni \(8\to 4,4\to 2,2,2,2\): le chiamate su \(2\) elementi sono casi base. L'albero delle chiamate ha \(1+2+4=7\) chiamate: \(3\) chiamate interne (una addizione ciascuna) e \(4\) casi base su due elementi (una addizione ciascuno), per un totale di \(3+4=7\) addizioni.
Per \(n=2^k\), l'albero ha livelli \(0,1,\dots,k-1\), con \(2^i\) chiamate al livello \(i\) su sotto-array di \(2^{k-i}\) elementi; le chiamate al livello \(k-1\) sono i casi base su \(2\) elementi. Il numero di chiamate è
\[ \sum_{i=0}^{k-1}2^i = 2^k-1 = n-1 \](circa la metà delle \(2n-1\) chiamate di
RecursiveSum). Il numero di addizioni è ancora \((2^{k-1}-1)+2^{k-1} = 2^k-1 = n-1\): una per ciascuna delle \(2^{k-1}-1\) chiamate interne e una per ciascuno dei \(2^{k-1}\) casi base. -
Ogni addizione prende due numeri (elementi dell'array o somme parziali) e li sostituisce con un unico numero (la loro somma). All'inizio ci sono \(n\) numeri, alla fine ce n'è uno solo (la somma totale), e ogni addizione diminuisce il conteggio esattamente di uno. Quindi servono esattamente \(n-1\) addizioni, indipendentemente dall'ordine in cui vengono eseguite (iterativo, divide et impera, decremento di uno, …).
Esercizio 17.3: Algoritmi ricorsivi (E3)
Dato un array \(a[1..n]\) di \(n\ge 1\) numeri interi, consideriamo il problema di determinare il valore massimo nell'array. Vogliamo risolverlo con un algoritmo ricorsivo divide et impera RecursiveMax\((a,l,r)\), seguendo lo stesso schema di RecursiveSum\((a,l,r)\), e applicarlo all'array
Domande
-
Scrivere lo pseudocodice di
RecursiveMax\((a,l,r)\), che restituisce il massimo di \(a[l..r]\) (caso base \(l=r\), indice centrale \(m=\lfloor (l+r)/2\rfloor\)). -
Disegnare l'albero delle chiamate di
RecursiveMax\((a,1,6)\) per l'array dato, con il valore restituito da ogni chiamata. Quante chiamate e quanti confronti tra elementi vengono eseguiti? -
Dimostrare per induzione forte che
RecursiveMax\((a,l,r)\) restituisce \(\max\{a[l],\dots,a[r]\}\). -
Scrivere l'equazione di ricorrenza per il numero di passi \(S(n)\) di
RecursiveMaxe risolverla con il Master theorem. Poi calcolare \(S(n)\) esattamente. -
Determinare il tempo di esecuzione nel caso peggiore \(T(|I|)\), supponendo che ogni elemento usi un numero costante di bit, e confrontare l'algoritmo con l'algoritmo iterativo
FindMaximumdel capitolo «Tempo di esecuzione».
Soluzione
-
Il massimo di \(a[l..r]\) è il maggiore tra i massimi delle due metà:
Algoritmo: RecursiveMax
Input: Un array \(a\) e due indici \(l \le r\)Output: Il valore massimo in \(a[l..r]\)1function RecursiveMax(a, l, r)2if \(l = r\) then3return \(a[l]\)4end5\(m := \lfloor (l+r)/2 \rfloor\)6\(x_1 :=\) RecursiveMax\((a, l, m)\)7\(x_2 :=\) RecursiveMax\((a, m+1, r)\)8if \(x_1 \ge x_2\) then9return \(x_1\)10else11return \(x_2\)12end13end -
La radice calcola \(m=\lfloor 7/2\rfloor=3\). Questo è l'albero delle chiamate, in cui
RM\((l,r)\) indica la chiamataRecursiveMax\((a,l,r)\) e i valori restituiti sono in rosso:I confronti eseguiti alla riga 8 sono:
chiamata \(x_1\) \(x_2\) valore restituito RM\((1,2)\)5 17 \(5<17\Rightarrow 17\) RM\((1,3)\)17 2 \(17\ge 2\Rightarrow 17\) RM\((4,5)\)11 17 \(11<17\Rightarrow 17\) RM\((4,6)\)17 8 \(17\ge 8\Rightarrow 17\) RM\((1,6)\)17 17 \(17\ge 17\Rightarrow 17\) Ci sono \(11=2\cdot 6-1\) chiamate e \(5=6-1\) confronti (uno per ogni chiamata interna). Il massimo è \(17\).
-
Usiamo l'induzione forte su \(k=r-l+1\ge 1\).
Caso base (\(k=1\)): \(l=r\) e l'algoritmo restituisce \(a[l]=\max\{a[l]\}\).
Passo induttivo (\(k\ge 2\)): supponiamo che l'enunciato valga per tutti i sotto-array con meno di \(k\) elementi. Le due metà \(a[l..m]\) e \(a[m+1..r]\) hanno \(\lceil k/2\rceil\) e \(\lfloor k/2\rfloor\) elementi, entrambi compresi tra \(1\) e \(k-1\); per l'ipotesi induttiva
\[ x_1=\max\{a[l],\dots,a[m]\}, \qquad x_2=\max\{a[m+1],\dots,a[r]\}. \]Ogni elemento di \(a[l..r]\) appartiene a una delle due metà, quindi è al più \(\max\{x_1,x_2\}\); inoltre \(\max\{x_1,x_2\}\) è un elemento di \(a[l..r]\). Quindi \(\max\{a[l],\dots,a[r]\}=\max\{x_1,x_2\}\), che è esattamente il valore restituito dalle righe 8–11.
-
Se \(n=1\), vengono eseguite le righe 2 e 3: \(S(1)=2\). Se \(n\ge 2\):
-
La riga 6 viene eseguita \(1\) volta, più i \(S(\lceil n/2\rceil)\) passi della chiamata
-
La riga 7 viene eseguita \(1\) volta, più i \(S(\lfloor n/2\rfloor)\) passi della chiamata
Quindi
\[ S(n)= \begin{cases} 2 & \text{se } n=1,\\ S(\lceil n/2\rceil)+S(\lfloor n/2\rfloor)+6 & \text{se } n\ge 2, \end{cases} \qquad\text{cioè}\qquad S(n)=2\,S(n/2)+c. \]Master theorem:
-
\(a=2\), \(b=2\), \(f(n)=c\);
-
\(n^{\log_b a}=n^{\log_2 2}=n\);
-
\(f(n)=c\) cresce polinomialmente più lentamente di \(n\) \(\rightarrow\) caso 1;
-
\(f(n)=c=O(n^{1-\varepsilon})\) con \(\varepsilon=1\);
-
\(S(n)=\Theta(n^{\log_2 2})=\Theta(n)\).
Valore esatto: come per
RecursiveSum, l'albero delle chiamate ha \(n\) foglie (costo \(2\) ciascuna) e \(n-1\) nodi interni (costo \(6\) ciascuno), quindi\[ S(n)=2n+6(n-1)=8n-6. \](Si può verificare per induzione forte: \(S(1)=2=8-6\) e \(S(n)=(8\lceil n/2\rceil-6)+(8\lfloor n/2\rfloor-6)+6=8n-6\).) Per l'array dato, \(S(6)=42\).
-
Poiché \(|I|=\Theta(n)\) e \(S(n)=8n-6=\Theta(n)\), esattamente come nel capitolo «Algoritmi ricorsivi» otteniamo
Il tempo di esecuzione nel caso peggiore di
RecursiveMaxè \(T(|I|)=\Theta(|I|)\).FindMaximumesegue \(4n-1\) passi nel caso peggiore: entrambi gli algoritmi sono lineari ed eseguono \(n-1\) confronti tra elementi. L'algoritmo ricorsivo esegue più passi (\(8n-6\)) e usa \(\Theta(\log_2 n)\) di memoria aggiuntiva per la pila delle chiamate, contro \(\Theta(1)\) diFindMaximum.
Esercizio 17.4: Algoritmi ricorsivi (E4)
Consideriamo il seguente algoritmo ricorsivo, che suddivide il sotto-array \(a[l..r]\) in tre parti:
e l'array di \(n=9\) elementi \(a=[2, 7, 1, 8, 2, 8, 1, 8, 2]\).
Domande
-
Mostrare che, per \(n=r-l+1\ge 3\), le tre parti sono non vuote e hanno meno di \(n\) elementi. Quali sono le loro dimensioni?
-
Disegnare l'albero delle chiamate di
RecursiveSum3\((a,1,9)\) con il valore restituito da ogni chiamata. -
Scrivere l'equazione di ricorrenza per il numero di passi \(S(n)\) e risolverla con il Master theorem.
-
Supponiamo \(n=3^k\), \(k\ge 1\). Calcolare il numero di chiamate, il numero di addizioni e la profondità della ricorsione, e confrontarli con quelli di
RecursiveSum.
Soluzione
-
Siano \(n=r-l+1\ge 3\) e \(s=\lfloor n/3\rfloor\ge 1\). Le tre parti sono
\[ a[l..m_1] \ (s \text{ elementi}),\qquad a[m_1+1..m_2]\ (s \text{ elementi}),\qquad a[m_2+1..r]\ (n-2s \text{ elementi}). \]Poiché \(s\ge 1\), le prime due parti sono non vuote e hanno \(s\le n/3<n\) elementi. Scrivendo \(n=3s+\rho\) con \(\rho\in\{0,1,2\}\), la terza parte ha \(n-2s=s+\rho\) elementi, con \(1\le s\le s+\rho<3s+\rho=n\). Quindi anche la terza parte è non vuota e più piccola di \(n\), e tutte le dimensioni sono circa \(n/3\) (differiscono al più di \(2\)).
-
La radice ha \(n=9\), \(s=3\), \(m_1=3\), \(m_2=6\); ogni parte ha \(3\) elementi, che vengono suddivisi ancora in parti di un elemento:
Per esempio,
RS3\((4,6)\) calcola \(s=1\), \(m_1=4\), \(m_2=5\) e restituisce \(8+2+8=18\). L'algoritmo restituisce \(10+18+11=39\), che è la somma degli elementi. Ci sono \(13\) chiamate e \(4\cdot 2=8\) addizioni. -
Per \(n\le 2\), vengono eseguite le righe 2, 3 e (se \(n=2\)) 4: \(S(1)=2\), \(S(2)=3\). Per \(n\ge 3\), le righe 2, 6, 7, 8, 12 vengono eseguite una volta, e le righe 9, 10, 11 una volta ciascuna più i passi delle tre chiamate:
\[ S(n) = S(s)+S(s)+S(n-2s)+8, \qquad s=\lfloor n/3\rfloor, \]che, ignorando le parti intere inferiori, scriviamo come
\[ S(n)=3\,S(n/3)+c. \]Master theorem:
-
\(a=3\), \(b=3\), \(f(n)=c\);
-
\(n^{\log_b a}=n^{\log_3 3}=n\);
-
\(f(n)=c\) cresce polinomialmente più lentamente di \(n\) \(\rightarrow\) caso 1;
-
\(f(n)=c=O(n^{1-\varepsilon})\) con \(\varepsilon=1\);
-
\(S(n)=\Theta(n^{\log_3 3})=\Theta(n)\).
Come previsto, suddividere in tre parti invece che in due non cambia l'ordine di crescita: ogni elemento deve essere letto almeno una volta.
-
-
Per \(n=3^k\), ogni chiamata su \(3^{j}\) elementi, \(j\ge 1\), effettua tre chiamate su \(3^{j-1}\) elementi, e le chiamate su \(3^0=1\) elemento sono casi base. Il livello \(i\) dell'albero delle chiamate contiene \(3^i\) chiamate, per \(i=0,1,\dots,k\):
-
chiamate: \(\displaystyle\sum_{i=0}^{k}3^i=\frac{3^{k+1}-1}{2}=\frac{3n-1}{2}\) (per \(n=9\): \(13\), come nella domanda 2), contro le \(2n-1\) di
RecursiveSum; -
addizioni: ciascuna delle \(\frac{3n-1}{2}-n=\frac{n-1}{2}\) chiamate interne esegue \(2\) addizioni, per un totale di \(n-1\), come
RecursiveSum(ogni algoritmo che somma due numeri alla volta richiede \(n-1\) addizioni); -
profondità della ricorsione: l'altezza dell'albero è \(k=\log_3 n\), quindi la profondità è \(\log_3 n+1\) (per \(n=9\): \(3\)), contro \(\lceil\log_2 n\rceil+1\) di
RecursiveSum. Poiché \(\log_3 n=\frac{\log_2 n}{\log_2 3}\approx 0.63\,\log_2 n\), la profondità è minore di un fattore costante, ma è comunque \(\Theta(\log n)\).
-
Esercizio 17.5: Algoritmi ricorsivi (E5)
Consideriamo il seguente array di \(n=8\) numeri naturali:
L'array viene ordinato dalla chiamata MergeSort\((a,1,8)\) dell'algoritmo studiato nel capitolo «Algoritmi ricorsivi» (con \(m=\lfloor (l+r)/2\rfloor\) e la procedura Merge\((a,l,m,r)\)).
Domande
-
Disegnare l'albero delle chiamate di
MergeSort\((a,1,8)\), indicando per ogni chiamata gli indici \((l,r)\), il sotto-array ricevuto e il sotto-array ordinato prodotto. -
Elencare le chiamate di
Mergenell'ordine in cui vengono eseguite, e dare il contenuto dell'array \(a\) dopo ciascuna di esse. -
Per ogni chiamata di
Merge, dare il numero di confronti tra elementi (riga \(L[i]\le R[j]\)). Qual è il numero totale di confronti? -
Quante chiamate di
MergeSorte quante chiamate diMergevengono eseguite? Generalizzare a un array di \(n=2^k\) elementi.
Soluzione
-
Poiché \(n=8\) è una potenza di \(2\), ogni sotto-array viene suddiviso in due metà della stessa dimensione. In ogni nodo, in blu il sotto-array ricevuto e in rosso il sotto-array ordinato prodotto:
-
Le chiamate di
Mergevengono eseguite in profondità, dopo le due chiamate ricorsive del corrispondenteMergeSort(in rosso il sotto-array modificato):chiamata sotto-array fusi array \(a\) dopo la chiamata Merge\((a,1,1,2)\)\([38]\), \([27]\) \([\red{27,38},43,3,9,82,10,15]\) Merge\((a,3,3,4)\)\([43]\), \([3]\) \([27,38,\red{3,43},9,82,10,15]\) Merge\((a,1,2,4)\)\([27,38]\), \([3,43]\) \([\red{3,27,38,43},9,82,10,15]\) Merge\((a,5,5,6)\)\([9]\), \([82]\) \([3,27,38,43,\red{9,82},10,15]\) Merge\((a,7,7,8)\)\([10]\), \([15]\) \([3,27,38,43,9,82,\red{10,15}]\) Merge\((a,5,6,8)\)\([9,82]\), \([10,15]\) \([3,27,38,43,\red{9,10,15,82}]\) Merge\((a,1,4,8)\)\([3,27,38,43]\), \([9,10,15,82]\) \([\red{3,9,10,15,27,38,43,82}]\) -
I confronti di ciascuna
Mergesono:-
Merge\((a,1,1,2)\): \(38\le 27\)? No. Poi si copia \(L\). 1 confronto. -
Merge\((a,3,3,4)\): \(43\le 3\)? No. 1 confronto. -
Merge\((a,1,2,4)\), \(L=[27,38]\), \(R=[3,43]\): \(27\le 3\)? No; \(27\le 43\)? Sì; \(38\le 43\)? Sì; poi \(L\) è esaurito e si copia \(43\). 3 confronti. -
Merge\((a,5,5,6)\): \(9\le 82\)? Sì. 1 confronto. -
Merge\((a,7,7,8)\): \(10\le 15\)? Sì. 1 confronto. -
Merge\((a,5,6,8)\), \(L=[9,82]\), \(R=[10,15]\): \(9\le 10\)? Sì; \(82\le 10\)? No; \(82\le 15\)? No; poi \(R\) è esaurito e si copia \(82\). 3 confronti. -
Merge\((a,1,4,8)\), \(L=[3,27,38,43]\), \(R=[9,10,15,82]\):\(k\) \(i\) \(j\) \(L[i]\) \(R[j]\) azione 1 1 1 3 9 \(3\le 9\): \(a[1]\leftarrow 3\) 2 2 1 27 9 \(27>9\): \(a[2]\leftarrow 9\) 3 2 2 27 10 \(27>10\): \(a[3]\leftarrow 10\) 4 2 3 27 15 \(27>15\): \(a[4]\leftarrow 15\) 5 2 4 27 82 \(27\le 82\): \(a[5]\leftarrow 27\) 6 3 4 38 82 \(38\le 82\): \(a[6]\leftarrow 38\) 7 4 4 43 82 \(43\le 82\): \(a[7]\leftarrow 43\) 8 5 4 – 82 \(L\) esaurito: \(a[8]\leftarrow 82\) 7 confronti.
Il numero totale di confronti è \(1+1+3+1+1+3+7=17\). Ogni
Mergedi \(n'\) elementi esegue al più \(n'-1\) confronti; qui ogniMergeraggiunge questo massimo (\(1,1,3,1,1,3,7\)), quindi questo array è un caso peggiore per il numero di confronti del Merge Sort con \(n=8\): \(17=8\log_2 8-8+1\). -
-
L'albero delle chiamate ha \(8\) foglie e \(7\) nodi interni: ci sono \(15=2\cdot 8-1\) chiamate di
MergeSorte \(7=8-1\) chiamate diMerge(una per ogni nodo interno).Per \(n=2^k\), il livello \(i\) dell'albero delle chiamate contiene \(2^i\) chiamate, per \(i=0,1,\dots,k\), quindi il numero di chiamate di
MergeSortè \(\sum_{i=0}^{k}2^i=2^{k+1}-1=2n-1\). Le foglie sono le \(n\) chiamate su un elemento (che non chiamanoMerge), quindi il numero di chiamate diMergeè \(2n-1-n=n-1\).
Esercizio 17.6: Algoritmi ricorsivi (E6)
Consideriamo l'array di \(n=9\) elementi
in cui \(a[1..5]\) e \(a[6..9]\) sono ordinati, e la chiamata Merge\((a,1,5,9)\) della procedura Merge studiata nel capitolo «Algoritmi ricorsivi». Il numero di passi di Merge su \(n\) elementi è richiamato nell'introduzione agli esercizi (\(t\) è il numero di iterazioni del primo ciclo while).
Domande
-
Dare i valori di \(n_1\), \(n_2\), \(L\) e \(R\).
-
Tracciare l'esecuzione di
Merge\((a,1,5,9)\) in una tabella con colonne \(k\), \(i\), \(j\), \(L[i]\), \(R[j]\), confronto e azione. -
Quanti confronti tra elementi vengono eseguiti? Quanti elementi vengono copiati dal primo ciclo while e quanti dagli altri due cicli? Quanti passi vengono eseguiti?
-
Ripetere la domanda 3 (senza la tabella) se la parte destra viene sostituita da \([22,23,24,25]\) e da \([0,1,1,1]\), cioè per \(a=[2,5,8,13,20,22,23,24,25]\) e per \(a=[2,5,8,13,20,0,1,1,1]\).
Soluzione
-
\(n_1=m-l+1=5-1+1=5\) e \(n_2=r-m=9-5=4\). I due cicli for di
Mergecopiano le due parti ordinate:\[ L=[2,5,8,13,20], \qquad R=[3,4,9,21]. \] -
Inizialmente \(i=1\), \(j=1\), \(k=1\). Una riga per ogni elemento copiato in \(a\):
\(k\) \(i\) \(j\) \(L[i]\) \(R[j]\) confronto azione 1 1 1 2 3 \(2\le 3\) \(a[1]\leftarrow 2\), \(i=2\) 2 2 1 5 3 \(5>3\) \(a[2]\leftarrow 3\), \(j=2\) 3 2 2 5 4 \(5>4\) \(a[3]\leftarrow 4\), \(j=3\) 4 2 3 5 9 \(5\le 9\) \(a[4]\leftarrow 5\), \(i=3\) 5 3 3 8 9 \(8\le 9\) \(a[5]\leftarrow 8\), \(i=4\) 6 4 3 13 9 \(13>9\) \(a[6]\leftarrow 9\), \(j=4\) 7 4 4 13 21 \(13\le 21\) \(a[7]\leftarrow 13\), \(i=5\) 8 5 4 20 21 \(20\le 21\) \(a[8]\leftarrow 20\), \(i=6\) 9 6 4 – 21 \(L\) esaurito \(a[9]\leftarrow 21\), \(j=5\) Dopo l'\(8\)-a iterazione \(i=6>n_1\): il primo ciclo while termina, il ciclo che copia il resto di \(L\) viene saltato, e il ciclo che copia il resto di \(R\) copia \(R[4]=21\). Il risultato è
\[ a=[2,3,4,5,8,9,13,20,21]. \] -
Ci sono \(t=8\) confronti, che è il massimo valore possibile \(n-1=8\). Il primo ciclo while copia \(8\) elementi, il ciclo su \(L\) copia \(0\) elementi e il ciclo su \(R\) copia \(1\) elemento. Il numero di passi è
\[ 6n+t+11 = 6\cdot 9+8+11 = 73. \] -
Parte destra \([22,23,24,25]\). Tutti gli elementi di \(L\) sono minori di \(R[1]=22\): il primo ciclo while copia \(2,5,8,13,20\) con i confronti \(2\le 22\), \(5\le 22\), \(8\le 22\), \(13\le 22\), \(20\le 22\), e si ferma perché \(L\) è esaurito. Il ciclo su \(R\) copia \(22,23,24,25\). Quindi \(t=5\) confronti (\(5\) elementi copiati dal primo ciclo, \(4\) dal ciclo su \(R\)) e \(6\cdot 9+5+11=70\) passi.
Parte destra \([0,1,1,1]\). Tutti gli elementi di \(R\) sono minori di \(L[1]=2\): il primo ciclo while copia \(0,1,1,1\) con i confronti \(2\le 0\), \(2\le 1\), \(2\le 1\), \(2\le 1\) (tutti falsi), e si ferma perché \(R\) è esaurito. Il ciclo su \(L\) copia \(2,5,8,13,20\). Quindi \(t=4\) confronti (\(4\) elementi copiati dal primo ciclo, \(5\) dal ciclo su \(L\)) e \(6\cdot 9+4+11=69\) passi. Questo è il minimo numero possibile di confronti, \(\min\{n_1,n_2\}=4\).
In tutti i casi il numero di passi è compreso tra \(6n+11=65\) e \(7n+10=73\):
Mergeè lineare nel numero di elementi, qualunque siano i loro valori.
Esercizio 17.7: Algoritmi ricorsivi (E7)
Consideriamo la procedura Merge\((a,l,m,r)\) studiata nel capitolo «Algoritmi ricorsivi», che fonde i sotto-array ordinati \(a[l..m]\) (\(n_1\) elementi) e \(a[m+1..r]\) (\(n_2\) elementi), con \(n_1,n_2\ge 1\). Sia \(C\) il numero di confronti tra elementi (\(L[i]\le R[j]\)) eseguiti da Merge, e sia \(C_\text{MS}(n)\) il numero totale di confronti eseguiti da MergeSort su un array di \(n\) elementi.
Domande
-
Dimostrare che \(\min\{n_1,n_2\}\le C\le n_1+n_2-1\).
-
Mostrare che entrambi i limiti sono raggiunti: per \(n_1=n_2=4\), dare due sotto-array ordinati per i quali
Mergeeffettua \(4\) confronti e due per i quali ne effettua \(7\). -
Sia \(n=2^k\). Scrivere equazioni di ricorrenza per il minimo \(B(n)\) e per il massimo \(W(n)\) di \(C_\text{MS}(n)\) su tutti gli array di \(n\) elementi, e risolverle.
-
Per \(n=8\), verificare le formule con gli array \([1,2,3,4,5,6,7,8]\) e \([1,5,3,7,2,6,4,8]\). Che cosa succede con l'array \([8,7,6,5,4,3,2,1]\)?
Soluzione
-
Ogni iterazione del primo ciclo while esegue esattamente un confronto e copia esattamente un elemento in \(a\); gli altri due cicli non eseguono confronti. Quindi \(C=t\), il numero di iterazioni del primo ciclo while.
-
Limite inferiore. Il ciclo termina solo quando \(i>n_1\) oppure \(j>n_2\), cioè quando tutti gli elementi di \(L\) o tutti gli elementi di \(R\) sono stati copiati dal ciclo. Quindi \(t\ge n_1\) oppure \(t\ge n_2\), e in entrambi i casi \(t\ge\min\{n_1,n_2\}\).
-
Limite superiore. Ogni iterazione aumenta \(i\) o \(j\) di uno, e prima dell'ultima iterazione valgono sia \(i\le n_1\) sia \(j\le n_2\). Quando il ciclo termina, esattamente uno dei due array è esaurito, mentre l'altro contiene ancora almeno un elemento (non era esaurito prima dell'ultima iterazione, e l'ultima iterazione ha copiato un elemento del primo array). Quindi il ciclo copia al più \(n_1+n_2-1\) elementi: \(t\le n_1+n_2-1\).
-
-
\(4\) confronti: tutti gli elementi di una parte sono minori di tutti gli elementi dell'altra, per esempio \(L=[1,2,3,4]\) e \(R=[5,6,7,8]\). I confronti sono \(1\le 5\), \(2\le 5\), \(3\le 5\), \(4\le 5\); poi \(L\) è esaurito e \(5,6,7,8\) vengono copiati senza confronti.
\(7\) confronti: gli elementi delle due parti si alternano, per esempio \(L=[1,3,5,7]\) e \(R=[2,4,6,8]\). I confronti sono
\[ 1\le 2,\ \ 3\le 2,\ \ 3\le 4,\ \ 5\le 4,\ \ 5\le 6,\ \ 7\le 6,\ \ 7\le 8, \]dopodiché \(L\) è esaurito e viene copiato senza confronti solo \(8\).
-
Sia \(n=2^k\).
MergeSortesegue i confronti delle due chiamate ricorsive su \(n/2\) elementi più quelli dellaMergefinale con \(n_1=n_2=n/2\), e le chiamate su un elemento non eseguono alcun confronto.-
Minimo. Per la domanda 1, la
Mergefinale esegue almeno \(n/2\) confronti, ed esattamente \(n/2\) se le due metà sono, per esempio, una interamente minore dell'altra (cosa che accade se l'array è ordinato, a tutti i livelli della ricorsione). Quindi\[ B(1)=0,\qquad B(n)=2\,B(n/2)+\frac{n}{2}. \] -
Massimo. La
Mergefinale esegue al più \(n-1\) confronti, ed esattamente \(n-1\) se gli elementi delle due metà ordinate si alternano. Quindi\[ W(1)=0,\qquad W(n)=2\,W(n/2)+n-1. \](Che il massimo venga raggiunto contemporaneamente a tutti i livelli è mostrato dall'esempio della domanda 4.)
Risolviamo le equazioni di ricorrenza con l'albero di ricorsione: il livello \(i\), \(i=0,\dots,k-1\), ha \(2^i\) chiamate su \(n/2^i\) elementi.
\[ B(n)=\sum_{i=0}^{k-1}2^i\cdot\frac{n}{2^{i+1}}=\sum_{i=0}^{k-1}\frac{n}{2}=\frac{n}{2}\log_2 n, \]\[ W(n)=\sum_{i=0}^{k-1}2^i\left(\frac{n}{2^i}-1\right)=k\,n-\sum_{i=0}^{k-1}2^i=n\log_2 n-(2^k-1)=n\log_2 n-n+1. \]In entrambi i casi \(C_\text{MS}(n)=\Theta(n\log_2 n)\): anche il caso migliore di
MergeSortè \(\Theta(n\log n)\) (al contrario, il caso migliore diInsertionSortè lineare). -
-
Per \(n=8\) (\(k=3\)) le formule danno \(B(8)=4\cdot 3=12\) e \(W(8)=24-8+1=17\).
-
\([1,2,3,4,5,6,7,8]\) (ordinato): le quattro fusioni di coppie eseguono \(1\) confronto ciascuna, le due fusioni di quadruple (\([1,2]\) con \([3,4]\), \([5,6]\) con \([7,8]\)) \(2\) ciascuna, e la fusione finale di \([1,2,3,4]\) con \([5,6,7,8]\) esegue \(4\) confronti: \(4\cdot 1+2\cdot 2+4=12=B(8)\).
-
\([1,5,3,7,2,6,4,8]\): le coppie \([1,5]\), \([3,7]\), \([2,6]\), \([4,8]\) sono già ordinate (\(1\) confronto ciascuna); poi \([1,5]\) e \([3,7]\) si alternano, dando \([1,3,5,7]\) con \(3\) confronti, e \([2,6]\) e \([4,8]\) si alternano, dando \([2,4,6,8]\) con \(3\) confronti; infine \([1,3,5,7]\) e \([2,4,6,8]\) si alternano (\(7\) confronti, si veda la domanda 2). Totale: \(4\cdot 1+2\cdot 3+7=17=W(8)\).
-
\([8,7,6,5,4,3,2,1]\) (invertito): ogni fusione combina due metà in cui tutti gli elementi di \(R\) sono minori di tutti gli elementi di \(L\). Ogni fusione esegue \(\min\{n_1,n_2\}\) confronti, esattamente come per l'array ordinato: \(4\cdot 1+2\cdot 2+4=12=B(8)\). L'array invertito, che è il caso peggiore di
InsertionSort, è un caso migliore per il numero di confronti diMergeSort.
-
Esercizio 17.8: Algoritmi ricorsivi (E8)
Consideriamo la seguente variante a 3 vie del Merge Sort, che suddivide \(a[l..r]\) in tre parti e usa due volte la procedura Merge del capitolo «Algoritmi ricorsivi» (Merge funziona correttamente anche quando la sua parte destra è vuota, cioè \(n_2=0\)):
Consideriamo inoltre l'array \(a=[24, 5, 17, 9, 31, 2, 12, 20, 7]\) di \(n=9\) elementi.
Domande
-
Applicare
MergeSort3\((a,1,9)\) all'array dato: dare le tre parti della prima chiamata, le parti ordinate restituite dalle tre chiamate ricorsive, e il risultato di ciascuna delle due chiamate finali diMerge. -
Spiegare che cosa fanno le righe 8 e 9, e mostrare che insieme eseguono \(\Theta(n)\) passi, dove \(n=r-l+1\).
-
Scrivere l'equazione di ricorrenza per il numero di passi \(S(n)\) di
MergeSort3e risolverla con il Master theorem.MergeSort3è asintoticamente più veloce diMergeSort? -
Sia \(n=3^k\). Confrontare il massimo numero di confronti tra elementi eseguiti a ciascun livello della ricorsione e il numero di livelli con quelli di
MergeSort. Quale algoritmo esegue meno confronti nel caso peggiore, per \(n\) grande?
Soluzione
-
Nella prima chiamata \(l=1\), \(r=9\): \(m_1=\lfloor 11/3\rfloor=3\) e \(m_2=\lfloor 19/3\rfloor=6\). Le tre parti sono
\[ a[1..3]=[24,5,17],\qquad a[4..6]=[9,31,2],\qquad a[7..9]=[12,20,7]. \]Ogni parte ha \(3\) elementi; per esempio, per \(a[1..3]\): \(m_1=\lfloor 5/3\rfloor=1\) e \(m_2=\lfloor 7/3\rfloor=2\), quindi viene suddivisa in tre parti di un elemento. Le tre chiamate ricorsive restituiscono
\[ a[1..3]=[5,17,24],\qquad a[4..6]=[2,9,31],\qquad a[7..9]=[7,12,20], \]cioè \(a=[5,17,24,2,9,31,7,12,20]\). Poi:
-
Merge\((a,1,3,6)\) fonde \([5,17,24]\) e \([2,9,31]\) in \(a[1..6]=[2,5,9,17,24,31]\); ora \(a=[2,5,9,17,24,31,7,12,20]\); -
Merge\((a,1,6,9)\) fonde \([2,5,9,17,24,31]\) e \([7,12,20]\): \(a=[2,5,7,9,12,17,20,24,31]\).
-
-
Dopo le tre chiamate ricorsive, \(a[l..m_1]\), \(a[m_1+1..m_2]\) e \(a[m_2+1..r]\) sono ordinati. La riga 8 fonde le prime due parti, in modo che \(a[l..m_2]\) sia ordinato; la riga 9 fonde \(a[l..m_2]\) con la terza parte, in modo che \(a[l..r]\) sia ordinato.
Mergesu \(n'\) elementi esegue tra \(6n'+11\) e \(7n'+10\) passi. La prima chiamata lavora su \(n'=m_2-l+1\le n\) elementi e la seconda su \(n'=n\) elementi, quindi le due chiamate insieme eseguono almeno \(6n+11\) e al più \(2(7n+10)=14n+20\) passi: \(\Theta(n)\). -
Per \(n\ge 2\), le righe 2–9 vengono eseguite una volta ciascuna (\(8\) passi), più i passi di tre chiamate ricorsive su circa \(n/3\) elementi, più i \(\Theta(n)\) passi delle due chiamate di
Merge:\[ S(n)=3\,S(n/3)+\Theta(n). \]Master theorem:
-
\(a=3\), \(b=3\), \(f(n)=\Theta(n)\);
-
\(n^{\log_b a}=n^{\log_3 3}=n\);
-
\(f(n)\) e \(n^{\log_b a}\) crescono allo stesso modo \(\rightarrow\) caso 2;
-
\(f(n)=\Theta(n)=\Theta(n^{\log_3 3}\log^0 n)\) con \(k=0\);
-
\(S(n)=\Theta(n^{\log_3 3}\log n)=\Theta(n\log n)\).
MergeSort3non è asintoticamente più veloce diMergeSort: entrambi eseguono \(\Theta(n\log n)\) passi. L'albero di ricorsione diMergeSort3ha solo \(\log_3 n+1\) livelli invece di \(\log_2 n+1\), ma\[ \log_3 n=\frac{\log_2 n}{\log_2 3}, \]e i due logaritmi differiscono solo per un fattore costante, che scompare all'interno di \(\Theta\).
-
-
Per \(n=3^k\), ogni chiamata su \(n'=3^j\ge 3\) elementi li suddivide in tre parti di \(n'/3\) elementi. La prima
Mergelavora su \(2n'/3\) elementi ed esegue al più \(2n'/3-1\) confronti, la seconda lavora su \(n'\) elementi ed esegue al più \(n'-1\) confronti: al più \(\frac{5}{3}n'-2\) confronti per chiamata. Sommando sulle \(3^i\) chiamate del livello \(i\) (con \(n'=n/3^i\)), ogni livello esegue meno di \(\frac{5}{3}n\) confronti, e ci sono \(k=\log_3 n\) livelli conMerge. Nel caso peggiore:-
MergeSort3: circa \(\displaystyle\frac{5}{3}\,n\log_3 n=\frac{5}{3\log_2 3}\,n\log_2 n\approx 1.05\,n\log_2 n\) confronti; -
MergeSort: circa \(n\log_2 n\) confronti
(il valore esatto per
MergeSortè \(n\log_2 n-n+1\) quando \(n\) è una potenza di \(2\)). I livelli sono meno, ma ogni livello è più costoso: per \(n\) grande,MergeSort3esegue circa il \(5\%\) di confronti in più nel caso peggiore. Entrambi sono \(\Theta(n\log n)\). -
Esercizio 17.9: Algoritmi ricorsivi (E9)
Per sotto-array piccoli InsertionSort è in pratica più veloce di MergeSort. Consideriamo il seguente algoritmo ibrido, in cui \(k\ge 1\) è un parametro e InsertionSort\((a,l,r)\) è l'Insertion Sort applicato al sotto-array \(a[l..r]\) (esegue \(\Theta(p^2)\) passi nel caso peggiore su \(p\) elementi):
Domande
-
Applicare
HybridMergeSort\((a,1,8,2)\) all'array \(a=[7,2,9,4,3,8,6,1]\): elencare, in ordine, le chiamate delle procedureInsertionSorteMerge, con i loro risultati. -
Sia \(n=k\cdot 2^h\) per un numero naturale \(h\). Mostrare che le chiamate di
InsertionSortvengono eseguite su \(n/k\) sotto-array di esattamente \(k\) elementi, e che eseguono \(\Theta(nk)\) passi in totale nel caso peggiore. -
Mostrare che le chiamate di
Mergeeseguono \(\Theta\big(n\log_2(n/k)\big)\) passi in totale. -
Concludere che il numero di passi di
HybridMergeSortnel caso peggiore è\[ S(n)=\Theta\big(nk+n\log(n/k)\big), \]e discutere i casi estremi \(k=1\) e \(k=n\).
-
Qual è il massimo ordine di crescita di \(k\), in funzione di \(n\), per cui
HybridMergeSortesegue ancora \(\Theta(n\log n)\) passi?
Soluzione
-
Con \(k=2\), i sotto-array con al più \(2\) elementi vengono ordinati da
InsertionSort. La chiamata su \(a[1..8]\) (\(m=4\)) chiama \(a[1..4]\) (\(m=2\)) e \(a[5..8]\) (\(m=6\)), che chiamano i sotto-array di due elementi. In ordine:chiamata sotto-array risultato InsertionSort\((a,1,2)\)\([7,2]\) \([2,7]\) InsertionSort\((a,3,4)\)\([9,4]\) \([4,9]\) Merge\((a,1,2,4)\)\([2,7]\), \([4,9]\) \([2,4,7,9]\) InsertionSort\((a,5,6)\)\([3,8]\) \([3,8]\) InsertionSort\((a,7,8)\)\([6,1]\) \([1,6]\) Merge\((a,5,6,8)\)\([3,8]\), \([1,6]\) \([1,3,6,8]\) Merge\((a,1,4,8)\)\([2,4,7,9]\), \([1,3,6,8]\) \([1,2,3,4,6,7,8,9]\) Ci sono \(4=n/k\) chiamate di
InsertionSorte \(3\) chiamate diMerge, contro le \(7\) chiamate diMergediMergeSortsu \(8\) elementi. -
Sia \(n=k\cdot 2^h\). Una chiamata su \(k\cdot 2^j\) elementi con \(j\ge 1\) ha più di \(k\) elementi, quindi suddivide il sotto-array in due metà di \(k\cdot 2^{j-1}\) elementi ciascuna. Quindi l'albero di ricorsione ha livelli \(0,1,\dots,h\): il livello \(i\) contiene \(2^i\) chiamate su \(n/2^i=k\cdot 2^{h-i}\) elementi, e le chiamate del livello \(h\), su esattamente \(k\) elementi, sono le foglie, che chiamano
InsertionSort. Ci sono \(2^h=n/k\) foglie.Nel caso peggiore
InsertionSortsu \(k\) elementi esegue \(\frac{3k^2+9k-8}{2}=\Theta(k^2)\) passi (si veda il capitolo «Tempo di esecuzione»). Questo accade su ogni foglia contemporaneamente, per esempio se l'intero array è ordinato in ordine decrescente (allora ogni sotto-array delle foglie è in ordine decrescente). In totale:\[ \frac{n}{k}\cdot\Theta(k^2)=\Theta(nk). \] -
Le chiamate di
Mergesono eseguite dai nodi interni, cioè ai livelli \(0,1,\dots,h-1\). Al livello \(i\) ci sono \(2^i\) chiamate diMerge, ciascuna su \(n/2^i\) elementi. PoichéMergesu \(n'\) elementi esegue tra \(6n'+11\) e \(7n'+10\) passi, le chiamate al livello \(i\) eseguono tra\[ 2^i\Big(6\frac{n}{2^i}+11\Big)\ge 6n \qquad\text{e}\qquad 2^i\Big(7\frac{n}{2^i}+10\Big)=7n+10\cdot 2^i\le 17n \]passi (poiché \(2^i\le 2^h=n/k\le n\)). Ogni livello costa \(\Theta(n)\) e ci sono \(h=\log_2(n/k)\) livelli con
Merge, quindi il totale è\[ \Theta\big(n\log_2(n/k)\big). \] -
I passi di
HybridMergeSortsono quelli delle foglie (domanda 2), più quelli dei nodi interni (domanda 3), più un numero costante di passi (righe 2–8) per ciascuna delle \(2^{h+1}-1<2n\) chiamate, che è \(O(n)\) e viene assorbito dagli altri termini. Nel caso peggiore,\[ S(n)=\Theta(nk)+\Theta\big(n\log(n/k)\big)=\Theta\big(nk+n\log(n/k)\big). \]-
\(k=1\): le foglie contengono un elemento,
InsertionSortnon fa nulla di significativo e l'algoritmo èMergeSort: \(S(n)=\Theta(n+n\log n)=\Theta(n\log n)\). -
\(k=n\): c'è una sola foglia, l'intero array viene ordinato da
InsertionSorte non c'è alcuna fusione: \(S(n)=\Theta(n^2+n\log 1)=\Theta(n^2)\).
-
-
Poiché \(\log(n/k)\le \log n\), si ha sempre \(n\log(n/k)=O(n\log n)\). Quindi \(S(n)=\Theta(n\log n)\) se e solo se, inoltre, \(nk=O(n\log n)\), cioè
\[ k=O(\log n). \]Infatti, se \(k=\Theta(\log n)\), allora \(nk=\Theta(n\log n)\) e \(n\log(n/k)=n\log n-n\log k\le n\log n\), quindi \(S(n)=\Theta(n\log n)\). Se \(k\) cresce più velocemente di \(\log n\) (per esempio \(k=\sqrt n\)), il termine \(nk\) domina e \(S(n)\) cresce più velocemente di \(n\log n\).
Quindi il massimo ordine di crescita ammissibile è \(k=\Theta(\log n)\). In pratica, \(k\) si sceglie sperimentalmente (valori tipici sono tra \(8\) e \(32\)), come la dimensione massima per cui, sul calcolatore dato, l'Insertion Sort è più veloce del Merge Sort.
Esercizio 17.10: Algoritmi ricorsivi (E10)
Consideriamo il seguente array ordinato di \(n=10\) elementi:
e l'algoritmo RecursiveBinarySearch\((a,left,right,m)\), con prima chiamata RecursiveBinarySearch\((a,1,10,m)\) e indice centrale
Domande
-
Applicare l'algoritmo di ricerca binaria ricorsiva per i seguenti valori di \(m\):
-
\(m = 21\)
-
\(m = 1\)
-
\(m = 34\)
-
\(m = 15\)
-
\(m = 40\)
Per ogni ricerca, riportare una riga per ogni chiamata, indicando i valori di \(left\), \(right\), \(mid\), \(a[mid]\) e l'azione eseguita, e dare il valore restituito.
-
-
Disegnare l'albero delle chiamate per \(m=15\), indicando i valori restituiti da ogni chiamata.
-
Per ogni ricerca, contare il numero di chiamate su un intervallo non vuoto e il numero totale di chiamate. Confrontare questi numeri con il limite \(\lfloor \log_2 n \rfloor + 1\) sul numero di chiamate su un intervallo non vuoto.
Soluzione
-
-
Ricerca di \(m = 21\), prima chiamata
RecursiveBinarySearch\((a,1,10,21)\):chiamata \(left\) \(right\) \(mid\) \(a[mid]\) azione \(1\) 1 10 5 13 \(a[mid] < m\) \(\Rightarrow\) chiama \(left=6\), \(right=10\) \(2\) 6 10 8 25 \(a[mid] > m\) \(\Rightarrow\) chiama \(left=6\), \(right=7\) \(3\) 6 7 6 17 \(a[mid] < m\) \(\Rightarrow\) chiama \(left=7\), \(right=7\) \(4\) 7 7 7 21 \(a[mid] = m\) \(\Rightarrow\) Trovato! Ritorna \(7\) Il valore \(7\) viene restituito lungo le chiamate: l'elemento 21 viene trovato all'indice 7.
-
Ricerca di \(m = 1\), prima chiamata
RecursiveBinarySearch\((a,1,10,1)\):chiamata \(left\) \(right\) \(mid\) \(a[mid]\) azione \(1\) 1 10 5 13 \(a[mid] > m\) \(\Rightarrow\) chiama \(left=1\), \(right=4\) \(2\) 1 4 2 4 \(a[mid] > m\) \(\Rightarrow\) chiama \(left=1\), \(right=1\) \(3\) 1 1 1 1 \(a[mid] = m\) \(\Rightarrow\) Trovato! Ritorna \(1\) L'elemento 1 viene trovato all'indice 1.
-
Ricerca di \(m = 34\), prima chiamata
RecursiveBinarySearch\((a,1,10,34)\):chiamata \(left\) \(right\) \(mid\) \(a[mid]\) azione \(1\) 1 10 5 13 \(a[mid] < m\) \(\Rightarrow\) chiama \(left=6\), \(right=10\) \(2\) 6 10 8 25 \(a[mid] < m\) \(\Rightarrow\) chiama \(left=9\), \(right=10\) \(3\) 9 10 9 30 \(a[mid] < m\) \(\Rightarrow\) chiama \(left=10\), \(right=10\) \(4\) 10 10 10 34 \(a[mid] = m\) \(\Rightarrow\) Trovato! Ritorna \(10\) L'elemento 34 viene trovato all'indice 10.
-
Ricerca di \(m = 15\), prima chiamata
RecursiveBinarySearch\((a,1,10,15)\):chiamata \(left\) \(right\) \(mid\) \(a[mid]\) azione \(1\) 1 10 5 13 \(a[mid] < m\) \(\Rightarrow\) chiama \(left=6\), \(right=10\) \(2\) 6 10 8 25 \(a[mid] > m\) \(\Rightarrow\) chiama \(left=6\), \(right=7\) \(3\) 6 7 6 17 \(a[mid] > m\) \(\Rightarrow\) chiama \(left=6\), \(right=5\) \(4\) 6 5 - - \(left > right\) \(\Rightarrow\) Non trovato! Ritorna \(-1\) L'elemento 15 non è nell'array (restituisce \(-1\)).
-
Ricerca di \(m = 40\), prima chiamata
RecursiveBinarySearch\((a,1,10,40)\):chiamata \(left\) \(right\) \(mid\) \(a[mid]\) azione \(1\) 1 10 5 13 \(a[mid] < m\) \(\Rightarrow\) chiama \(left=6\), \(right=10\) \(2\) 6 10 8 25 \(a[mid] < m\) \(\Rightarrow\) chiama \(left=9\), \(right=10\) \(3\) 9 10 9 30 \(a[mid] < m\) \(\Rightarrow\) chiama \(left=10\), \(right=10\) \(4\) 10 10 10 34 \(a[mid] < m\) \(\Rightarrow\) chiama \(left=11\), \(right=10\) \(5\) 11 10 - - \(left > right\) \(\Rightarrow\) Non trovato! Ritorna \(-1\) L'elemento 40 non è nell'array (restituisce \(-1\)).
-
-
Ogni chiamata effettua al più una chiamata ricorsiva, quindi l'albero delle chiamate è un cammino. Per \(m=15\) (frecce nere: chiamate ricorsive; frecce blu: valori restituiti):
L'ultima chiamata è sull'intervallo vuoto \(left=6\), \(right=5\) e restituisce \(-1\); ogni altra chiamata restituisce, invariato, il valore restituito dalla sua chiamata ricorsiva.
-
Per \(n=10\) si ha \(\lfloor \log_2 10 \rfloor + 1 = 3+1 = 4\), poiché \(2^3 = 8 \le 10 < 16 = 2^4\).
\(m\) chiamate su un intervallo non vuoto chiamate totali 21 4 4 1 3 3 34 4 4 15 3 4 40 4 5 In ogni ricerca il numero di chiamate su un intervallo non vuoto è al più \(4\), come previsto dal limite. Per \(m=40>a[10]\) l'algoritmo prosegue sempre nella metà destra, il limite \(4\) viene raggiunto, e segue ancora una chiamata sull'intervallo vuoto: il numero totale di chiamate è \(\lfloor \log_2 10 \rfloor + 2 = 5\), che è il massimo possibile per \(n=10\).
Esercizio 17.11: Algoritmi ricorsivi (E11)
Consideriamo l'algoritmo RecursiveBinarySearch\((a,left,right,m)\) applicato a un array ordinato \(a[1..n]\), con prima chiamata RecursiveBinarySearch\((a,1,n,m)\). Il suo numero di passi nel caso peggiore \(S(n)\) soddisfa l'equazione di ricorrenza richiamata nell'introduzione agli esercizi.
Domande
-
Determinare il massimo numero di chiamate su un intervallo non vuoto e il massimo numero totale di chiamate (compresa l'ultima chiamata su un intervallo vuoto) per un array di \(n = 1000\) elementi. Dare un esempio di valore \(m\) per cui il massimo viene raggiunto, se \(a[i] = 2i\) per \(i=1,\dots,1000\).
-
Rispondere alle stesse domande per \(n = 10^6\).
-
Calcolare \(S(1000)\) e \(S(10^6)\).
-
Determinare il più piccolo \(n\) per cui, nel caso peggiore, l'algoritmo effettua \(21\) chiamate su un intervallo non vuoto.
-
Qual è il massimo numero di chiamate contemporaneamente presenti nella pila delle chiamate per \(n=10^6\)? Confrontare con il numero di elementi esaminati da una ricerca lineare nel caso peggiore.
Soluzione
-
Il massimo numero di chiamate su un intervallo non vuoto è \(\lfloor \log_2 n \rfloor + 1\), e il massimo numero totale di chiamate è \(\lfloor \log_2 n \rfloor + 2\). Per \(n = 1000\):
\[ 2^9 = 512 \le 1000 < 1024 = 2^{10} \quad\Longrightarrow\quad \lfloor \log_2 1000 \rfloor = 9 . \]Quindi:
-
massimo numero di chiamate su un intervallo non vuoto: \(9+1 = 10\);
-
massimo numero totale di chiamate: \(9+2 = 11\).
Il massimo viene raggiunto, per esempio, da ogni \(m > a[1000] = 2000\), ad esempio \(m = 2001\): l'algoritmo prosegue sempre nella metà destra, le cui dimensioni sono
\[ 1000,\ 500,\ 250,\ 125,\ 62,\ 31,\ 15,\ 7,\ 3,\ 1,\ 0, \]cioè \(10\) intervalli non vuoti seguiti da quello vuoto.
-
-
Per \(n = 10^6\):
\[ 2^{19} = 524\,288 \le 10^6 < 1\,048\,576 = 2^{20} \quad\Longrightarrow\quad \lfloor \log_2 10^6 \rfloor = 19 . \]Quindi:
-
massimo numero di chiamate su un intervallo non vuoto: \(19+1 = 20\);
-
massimo numero totale di chiamate: \(19+2 = 21\).
Anche qui, il massimo viene raggiunto da ogni \(m > a[n]\).
-
-
Per espansione, \(S(n) = 5\big(\lfloor \log_2 n \rfloor + 1\big) + 2\) per \(n \ge 1\). Quindi
\[ S(1000) = 5\cdot 10 + 2 = 52, \qquad S(10^6) = 5 \cdot 20 + 2 = 102 . \]Moltiplicare per \(1000\) la dimensione dell'array fa solo raddoppiare (approssimativamente) il numero di passi.
-
Serve \(\lfloor \log_2 n \rfloor + 1 = 21\), cioè \(\lfloor \log_2 n \rfloor = 20\), cioè
\[ 2^{20} \le n < 2^{21}. \]Il più piccolo \(n\) di questo tipo è \(n = 2^{20} = 1\,048\,576\). Infatti, per \(n = 2^{20}-1\) si ha \(\lfloor \log_2 n \rfloor = 19\) e solo \(20\) chiamate su un intervallo non vuoto.
-
Nessuna chiamata termina prima che sia terminata la sua chiamata ricorsiva; quindi, quando è in esecuzione la chiamata più profonda, tutte le chiamate del cammino sono nella pila delle chiamate. Per \(n = 10^6\) ci sono al più \(21\) chiamate contemporaneamente nella pila delle chiamate (memoria aggiuntiva \(\Theta(\log n)\)).
Una ricerca lineare esamina, nel caso peggiore, tutti i \(10^6\) elementi dell'array, mentre la ricerca binaria ricorsiva ne esamina al più \(20\) (uno per ogni chiamata su un intervallo non vuoto).
Esercizio 17.12: Algoritmi ricorsivi (E12)
Consideriamo il seguente algoritmo di ricerca ternaria, che suddivide l'intervallo \(a[left..right]\) in tre parti usando due indici \(m_1\) e \(m_2\).
Domande
-
Sia \(n = right-left+1 \ge 1\). Mostrare che ciascuno dei tre sotto-intervalli su cui si può effettuare una chiamata ricorsiva ha al più \(\lfloor n/3 \rfloor\) elementi, e scrivere un'equazione di ricorrenza per il numero di passi nel caso peggiore \(S(n)\).
-
Risolvere l'equazione di ricorrenza \(S(n) = S(n/3) + c\) con il Master theorem.
-
Mostrare che il numero di chiamate su un intervallo non vuoto è al più \(\lfloor \log_3 n \rfloor + 1\). Calcolare questo limite per \(n = 10^6\) e confrontarlo con il limite corrispondente per la ricerca binaria ricorsiva.
-
Ogni chiamata della ricerca binaria ricorsiva confronta \(m\) con elementi dell'array al più \(2\) volte (\(a[mid]=m\) e \(a[mid]<m\)), mentre ogni chiamata della ricerca ternaria lo fa al più \(4\) volte. Confrontare il massimo numero di confronti dei due algoritmi per \(n=10^6\) e per \(n \to \infty\). La ricerca ternaria è più veloce della ricerca binaria?
Soluzione
-
Siano \(n = right-left+1 \ge 1\) e \(n-1 = right-left = 3d+s\) con \(d = \lfloor (n-1)/3 \rfloor\) e \(s \in \{0,1,2\}\). I tre sotto-intervalli hanno le seguenti dimensioni:
-
\(a[left..m_1-1]\): \(m_1 - left = d\) elementi;
-
\(a[m_2+1..right]\): \(right - m_2 = d\) elementi;
-
\(a[m_1+1..m_2-1]\): \(m_2 - m_1 - 1 = (n-1) - 2d - 1 = d + s - 1\) elementi (se questo numero è negativo, l'intervallo è vuoto).
Poiché \(d = \lfloor (n-1)/3 \rfloor \le \lfloor n/3 \rfloor\), le prime due dimensioni sono al più \(\lfloor n/3 \rfloor\). Per l'intervallo centrale:
-
se \(s \in \{0,1\}\), allora \(d+s-1 \le d \le \lfloor n/3 \rfloor\);
-
se \(s = 2\), allora \(n = 3d+3\), quindi \(\lfloor n/3 \rfloor = d+1 = d+s-1\).
Quindi ogni sotto-intervallo ha al più \(\lfloor n/3 \rfloor\) elementi. Per esempio, per \(n = 9\): \(d=2\), \(s=2\), e i tre sotto-intervalli hanno \(2\), \(3\), \(2\) elementi, con \(\lfloor 9/3 \rfloor = 3\).
Ogni chiamata esegue un numero costante di passi oltre alla chiamata ricorsiva, e effettua al più una chiamata ricorsiva. Quindi, il numero di passi nel caso peggiore soddisfa
\[ S(n) = S\big(\lfloor n/3 \rfloor\big) + c \qquad (n \ge 1), \qquad S(0) = c_0, \]per certe costanti \(c, c_0 > 0\).
-
-
Applichiamo il Master theorem a \(S(n) = S(n/3) + c\):
-
Identificare: \(a = 1\), \(b = 3\), \(f(n) = c\).
-
Calcolare: \(n^{\log_b a} = n^{\log_3 1} = n^0 = 1\).
-
Confrontare: \(f(n) = c = \Theta(1) = \Theta\big(n^{\log_3 1}\log^0 n\big)\).
-
Verificare: vale il caso 2 del Master theorem con \(k=0\).
-
Concludere:
\[ S(n) = \Theta\big(n^{\log_3 1}\log^{1} n\big) = \Theta(\log n). \]
La ricerca ternaria ha lo stesso ordine di crescita della ricerca binaria: la base del logaritmo è irrilevante all'interno di \(\Theta\), poiché \(\log_3 n = \log_2 n / \log_2 3\).
-
-
Sia \(n_i\) la dimensione dell'intervallo della \(i\)-esima chiamata (\(n_1=n\)). Per la domanda 1, \(n_{i+1} \le \lfloor n_i/3 \rfloor\), e per induzione \(n_i \le \lfloor n/3^{i-1} \rfloor\) (usando \(\lfloor \lfloor x/3^{i-1} \rfloor /3 \rfloor = \lfloor x/3^{i} \rfloor\)). Se la \(i\)-esima chiamata è su un intervallo non vuoto, allora \(n_i \ge 1\), quindi \(3^{i-1} \le n\), cioè \(i - 1 \le \log_3 n\) e, poiché \(i-1\) è un intero,
\[ i \le \lfloor \log_3 n \rfloor + 1 . \]Per \(n = 10^6\): \(3^{12} = 531\,441 \le 10^6 < 1\,594\,323 = 3^{13}\), quindi \(\lfloor \log_3 10^6 \rfloor = 12\) e la ricerca ternaria effettua al più
\[ 12 + 1 = 13 \]chiamate su un intervallo non vuoto (e questo valore viene raggiunto per \(n=10^6\)). La ricerca binaria ricorsiva effettua al più \(\lfloor \log_2 10^6 \rfloor + 1 = 19+1 = 20\) di tali chiamate. La ricerca ternaria effettua meno chiamate.
-
Massimo numero di confronti con elementi dell'array per \(n=10^6\):
\[ \text{ricerca binaria: } 2 \cdot 20 = 40, \qquad \text{ricerca ternaria: } 4 \cdot 13 = 52 . \]Per \(n \to \infty\) i due limiti si comportano come \(2\log_2 n\) e \(4 \log_3 n\), e
\[ \lim_{n\to\infty} \frac{4\log_3 n}{2 \log_2 n} = \frac{4}{2\log_2 3} = \frac{2}{\log_2 3} \approx 1.26 . \]Sebbene la ricerca ternaria effettui meno chiamate, ogni chiamata è più costosa, e in totale esegue circa il \(26\%\) di confronti in più della ricerca binaria nel caso peggiore. Entrambi gli algoritmi sono \(\Theta(\log n)\): la ricerca ternaria non è asintoticamente più veloce, e in pratica è leggermente più lenta.
Esercizio 17.13: Algoritmi ricorsivi (E13)
Consideriamo l'array ordinato con duplicati
e il seguente algoritmo, la cui prima chiamata è FirstOccurrence\((a,1,n,m)\).
Domande
-
Applicare
RecursiveBinarySearch\((a,1,10,m)\) per \(m=4\) e \(m=9\). Restituisce la prima occorrenza di \(m\)? -
Applicare
FirstOccurrence\((a,1,10,m)\) per \(m = 4\), \(m = 9\) e \(m = 5\). Per ogni ricerca, riportare una riga per ogni chiamata e il valore restituito da ciascuna chiamata. -
Dimostrare per induzione forte su \(k = right-left+1\) che
FirstOccurrenceè corretto. -
Scrivere l'equazione di ricorrenza per il numero di passi nel caso peggiore \(S(n)\) e risolverla.
-
Uno studente propone un algoritmo diverso: trovare un'occorrenza qualsiasi \(i\) di \(m\) con
RecursiveBinarySearch, poi spostarsi a sinistra finché \(i > 1\) e \(a[i-1] = m\). Determinare il suo numero di passi nel caso peggiore. -
FirstOccurrenceè ricorsivo in coda?
Soluzione
-
Ricerca di \(m=4\) con
RecursiveBinarySearch\((a,1,10,4)\):chiamata \(left\) \(right\) \(mid\) \(a[mid]\) azione \(1\) 1 10 5 4 \(a[mid] = m\) \(\Rightarrow\) Trovato! Ritorna \(5\) Ricerca di \(m=9\) con
RecursiveBinarySearch\((a,1,10,9)\):chiamata \(left\) \(right\) \(mid\) \(a[mid]\) azione \(1\) 1 10 5 4 \(a[mid] < m\) \(\Rightarrow\) chiama \(left=6\), \(right=10\) \(2\) 6 10 8 9 \(a[mid] = m\) \(\Rightarrow\) Trovato! Ritorna \(8\) Gli indici restituiti sono \(5\) e \(8\), mentre le prime occorrenze sono all'indice \(2\) (per \(m=4\)) e \(7\) (per \(m=9\)).
RecursiveBinarySearchrestituisce un'occorrenza, non necessariamente la prima. -
Ricerca di \(m=4\) con
FirstOccurrence\((a,1,10,4)\):chiamata \(left\) \(right\) \(mid\) \(a[mid]\) azione restituisce \(1\) 1 10 5 4 \(a[mid] = m\) \(\Rightarrow\) \(r\) := chiama \(1\), \(4\) \(r=2\) \(\Rightarrow\) \(2\) \(2\) 1 4 2 4 \(a[mid] = m\) \(\Rightarrow\) \(r\) := chiama \(1\), \(1\) \(r=-1\) \(\Rightarrow\) \(mid = 2\) \(3\) 1 1 1 2 \(a[mid] < m\) \(\Rightarrow\) chiama \(2\), \(1\) \(-1\) \(4\) 2 1 - - \(left > right\) \(-1\) I valori restituiti si calcolano dal basso verso l'alto: la chiamata 4 restituisce \(-1\), la chiamata 3 restituisce \(-1\), la chiamata 2 riceve \(r=-1\) e restituisce il proprio \(mid=2\), la chiamata 1 riceve \(r = 2 \neq -1\) e restituisce \(2\). La prima occorrenza di \(4\) è all'indice \(2\).
Ricerca di \(m=9\) con
FirstOccurrence\((a,1,10,9)\):chiamata \(left\) \(right\) \(mid\) \(a[mid]\) azione restituisce \(1\) 1 10 5 4 \(a[mid] < m\) \(\Rightarrow\) chiama \(6\), \(10\) \(7\) \(2\) 6 10 8 9 \(a[mid] = m\) \(\Rightarrow\) \(r\) := chiama \(6\), \(7\) \(r=7\) \(\Rightarrow\) \(7\) \(3\) 6 7 6 7 \(a[mid] < m\) \(\Rightarrow\) chiama \(7\), \(7\) \(7\) \(4\) 7 7 7 9 \(a[mid] = m\) \(\Rightarrow\) \(r\) := chiama \(7\), \(6\) \(r=-1\) \(\Rightarrow\) \(mid=7\) \(5\) 7 6 - - \(left > right\) \(-1\) La prima occorrenza di \(9\) è all'indice \(7\).
Ricerca di \(m=5\) con
FirstOccurrence\((a,1,10,5)\):chiamata \(left\) \(right\) \(mid\) \(a[mid]\) azione restituisce \(1\) 1 10 5 4 \(a[mid] < m\) \(\Rightarrow\) chiama \(6\), \(10\) \(-1\) \(2\) 6 10 8 9 \(a[mid] > m\) \(\Rightarrow\) chiama \(6\), \(7\) \(-1\) \(3\) 6 7 6 7 \(a[mid] > m\) \(\Rightarrow\) chiama \(6\), \(5\) \(-1\) \(4\) 6 5 - - \(left > right\) \(-1\) L'elemento \(5\) non è nell'array (restituisce \(-1\)).
-
Dimostriamo che, per \(1 \le left \le right+1 \le n+1\),
FirstOccurrence\((a,left,right,m)\) restituisce il più piccolo indice \(i\in\{left,\dots,right\}\) con \(a[i]=m\), oppure \(-1\) se non ce ne sono. Induzione forte su \(k = right-left+1\).Caso base \(k=0\): l'intervallo è vuoto, \(m\) non compare, e l'algoritmo restituisce \(-1\).
Passo induttivo \(k \ge 1\): supponiamo l'enunciato vero per tutte le dimensioni \(k'<k\). Si ha \(left \le mid \le right\), e ogni chiamata ricorsiva è su un intervallo di dimensione \(right-mid\) oppure \(mid-left\), entrambe minori di \(k\).
-
Se \(a[mid] < m\): tutti gli elementi \(a[left..mid]\) sono \(< m\) (array ordinato), quindi le occorrenze di \(m\) in \(a[left..right]\) sono esattamente quelle in \(a[mid+1..right]\), e la più piccola viene restituita dalla chiamata ricorsiva (ipotesi induttiva).
-
Se \(a[mid] > m\): tutti gli elementi \(a[mid..right]\) sono \(> m\), quindi le occorrenze sono esattamente quelle in \(a[left..mid-1]\), e concludiamo come sopra.
-
Se \(a[mid] = m\): \(mid\) è un'occorrenza, quindi la prima occorrenza è in \(a[left..mid-1]\) oppure è \(mid\). Per l'ipotesi induttiva, \(r\) è la prima occorrenza in \(a[left..mid-1]\), oppure \(-1\) se non ce ne sono. Nel primo caso l'algoritmo restituisce \(r\) (riga 16), nel secondo caso restituisce \(mid\) (riga 14). Entrambi sono corretti.
-
-
Sia \(n = right-left+1\). Ogni chiamata esegue un numero costante di passi (al più \(7\): righe 2, 5, 6, 9, 12, 13 e 14 oppure 16) e effettua al più una chiamata ricorsiva, su un intervallo di \(\lfloor (n-1)/2 \rfloor\) oppure \(\lfloor n/2 \rfloor\) elementi. Quindi
\[ S(n) = S\big(\lfloor n/2 \rfloor\big) + c \quad (n\ge 1), \qquad S(0) = c_0 . \]Per il caso 2 del Master theorem (\(a=1\), \(b=2\), \(f(n)=c=\Theta(n^{\log_2 1}\log^0 n)\), \(k=0\)):
\[ S(n) = \Theta(\log n). \]Come per
RecursiveBinarySearch, ci sono al più \(\lfloor \log_2 n\rfloor+1\) chiamate su un intervallo non vuoto (qui \(\lfloor \log_2 10\rfloor+1=4\); la ricerca di \(m=9\) lo raggiunge). Si noti cheFirstOccurrencenon si ferma mai quando trova \(m\): la ricorsione termina sempre con una chiamata su un intervallo vuoto. -
Consideriamo l'array \(a[1..n]\) con tutti gli elementi uguali a \(m\).
RecursiveBinarySearchrestituisce immediatamente \(i = mid = 1 + \lfloor (n-1)/2 \rfloor\). Poi l'algoritmo si sposta a sinistra \(i-1 = \lfloor (n-1)/2 \rfloor\) volte, fino a \(i = 1\). Ogni spostamento costa un numero costante di passi, quindi il numero di passi nel caso peggiore è\[ \Theta(\log n) + \Theta(n) = \Theta(n). \]L'algoritmo proposto è lineare nel caso peggiore, mentre
FirstOccurrenceè logaritmico. -
No. Le chiamate ricorsive delle righe 7 e 10 sono chiamate in coda, ma la chiamata della riga 12 non lo è: dopo che termina, l'algoritmo deve ancora verificare \(r = -1\) (riga 13). L'algoritmo può essere reso ricorsivo in coda passando il miglior candidato trovato finora come parametro aggiuntivo \(best\) (inizialmente \(-1\)): quando \(a[mid] = m\), chiama
FirstOccurrence\((a,left,mid-1,m,mid)\), e l'intervallo vuoto restituisce \(best\) invece di \(-1\).
Esercizio 17.14: Algoritmi ricorsivi (E14)
Consideriamo l'array di \(n = 5\) numeri interi
e l'algoritmo BruteForceMaximumSubarray\((a,n)\), che esamina tutte le coppie \((l,r)\) con \(1 \le l \le r \le n\) nell'ordine \((1,1), (1,2), \dots, (1,n), (2,2), \dots, (n,n)\), partendo da \(best = a[1]\) e aggiornando \(best\), \(bl\), \(br\) solo quando \(sum > best\). Il suo numero di passi su un array di \(n\) elementi, in funzione del numero \(U\) di aggiornamenti di \(best\), è richiamato nell'introduzione agli esercizi.
Domande
-
Quanti sotto-array ha \(a\)?
-
Applicare l'algoritmo di forza bruta ad \(a\). Per ogni coppia \((l,r)\), riportare la somma di \(a[l..r]\) e se \(best\) viene aggiornato. Quale terna \((bl,br,best)\) viene restituita?
-
Calcolare il numero esatto di passi eseguiti dall'algoritmo su \(a\).
-
Per \(n = 1000\), dare un limite inferiore e un limite superiore al numero di passi eseguiti dall'algoritmo su un qualunque input di dimensione \(n\).
Soluzione
-
I sotto-array sono le coppie \((l,r)\) con \(1 \le l \le r \le 5\):
\[ \frac{n(n+1)}{2} = \frac{5 \cdot 6}{2} = 15 . \] -
Inizialmente \(best = a[1] = 2\), \(bl = br = 1\).
\(l\) \(r\) \(sum\) di \(a[l..r]\) azione 1 1 \(2\) \(sum \le best = 2\) 1 2 \(-2\) 1 3 \(1\) 1 4 \(0\) 1 5 \(2\) \(sum \le best = 2\) 2 2 \(-4\) 2 3 \(-1\) 2 4 \(-2\) 2 5 \(0\) 3 3 \(3\) \(sum > best\) \(\Rightarrow\) \(best=3\), \(bl=3\), \(br=3\) 3 4 \(2\) 3 5 \(4\) \(sum > best\) \(\Rightarrow\) \(best=4\), \(bl=3\), \(br=5\) 4 4 \(-1\) 4 5 \(1\) 5 5 \(2\) L'algoritmo restituisce \((3,5,4)\): il sotto-array di somma massima è \(a[3..5] = [3,-1,2]\) con somma \(4\). Si noti che l'intero array ha somma \(2\) e il sotto-array \(a[1..1]\) ha somma \(2\): poiché il confronto è stretto, non causano mai un aggiornamento.
-
Ci sono \(U = 2\) aggiornamenti (coppie \((3,3)\) e \((3,5)\)). Con \(n = 5\):
\[ 2\cdot 5^2 + 7 \cdot 5 + 6 + 3 \cdot 2 = 50 + 35 + 6 + 6 = 97 \text{ passi.} \] -
Il numero di aggiornamenti soddisfa \(0 \le U \le n(n+1)/2\). Quindi il numero di passi è almeno
\[ 2\cdot 1000^2 + 7\cdot 1000 + 6 = 2\,007\,006 \]e al più
\[ 2\,007\,006 + 3 \cdot \frac{1000 \cdot 1001}{2} = 2\,007\,006 + 1\,501\,500 = 3\,508\,506 . \]In entrambi i casi il numero di passi è circa \(2\)–\(3.5\) milioni: l'algoritmo è \(\Theta(n^2)\) su ogni input, non solo nel caso peggiore.
Esercizio 17.15: Algoritmi ricorsivi (E15)
Consideriamo l'array di \(n=8\) numeri interi
e l'algoritmo FindMaxCrossingSubarray\((a,l,mid,r)\), che scorre \(i = mid, mid-1, \dots, l\) calcolando le somme di \(a[i..mid]\), e \(j = mid+1, \dots, r\) calcolando le somme di \(a[mid+1..j]\), tenendo la migliore di ciascuna (confronto stretto, valori iniziali \(-\infty\)).
Domande
-
Calcolare
FindMaxCrossingSubarray\((a,1,4,8)\) passo per passo. -
Calcolare
FindMaxCrossingSubarray\((a,5,6,8)\) passo per passo. -
Il sotto-array trovato nella domanda 1 è un sotto-array di somma massima di \(a\)? Giustificare la risposta.
-
Quante iterazioni eseguono i cicli nelle domande 1 e 2? Perché il sotto-array restituito nella domanda 1 è obbligato a contenere \(a[4]\) e \(a[5]\), anche se \(a[5] = -6\)?
Soluzione
-
\(l = 1\), \(mid = 4\), \(r = 8\).
Ciclo sinistro (\(i\) da \(4\) a \(1\) in discesa):
\(i\) \(a[i]\) \(sum\) \(leftsum\) \(maxleft\) 4 4 4 4 4 3 \(-2\) 2 4 4 2 3 5 5 2 1 \(-1\) 4 5 2 Ciclo destro (\(j\) da \(5\) a \(8\)):
\(j\) \(a[j]\) \(sum\) \(rightsum\) \(maxright\) 5 \(-6\) \(-6\) \(-6\) 5 6 1 \(-5\) \(-5\) 6 7 5 0 0 7 8 \(-3\) \(-3\) 0 7 L'algoritmo restituisce \((maxleft, maxright, leftsum+rightsum) = (2, 7, 5+0) = (2,7,5)\): il sotto-array \(a[2..7] = [3,-2,4,-6,1,5]\) con somma \(5\).
-
\(l = 5\), \(mid = 6\), \(r = 8\).
\(i\) \(a[i]\) \(sum\) \(leftsum\) \(maxleft\) 6 1 1 1 6 5 \(-6\) \(-5\) 1 6 \(j\) \(a[j]\) \(sum\) \(rightsum\) \(maxright\) 7 5 5 5 7 8 \(-3\) 2 5 7 L'algoritmo restituisce \((6, 7, 1+5) = (6,7,6)\): il sotto-array \(a[6..7] = [1,5]\) con somma \(6\).
-
No. Il sotto-array \(a[6..7]\) trovato nella domanda 2 ha somma \(6 > 5\). In effetti, \(a[6..7]\) è un sotto-array di somma massima di \(a\): il sotto-array di somma massima si trova interamente nella metà destra \(a[5..8]\) e non attraversa il punto medio \(mid = 4\).
FindMaxCrossingSubarraytrova solo il miglior sotto-array tra quelli che attraversano il punto medio. Ecco perchéFindMaximumSubarrayrisolve ricorsivamente anche le due metà e restituisce il migliore dei tre candidati: qui la metà sinistra \(a[1..4]\) dà \(a[2..4]\) con somma \(3-2+4=5\), la metà destra \(a[5..8]\) dà \(a[6..7]\) con somma \(6\), il sotto-array che attraversa il punto medio ha somma \(5\), e la risposta è \(a[6..7]\) con somma \(6\). -
Nella domanda 1 i cicli eseguono \((mid-l+1) + (r-mid) = 4+4 = 8 = r-l+1\) iterazioni; nella domanda 2 eseguono \(2+2 = 4 = r-l+1\) iterazioni. In generale,
FindMaxCrossingSubarrayesegue esattamente \(n = r-l+1\) iterazioni, quindi \(\Theta(n)\) passi.Per definizione, un sotto-array che attraversa il punto medio è \(a[i..j]\) con \(i \le mid < j\): contiene sempre sia \(a[mid]\) sia \(a[mid+1]\). Infatti, il ciclo sinistro parte da \(i = mid\) e il ciclo destro da \(j = mid+1\), e sia \(leftsum\) sia \(rightsum\) vengono aggiornati alla prima iterazione (valori iniziali \(-\infty\)). I sotto-array che non contengono \(a[5] = -6\) sono considerati dalle chiamate ricorsive sulle due metà.
Esercizio 17.16: Algoritmi ricorsivi (E16)
Consideriamo l'array di \(n = 8\) numeri interi
e l'algoritmo divide et impera FindMaximumSubarray\((a,l,r)\), con prima chiamata FindMaximumSubarray\((a,1,8)\), \(mid = \lfloor (l+r)/2 \rfloor\), e parità risolte a favore della metà sinistra, poi della metà destra.
Domande
-
Disegnare l'albero delle chiamate, indicando per ogni chiamata \([l..r]\) la terna \((i,j,s)\) che restituisce.
-
Per ogni chiamata su almeno due elementi, riportare le terne restituite dalle due chiamate ricorsive e da
FindMaxCrossingSubarray, e la terna restituita. -
Mostrare il calcolo di
FindMaxCrossingSubarraynella prima chiamata. -
Quante chiamate di
FindMaximumSubarrayvengono eseguite? Quante iterazioni vengono eseguite, in totale, dai cicli di tutte le chiamate diFindMaxCrossingSubarray? Confrontare con \(n \log_2 n\).
Soluzione
-
Albero delle chiamate (ogni nodo: il sotto-array \([l..r]\) e la terna restituita \((i,j,s)\)):
-
Risultati delle chiamate su almeno due elementi (candidati sinistro, destro e che attraversa il punto medio, e terna restituita):
chiamata \(mid\) sinistra destra attraversante restituita \([1..8]\) 4 \((2,4,6)\) \((6,7,8)\) \((2,7,9)\) \((2,7,9)\) \([1..4]\) 2 \((2,2,4)\) \((4,4,3)\) \((2,4,6)\) \((2,4,6)\) \([5..8]\) 6 \((6,6,2)\) \((7,7,6)\) \((6,7,8)\) \((6,7,8)\) \([1..2]\) 1 \((1,1,-2)\) \((2,2,4)\) \((1,2,2)\) \((2,2,4)\) \([3..4]\) 3 \((3,3,-1)\) \((4,4,3)\) \((3,4,2)\) \((4,4,3)\) \([5..6]\) 5 \((5,5,-5)\) \((6,6,2)\) \((5,6,-3)\) \((6,6,2)\) \([7..8]\) 7 \((7,7,6)\) \((8,8,-4)\) \((7,8,2)\) \((7,7,6)\) Per esempio, nella chiamata \([1..4]\) il sotto-array che attraversa il punto medio si calcola così: ciclo sinistro \(i=2\): \(sum=4\); \(i=1\): \(sum = 2\), quindi \(leftsum = 4\), \(maxleft = 2\); ciclo destro \(j=3\): \(sum=-1\); \(j=4\): \(sum = 2\), quindi \(rightsum = 2\), \(maxright = 4\); risultato \((2,4,6)\). Nella chiamata \([5..8]\): \(leftsum = 2\) (\(maxleft = 6\)), \(rightsum = 6\) (\(maxright = 7\)), risultato \((6,7,8)\).
-
Prima chiamata: \(l=1\), \(mid=4\), \(r=8\).
\(i\) \(a[i]\) \(sum\) \(leftsum\) \(maxleft\) 4 3 3 3 4 3 \(-1\) 2 3 4 2 4 6 6 2 1 \(-2\) 4 6 2 \(j\) \(a[j]\) \(sum\) \(rightsum\) \(maxright\) 5 \(-5\) \(-5\) \(-5\) 5 6 2 \(-3\) \(-3\) 6 7 6 3 3 7 8 \(-4\) \(-1\) 3 7 Il sotto-array che attraversa il punto medio è \(a[2..7] = [4,-1,3,-5,2,6]\) con somma \(6+3 = 9\). Poiché \(9 > 6\) (sinistra) e \(9 > 8\) (destra), l'algoritmo restituisce \((2,7,9)\): il sotto-array di somma massima è \(a[2..7]\) con somma \(9\). Si noti che contiene l'elemento negativo \(a[5]=-5\).
-
L'albero delle chiamate ha \(n = 8\) foglie e \(n-1 = 7\) nodi interni: \(2n-1 = 15\) chiamate di
FindMaximumSubarray, e \(7\) chiamate diFindMaxCrossingSubarray.Una chiamata di
FindMaxCrossingSubarraysu \(a[l..r]\) esegue \(r-l+1\) iterazioni. Le chiamate di ciascun livello dell'albero ricoprono l'intero array una volta:-
livello 0: \([1..8]\) \(\Rightarrow\) \(8\) iterazioni;
-
livello 1: \([1..4]\), \([5..8]\) \(\Rightarrow\) \(4 + 4 = 8\) iterazioni;
-
livello 2: \([1..2]\), \([3..4]\), \([5..6]\), \([7..8]\) \(\Rightarrow\) \(2+2+2+2 = 8\) iterazioni.
In totale \(3 \cdot 8 = 24\) iterazioni, che è esattamente \(n\log_2 n = 8 \cdot 3 = 24\). Questo è il termine \(c\,n\log_2 n\) dell'albero di ricorsione, che dà \(S(n) = \Theta(n\log n)\).
-
Esercizio 17.17: Algoritmi ricorsivi (E17)
Consideriamo i due array
e l'algoritmo iterativo Kadane\((a,n)\): parte con \(best := a[1]\), \(bl := br := 1\), \(cur := a[1]\), \(cl := 1\) e, per \(i = 2, \dots, n\), esegue
-
se \(cur < 0\): \(cur \leftarrow a[i]\), \(cl \leftarrow i\) (ripartenza); altrimenti \(cur \leftarrow cur + a[i]\) (estensione);
-
se \(cur > best\): \(best \leftarrow cur\), \(bl \leftarrow cl\), \(br \leftarrow i\).
Restituisce \((bl, br, best)\).
Domande
-
Applicare
Kadaneall'array \(a\), riportando per ogni \(i\) l'azione eseguita e i valori di \(cur\), \(cl\), \(best\), \(bl\), \(br\). -
Applicare
Kadaneall'array \(b\), i cui elementi sono tutti negativi. -
Che cosa restituiscono su \(b\) l'algoritmo di forza bruta e l'algoritmo divide et impera
FindMaximumSubarray? (Per l'algoritmo divide et impera, riportare i tre candidati della prima chiamata.) Dimostrare che, se tutti gli elementi di un array sono negativi, ogni sotto-array di somma massima è formato da un solo elemento. -
Una variante diffusa dell'algoritmo inizializza \(cur := 0\) e \(best := 0\) e, per \(i = 1, \dots, n\), esegue \(cur \leftarrow \max\{0, cur + a[i]\}\) e \(best \leftarrow \max\{best, cur\}\). Che cosa restituisce su \(a\) e su \(b\)? Quale problema risolve?
Soluzione
-
Esecuzione su \(a = [-2, 1, -3, 4, -1, 2, 1, -5, 4]\) (riga \(i=1\): inizializzazione; le altre righe riportano i valori alla fine dell'iterazione con indice \(i\)):
\(i\) \(a[i]\) azione \(cur\) \(cl\) \(best\) \((bl,br)\) 1 \(-2\) inizializzazione \(-2\) 1 \(-2\) \((1,1)\) 2 1 ripartenza (\(cur = -2 < 0\)) 1 2 1 \((2,2)\) 3 \(-3\) estensione \(-2\) 2 1 \((2,2)\) 4 4 ripartenza (\(cur = -2 < 0\)) 4 4 4 \((4,4)\) 5 \(-1\) estensione 3 4 4 \((4,4)\) 6 2 estensione 5 4 5 \((4,6)\) 7 1 estensione 6 4 6 \((4,7)\) 8 \(-5\) estensione 1 4 6 \((4,7)\) 9 4 estensione 5 4 6 \((4,7)\) L'algoritmo restituisce \((4,7,6)\): il sotto-array di somma massima è \(a[4..7] = [4,-1,2,1]\) con somma \(6\).
-
Esecuzione su \(b = [-8, -3, -6, -2, -5, -4]\):
\(i\) \(b[i]\) azione \(cur\) \(cl\) \(best\) \((bl,br)\) 1 \(-8\) inizializzazione \(-8\) 1 \(-8\) \((1,1)\) 2 \(-3\) ripartenza \(-3\) 2 \(\mathbf{-3}\) \((2,2)\) 3 \(-6\) ripartenza \(-6\) 3 \(-3\) \((2,2)\) 4 \(-2\) ripartenza \(-2\) 4 \(\mathbf{-2}\) \((4,4)\) 5 \(-5\) ripartenza \(-5\) 5 \(-2\) \((4,4)\) 6 \(-4\) ripartenza \(-4\) 6 \(-2\) \((4,4)\) Poiché \(cur\) è sempre negativo, l'algoritmo riparte a ogni iterazione: \(cur = b[i]\), e \(best\) è semplicemente il massimo elemento visto finora. L'algoritmo restituisce \((4,4,-2)\): il sotto-array di somma massima è il singolo elemento \(b[4] = -2\).
-
L'algoritmo di forza bruta esamina tutti i sotto-array e restituisce anch'esso \((4,4,-2)\).
L'algoritmo divide et impera, nella prima chiamata \([1..6]\) con \(mid = 3\), ottiene:
-
metà sinistra \([1..3]\): \((2,2,-3)\);
-
metà destra \([4..6]\): \((4,4,-2)\);
-
sotto-array che attraversa il punto medio: ciclo sinistro \(i=3\): \(sum=-6\), \(i=2\): \(-9\), \(i=1\): \(-17\), quindi \(leftsum = -6\) (\(maxleft = 3\)); ciclo destro \(j=4\): \(sum=-2\), \(j=5\): \(-7\), \(j=6\): \(-11\), quindi \(rightsum = -2\) (\(maxright = 4\)); risultato \((3,4,-8)\).
Il migliore dei tre è \((4,4,-2)\). Tutti e tre gli algoritmi restituiscono \(b[4..4]\) con somma \(-2\).
Dimostrazione della proprietà. Siano tutti gli elementi negativi, e sia \(a[l..r]\) un sotto-array con almeno due elementi (\(l<r\)). Allora
\[ \sum_{k=l}^{r} a[k] = a[l] + \sum_{k=l+1}^{r} a[k] < a[l], \]perché \(\sum_{k=l+1}^{r} a[k]\) è una somma di numeri negativi. Quindi il sotto-array di un solo elemento \(a[l..l]\) ha somma maggiore, e \(a[l..r]\) non è un sotto-array di somma massima. Quindi ogni sotto-array di somma massima ha un elemento, ed è un elemento massimo dell'array.
-
-
La variante calcola la massima somma di un sotto-array ammettendo il sotto-array vuoto (la cui somma è \(0\)): riportare \(cur\) a \(0\) significa «iniziare un nuovo sotto-array, vuoto».
-
Su \(a\): \(cur\) assume i valori \(0, 1, 0, 4, 3, 5, 6, 1, 5\), e l'algoritmo restituisce \(best = 6\), come prima, poiché il sotto-array di somma massima ha somma positiva.
-
Su \(b\): \(cur\) viene riportato a \(0\) a ogni iterazione, e l'algoritmo restituisce \(best = 0\), che è la somma del sotto-array vuoto. Questa non è una risposta corretta al nostro problema, in cui un sotto-array deve essere non vuoto (\(l \le r\)): la risposta corretta è \(-2\).
Nell'interpretazione sull'azione, la variante risponde alla domanda «qual è il massimo profitto se possiamo anche non comprare affatto?»: se il prezzo scende ogni giorno, la scelta migliore è non comprare (profitto \(0\)). Le due versioni coincidono ogni volta che l'array contiene almeno un elemento non negativo.
-