Il problema dello zaino

Il problema dello zaino che consiste in: dato un insieme di oggetti, ciascuno con un valore e un peso, si vuole scegliere un sottoinsieme che massimizza il valore totale senza superare una capacità massima . Affronteremo questo problema in due versioni

Versione 0.1: pesi unitari, valori diversi

In questo caso abbiamo oggetto tutti con lo stesso peso ma valori differenti, lo zaino può contenere solo un numero limitato di pezzi, pari alla capacità , inoltre gli oggetti sono già disposti in ordine crescente di valore, dal meno prezioso al più prezioso, questa rappresenta la forma più semplice di questo problema. Infatti la scelta ottimale e abbastanza logica consiste nel prendere i oggetti che hanno il valore più alto

Formalizzazione:

  • Abbiamo oggetti ordinati per valore crescente:
  • Vogliamo massimizzare il valore complessivo: poiché i pesi sono unitari e i valori ordinati, la soluzione ottima è semplicemente:
  • Il problema si risolve in maniera lineare in un tempo di esecuzione poiché non sono necessarie né scelte condizionali né chiamate ricorsive
int zainoLineare ( int v [] , int n , int K ) { 
	int somma = 0; 
	for ( int i = n - K ; i < n ; i ++) { 
		if ( i >= 0) somma += v [ i ]; 
	} 
	return somma;
}

Anche se non prettamente necessario per fini didattici esprimiamo la stessa soluzione anche in modo ricorsivo Ricordiamo che: poiché gli oggetti sono già ordinati non è necessario scegliere se includere o meno l’oggetto : se c’è ancora spazio nello zaino () lo si prende automaticamente

int F ( int v [] , int i , int k ) { 
	if ( i == 0 || k == 0) return 0; 
	return v [i -1] + F (v , i -1 , k -1);
}

La funzione viene richiamata inizialmente con e termina dopo chiamate ricorsive

Conclusioni: questo esempio ci permette di capire che un algoritmo iterativo può essere riscritto anche in forma ricorsiva anche quando la ricorsione non offre dei vantaggi in termini di efficienza

Versione generale 0/1: pesi e valori variabili

Il problema si evolve, gli oggetti non sono più simili tra loro: ogni oggetto ha dunque il proprio peso e un valore differente. La strategia di risoluzione del problema precedente non funziona più infatti: un oggetto di grande valore ma troppo pesante potrebbe impedire di portare con se più oggetti leggeri complessivamente più vantaggiosi, devono essere valutate tutte le possibili combinazioni e in questo la ricorsione diventa uno strumento fondamentale

Formalizzazione: Per ogni oggetto abbiamo peso e valore la capacità massima dello zaino è . Vogliamo massimizzare

  • Occorre confrontare sistematicamente le scelte includo/escludo
  • Definiamo come prima, i suoi casi base sono:
    • se o
    • se
    • visto che andremo a controllare tutte le possibili combinazioni il ci serve per squalificare tutte le ricorsioni che superano la capacità massima.
  • il suo caso caso ricorsivo è:

Si osservi che se una soluzione ottima non include allora è ottima su . Se include allora la soluzione residua è ottima su . Massimizzando i due casi otteniamo il valore ottimo.

int F ( int i , int k , int v [] , int w []) { 
	if ( k < 0) return INT_MIN /4; // rappresenta - infinito 
	if ( i == 0 || k == 0) return 0;
	int senza = F (i -1 , k , v , w ) ; 
	int con = v [ i ] + F (i -1 , k - w [ i ] , v , w ) ; 
	return ( senza > con ) ? senza : con ; 
}

La ricorsione da sola restituisce il valore ottimo, per ottenere una soluzione concreta facciamo un confronto locale per capire se è preso o escluso. Questa semplice soluzione ha complessità

Esercizi sulla ricorsione

La ricorsione è uno strumento potente per affrontare i problemi di ottimizzazione, gli esercizi dove ci viene chiesto di trova una soluzione ad un problema di ottimizzazione usando la ricorsione devono essere sviluppati seguendo questi passi:

  • 1. Individuare i casi base che rendono la ricorsione terminante
  • 2. Determinare la scomposizione del problema in sottoproblemi più piccoli
  • 3. Stabilire una regola per combinare le soluzioni