Esercizio - due elementi più piccoli di una stringa con divide and conquer (simulazione d'esame 2023-24, problema 1)
Testo (simulazione d'esame 2023-24 di Algoritmi per l'Ingegneria, Ing. Informatica UniPD, seconda parte, problema 1, 19 punti).
Punto 1 [11 punti]. Sia una potenza di due. Si progetti e si scriva lo pseudocodice di un algoritmo divide-and-conquer per calcolare la coppia, ordinata in senso crescente, dei due elementi più piccoli di una stringa di interi positivi distinti. Ad esempio, se , l'algoritmo deve restituire la coppia . (Attenzione: per essere non banale, l'algoritmo progettato deve eseguire meno di confronti, complessità banalmente ottenibile calcolando prima il minimo di , poi eliminandolo dalla stringa e calcolando il minimo della stringa residua.)
Punto 2 [8 punti]. Si scriva e si risolva esattamente la ricorrenza relativa al numero di confronti tra gli elementi della stringa effettuati dall'algoritmo sviluppato al Punto 1.
Teoria usata: Divide and conquer - proprietà di sottostruttura e albero delle chiamateUn algoritmo divide and conquer nasce da una proprietà di sottostruttura (funzioni Divide e Conquer, base n0) con sottoistanze di taglia strettamente minore; l'albero delle chiamate ne descrive l'esecuzione, e il D&C non ha memoria (ripete le sottoistanze uguali). Esempio completo: MAX di un array, con correttezza per induzione, ricorrenza e conteggio esatto n-1 tramite il teorema sugli alberi binari pieni.Divide and conquer - proprietà di sottostruttura e albero delle chiamate →, Albero della ricorrenza e formula generale per le ricorrenzeL'albero della ricorrenza ha la forma dell'albero delle chiamate dell'istanza peggiore e T(n) è la somma dei lavori dei nodi. Per la famiglia T(n) = s(n)T(f(n)) + w(n) con base T0 per n <= n0, contando i nodi per livello con le iterate di f e il livello f*(n,n0) si ottiene la formula chiusa T(n) = somma_l [prodotto_j s(f^(j)(n))] w(f^(l)(n)) + [prodotto s] T0, verificata per induzione e applicata a esempi (merge sort, Hanoi, ricorrenze con radice).Albero della ricorrenza e formula generale per le ricorrenze →, Problemi computazionali - istanze, soluzioni, correttezza e complessitàUn problema computazionale è una relazione totale (non necessariamente univoca) tra istanze e soluzioni; un algoritmo è una funzione dalle istanze alle soluzioni e risolve il problema se la sua uscita è sempre in relazione con l'ingresso. Correttezza = coerenza con la specifica; complessità = costo in funzione della taglia, per default al caso peggiore, con le istruzioni a costo unitario scelte in modo significativo.Problemi computazionali - istanze, soluzioni, correttezza e complessità →.
Il testo avverte di dedicare tempo a capire la traccia: si chiede un algoritmo ricorsivo che restituisce i due valori più piccoli (certamente distinti, perché gli elementi lo sono), con meno di confronti. L'algoritmo banale, con due scansioni, fa confronti:
for i <- 2 to n do if A[i] < A[1] then SWAP(A[1], A[i]) // n - 1 confronti: A[1] = minimo
for i <- 3 to n do if A[i] < A[2] then SWAP(A[2], A[i]) // n - 2 confronti: A[2] = secondo minimoPunto 1: proprietà di sottostruttura e pseudocodice
Problema. = stringhe di interi positivi distinti, = coppie con ; associa ad la coppia dei due più piccoli. Taglia: . Costo unitario: il confronto tra elementi di .
Caso base (nella traccia ): , ; basta un confronto.
Divide per : e , due sottoistanze di taglia (ancora potenza di due, ). Siano la coppia restituita per e quella per (con ); per ipotesi induttiva sono i due più piccoli di e .
Osservazione chiave. I due elementi più piccoli di stanno certamente in . Infatti ogni altro elemento di è maggiore di almeno due elementi (di e di ), e ogni altro elemento di è maggiore di e di : ha almeno due elementi più piccoli, quindi non può essere tra i due minimi.
Conquer. Si distinguono due casi (nessuna uguaglianza, gli elementi sono distinti):
- se : e ;
- se : e .
Prova del caso 1. Se allora e : è minore degli altri tre candidati, quindi . Il secondo minimo è il minore dei tre candidati rimasti ; ma (per ordinata), quindi non può essere il minore e .
Prova del caso 2. Simmetrica scambiando i ruoli di e : e danno ; i candidati per il secondo sono e lo esclude, quindi .
Correttezza della proprietà (induzione sulla taglia): la base è corretta; per le sottoistanze hanno taglia e per ipotesi le loro soluzioni sono corrette; le due prove sopra dicono che il Conquer produce i due più piccoli di .
Pseudocodice.
TWO_SMALLEST(A) {n = 2^k, k >= 1, elementi distinti}
n <- |A|
[B]: if n = 2 then
if A[1] < A[2] then return (A[1], A[2]) else return (A[2], A[1])
[D]: A1 <- A[1..n/2]; A2 <- A[n/2 + 1..n]
[R]: (x1, y1) <- TWO_SMALLEST(A1); (x2, y2) <- TWO_SMALLEST(A2)
[C]: if x1 < x2 then
if x2 < y1 then return (x1, x2) else return (x1, y1) // caso 1
else
if x1 < y2 then return (x2, x1) else return (x2, y2) // caso 2Esecuzione su .
A = <3 1 7 5 4 9 6 8> -> (1, 3)
/ \
<3 1 7 5> -> (1, 3) <4 9 6 8> -> (4, 6)
/ \ / \
<3 1> -> (1,3) <7 5> -> (5,7) <4 9> -> (4,9) <6 8> -> (6,8)Al nodo radice: , caso 1; ? No, quindi si restituisce ✓. Al nodo sinistro: (da , coppia ) e (da , coppia ): , e ? No, quindi ✓.
Punto 2: la ricorrenza e la sua soluzione
Ogni istanza non di base fa due confronti nel Conquer (uno tra e , uno tra il candidato e oppure : sempre esattamente due, qualunque sia l'istanza), il Divide non ne fa. Il caso base ne fa uno. Dunque: Formula generale con , , (due sottoistanze nel Divide), (taglia), (due confronti nel Conquer). Si ha : i livelli non di base sono , e le foglie, al livello , sono . Il livello ha nodi, ciascuno con lavoro :
Verifica per induzione: base ✓; passo, per : ✓. Valori: danno confronti, contro dell'algoritmo banale: molto meglio ( contro ). Con un programma, su stringhe casuali per ogni l'uscita coincide con i due minimi e i confronti sono sempre esattamente .
(Per curiosità: con un "torneo a eliminazione", cercando il minimo con confronti e poi il minimo tra i elementi che hanno perso direttamente contro il vincitore, si arriva a confronti, meglio ancora per grande: contro per . Non era richiesto.)
import random
def due_minimi(A, c):
n = len(A)
if n == 2:
c[0] += 1
return (A[0], A[1]) if A[0] < A[1] else (A[1], A[0])
x1, y1 = due_minimi(A[:n // 2], c)
x2, y2 = due_minimi(A[n // 2:], c)
c[0] += 2 # i due confronti del Conquer
if x1 < x2:
return (x1, x2) if x2 < y1 else (x1, y1)
return (x2, x1) if x1 < y2 else (x2, y2)
for k in range(1, 11):
n = 2**k
A = random.sample(range(1, 10 * n), n); c = [0]
assert due_minimi(A, c) == tuple(sorted(A)[:2]) and c[0] == 3 * n // 2 - 2
c = [0]; print(due_minimi([3, 1, 7, 5, 4, 9, 6, 8], c), c[0]) # (1, 3) 10Versione ripasso
Testo. potenza di due, di interi positivi distinti. (1) [11 punti] D&C TWO_SMALLEST che restituisce i due più piccoli in ordine crescente con meno di confronti; (2) [8] ricorrenza esatta dei confronti e sua soluzione.
Teoria: Divide and conquer - proprietà di sottostruttura e albero delle chiamateUn algoritmo divide and conquer nasce da una proprietà di sottostruttura (funzioni Divide e Conquer, base n0) con sottoistanze di taglia strettamente minore; l'albero delle chiamate ne descrive l'esecuzione, e il D&C non ha memoria (ripete le sottoistanze uguali). Esempio completo: MAX di un array, con correttezza per induzione, ricorrenza e conteggio esatto n-1 tramite il teorema sugli alberi binari pieni.Divide and conquer - proprietà di sottostruttura e albero delle chiamate →, Albero della ricorrenza e formula generale per le ricorrenzeL'albero della ricorrenza ha la forma dell'albero delle chiamate dell'istanza peggiore e T(n) è la somma dei lavori dei nodi. Per la famiglia T(n) = s(n)T(f(n)) + w(n) con base T0 per n <= n0, contando i nodi per livello con le iterate di f e il livello f*(n,n0) si ottiene la formula chiusa T(n) = somma_l [prodotto_j s(f^(j)(n))] w(f^(l)(n)) + [prodotto s] T0, verificata per induzione e applicata a esempi (merge sort, Hanoi, ricorrenze con radice).Albero della ricorrenza e formula generale per le ricorrenze →.
- Base : un confronto. Divide: metà , ; coppie , .
- Osservazione: i due minimi di stanno in (ogni altro elemento ha già due più piccoli nella sua metà).
- Conquer: se : , (perché ); se : , .
- Codice:
if x1<x2 then (if x2<y1 then (x1,x2) else (x1,y1)) else (if x1<y2 then (x2,x1) else (x2,y2)). Su : . - Ricorrenza: , (sempre due confronti nel Conquer). Formula generale: , foglie : . Confronto: del metodo banale; : contro .
- Errori tipici: confrontare tutte e quattro le candidate (più confronti); dimenticare che per escludere ; scrivere (con è ); contare un confronto in più nel Divide.
- Esecuzione su : metà sinistra (da e ), metà destra (da e ), radice: e falso . Confronti : (contro del metodo banale); verificato su stringhe casuali fino a . Un torneo darebbe (non richiesto).