Docente: Giuseppe Scollo
Università di Catania, sede di Comiso (RG)
Facoltà di Scienze Matematiche, Fisiche e Naturali
Corso di Studi in Informatica applicata, AA 2007-8
problema: dividere un righello in 2n parti di eguale lunghezza, marcandolo con una tacca di altezza n al centro, quindi con una tacca di altezza n-1 al centro di ciascuna delle sue due metà, una tacca di altezza n-2 al centro di ciascuno dei suoi quattro quarti, etc., fino alle tacche di altezza 1 al centro di ciascuna delle sue 2n-1 parti di eguale lunghezza
ecco una soluzione ricorsiva:
void righello(int l, int r, int h) { int m = (l+r)/2; if (h > 0) { righello(l, m, h-1); marca(m, h); righello(m, r, h-1); } }
è evidente una certa analogia formale di questa soluzione con la soluzione ricorsiva del problema delle torri di Hanoi ...
questo ci aiuterà a trovare un algoritmo non ricorsivo per il problema delle torri di Hanoi!
osserviamo innanzitutto che l'altezza h della m-sima tacca prodotta dall'algoritmo del righello ha lo stesso valore di N (diametro del disco) nella corrispondente mossa prodotta dall'algoritmo ricorsivo delle torri di Hanoi:
h
nell'invocazione di marca(m, h)
è uguale am
un algoritmo iterativo per il problema delle torri di Hanoi,
con una pila di n dischi da spostare
nel piolo accanto in direzione d
, consta nell'iterare
la seguente coppia di mosse, fino al conseguimento dell'obiettivo (v. inoltre
l'esercizio 1):
d
se
n è dispari, opposta se
n è pari
la correttezza di questo algoritmo si può facilmente verificare dimostrando per induzione su n che, nell'algoritmo ricorsivo delle torri di Hanoi:
d
se
n è dispari, -d
se
n è pari
negli algoritmi ricorsivi visti finora, l'efficienza dell'approccio divide et impera è stata garantita dal fatto che l'algoritmo partiziona il problema in istanze più piccole e indipendenti dello stesso problema, vale a dire operanti su parti disgiunte dell'input o della struttura dati in gioco
algoritmi ricorsivi che non soddisfano tale condizione di indipendenza possono avere costi computazionali proibitivi
ad esempio, l'algoritmo per il calcolo dei numeri di Fibonacci derivato direttamente dalla loro definizione ricorsiva richiede tempo esponenziale:
int F(int i) { if (i < 1) return 0; if (i == 1) return 1; return F(i-1) + F(i-2); }
l'inefficienza dell'algoritmo è dovuta al ripetersi di invocazioni ricorsive con ugual valore del parametro: il numero totale di invocazioni ricorsive per il calcolo di FN, per N > 1, è maggiore di FN+1 (v. esercizio 2), che è proporzionale a φN (v. lezione 4)
vediamo come ...
per valutare in tempo lineare una funzione definita induttivamente sui numeri naturali si può calcolarla per valori crescenti dell'argomento, a partire dal più piccolo e fino a quello desiderato, memorizzandone i valori in un array
ecco ad es. come si realizza il calcolo dei primi N numeri di Fibonacci con questa tecnica:
F[0] = 0; F[1] = 1; for (i = 2; i <= N; i++) F[i] = F[i-1] + F[i-2];
la programmazione dinamica bottom-up è efficace nel prevenire la reiterazione superflua di calcoli già effettuati, tuttavia con essa:
una diversa tecnica di programmazione dinamica è la seguente
anziché eliminare le invocazioni ricorsive, si trasforma una definizione ricorsiva di funzione così che:
questa tecnica prende il nome di
programmazione dinamica top-down:
con essa, una definizione ricorsiva di funzione per induzione naturale
può venire automaticamente
trasformata
in una definizione in cui il numero di invocazioni ricorsive è al più
pari al valore dell'argomento
ecco ad es. la trasformazione della funzione di calcolo dei numeri di Fibonacci secondo questa tecnica:
int F(int i) { static int notoF[maxN]; if (notoF[i] != 0) return notoF[i]; int t = i; if (i < 0) return 0; if (i > 1) t = F(i-1) + F(i-2); return notoF[i] = t; }
alla struttura dell'algoritmo ricorsivo (inefficiente) visto prima, quello trasformato aggiunge:
problema di ottimizzazione combinatoria, in diverse varianti: eccone la più semplice
dati: uno zaino di capacità M
problema:
varianti più complesse:
si assuma senza perdita di generalità che il volume di ogni oggetto non superi la capacità dello zaino
specifica di soluzione ricorsiva:
per ogni tipo i di oggetti, con
0 ≤
i
<
N, siano
allora la soluzione del problema è data da:
z =
max { val i
+ zi
| 0 ≤
i
<
N }
la specifica si traduce nel seguente algoritmo ricorsivo in C++, dove M sarà il parametro attuale nella prima invocazione della funzione ricorsiva:
typedef struct { int vol; int val; } Tipo; const int N = 10, M = 1000; Tipo t[N]; int zaino(int c) { int i, r, max, v; for (i = 0, max = 0, i < N; i++) if ((r = c - t[i].vol) >= 0) if ((v = t[i].val + zaino(r)) > max) max = v; return max; }
la possibilità di multiple invocazioni ricorsive con lo stesso valore del parametro è fonte di inefficienza
la trasformazione della soluzione ricorsiva inefficiente del problema dello zaino in una ricorsiva efficiente è immediata con la tecnica della programmazione dinamica top-down:
typedef struct { int vol; int val; } Tipo; const int N = 10, M = 1000; Tipo t[N]; static int notoZ[M]; int zaino(int c) { int i, r, max, v; if (notoZ[i] != 0) return notoZ[i]; for (i = 0, max = 0, i < N; i++) if ((r = c - t[i].vol) >= 0) if ((v = t[i].val + zaino(r)) > max) max = v; return notoZ[c] = max; }
diversamente dal problema del calcolo dei numeri di Fibonacci, la soluzione ricorsiva del problema dello zaino per un dato valore del parametro non richiede la soluzione dello stesso problema per tutti i precedenti valori del parametro, ma solo per alcuni di essi
se è costante il costo computazionale dell'esecuzione di una invocazione ricorsiva, detratto quello delle invocazioni ricorsive che essa eventualmente effettua, allora il costo computazionale di una funzione ricorsiva con argomento intero naturale, definita con una tecnica di programmazione dinamica, è lineare rispetto all'argomento
inoltre, come mostrato dalla variante considerata del problema dello zaino, la programmazione dinamica top-down ha un costo computazionale inferiore a quello della programmazione dinamica bottom-up quando la ricorsione non coinvolge tutti i valori dell'argomento inferiori a quello dato
varianti più complesse del problema dello zaino rivelano un altro vantaggio dell'approccio top-down rispetto a quello bottom-up: