Esercizi di Algoritmi e Strutture Dati Moreno Marzolla marzolla@cs.unibo.it 18 marzo 2011 Problema basato su 10.5 del libro di testo La CINA (Compagnia Italiana per il Noleggio di Automobili) dispone di k automobili, tutte disponibili per n giorni. Una stessa automobile può avere costo di affitto diverso in giorni diversi: indicheremo con c(a, g) il costo per il noleggio dell automobile a nel giorno g, dove 1 a k e 1 g n. Un cliente si rivolge alla CINA per procurarsi un mezzo di locomozione per l intero periodo (giorni da 1 a n, estremi inclusi). Per risparmiare e sfruttare i prezzi di noleggio migliori in ogni giornata il cliente è disposto a cambiare macchina da un giorno all altro. Per ottenere un cambio, però, è costretto dalla CINA a pagare una penale P, che si aggiunge al costo di noleggio. Il costo di una sequenza di noleggio è quindi dato dalla somma dei costi di noleggio e delle penali pagate per i cambi. Dati k, n, i costi c(a, g) e la penale P : 1. Calcolare il numero di possibili sequenze di noleggio diverse. 2. Scrivere un algoritmo che elenchi tutte le possibili sequenze di noleggio. Una sequenza di noleggio è un vettore di n numeri (v 1, v 2,... v n ) tale che per ogni i 1,... n, 1 v i k rappresenta il numero dell auto noleggiata nel giorno i. 3. Scrivere un algoritmo che calcoli la sequenza di noleggio di costo minimo, nell ipotesi in cui la penale P sia pari a zero. 4. Proporre un algoritmo che in tempo O(nk 2 ) calcoli la spesa minima per il cliente tra tutte le possibili sequenze di noleggio, assumendo una penale P > 0 (Suggerimento: usare la programmazione dinamica); 5. Modificare l algoritmo proposto al punto precedente in modo da avere in output la sequenza di noleggio di costo minimo. La modifica deve avere costo additivo O(nk). Soluzione 1. Il numero delle possibili sequenze di noleggio è k n. Quindi un eventuale algoritmo risolutivo che operi di forza bruta non sarebbe praticabile. 2. (Vedi programma Java) 3. In questo caso è possibile sviluppare una soluzione semplice: per ogni giorno, è sufficiente scegliere l auto che ha costo di noleggio minimo in quello specifico giorno. 4. Supponiamo che Cost(i, j) sia il costo minimo tra tutte le sequenze di noleggio durante i primi j giorni, che al giorno j-esimo usino l auto i, con 1 j n, e 1 i k. Considerando i noleggi solo per il primo giorno (j = 1), possiamo scrivere: Cost(i, 1) = c(i, 1), per ogni i = 1, 2,... k Nel caso di sequenze di noleggio che durano fino al giorno j, per j > 1, l acquirente ha a disposizione le seguenti alternative: 1
Può noleggiare la stessa audo i noleggiata nel giorno j 1. Il costo ottimo fino al giorno j 1 è Cost(i, j 1), a cui si somma il costo di noleggio della vettura i per il giorno j senza pagare alcuna penale. In tal caso si avrebbe: Cost(i, j) = Cost(i, j 1) + c(i, j) Può noleggiare un auto l i nel giorno j 1, e cambiare scegliendo l auto i nel giorno j. In tal caso il costo di noleggio Cost(i, j) diventa Cost(i, j) = Cost(l, j 1) + c(i, j) + P Il costo della sequenza ottima di noleggio Cost(i, j) che al giorno j usa l auto i sarà quindi il minimo tra le alternative descritte sopra, ossia: Cost(i, j) = mincost(i, j 1)+c(i, j), Cost(l, j 1)+c(i, j)+p per ogni l = 1, 2,... k, l i Il risultato del nostro problema, ossia il costo minimo tra tutte le sequenze di noleggio per tutti gli n giorni, è dato dal valore minimo dell ultima colonna della matrice Cost, ossia min Cost(i, n) 1 i k 5. Per determinare la sequenza che minimizza il costo di noleggio, una soluzione efficiente consiste nell utilizzare una matrice ausiliaria di dimensione k n che chiameremo prev(i, j), tale che prev(i, j) è l auto utilizzata il giorno j 1 per ottenere il costo Cost(i, j) al giorno j. La sequenza ottima di noleggio si ottiene ripercorrendo all indietro la matrice prev a partire dalla posizione nell ultima colonna che rappresenta l ultima vettura noleggiata; ricordiamo che l indice dell ultima vettura noleggiata è il valore di i che minimizza Cost(i, n). La soluzione con costo additivo O(nk) invece fa uso della sola tabella Cost(i, j) per calcolare la sequenza di noleggio, ripercorrendo la tabella a ritroso per ricostruire le scelte fatte. Consideriamo un semplice esempio con penale P = 0.5, k = 3, n = 4 e matrice dei costi giornalieri c(a, g) come segue: 1 1 1 2 c(a, g) = 0.5 1 7 4 0.9 1.2 0.1 10 Applicando l algoritmo descritto sopra, si ottiene la seguente matrice dei costi Cost(i, j): 1.0 2.0 3.0 4.6 Cost(i, j) = 0.5 1.5 8.5 6.6 0.9 2.1 2.1 12.1 e la seguente matrice prev: prev(i, j) = 1 0 0 2 1 1 1 2 1 2 1 2 Il seguente programma Java risolve l esercizio proposto: / CINA.java calcola la sequenza ottima di noleggio. Questo programma risolve il problema 10.5 p. 275 di Demetrescu, Finocchi, Italiano, Algoritmi e strutture dati (seconda edizione ), McGraw Hill, 2008. Version 0.1 del 2010/02/24 Autore: Moreno Marzolla (marzolla (at) cs.unibo. it ) 2
This file has been released by the author in the Public Domain public class CINA int k; // numero di auto int n; // lunghezza del periodo, in giorni double c [][]; // C[i ][ m] e il costo di noleggio dell auto i nel giorno m double P; // penale da pagare per i cambi // tabella di programmazione dinamica: cost[ i,m] e il costo // minimo di tutte le sequenze di noleggio, fino al giorno m // compreso, che hanno l auto i come auto prenotata per il giorno // m double cost [][]; int prev [][]; // Assumendo di scegliere l auto i al giorno j, // prev[ i ][ j ] indica il numero di auto noleggiata il // giorno j 1 che consente di ottenere il costo // cost [ i ][ j ] dell intera sequenza di noleggio public CINA( int k, int n, double c [][], double P ) this.k = k; this.n = n; this.c = c; this.p = P; cost = new double[k][n]; prev = new int[k][n ]; Stampa il contenuto delle tabelle di programmazione dinamica. public void stampatabella( ) int i, j ; System.out. println ( cost[ i ][ j ] ); for ( i=0; i<k; ++i ) for ( j=0; j<n; ++j ) System.out. print (cost [ i ][ j]+ ); System.out. println (); System.out. println ( prev[ i ][ j ] ); for ( i=0; i<k; ++i ) for ( j=0; j<n; ++j ) System.out. print (prev[ i ][ j]+ ); System.out. println (); Stampa le informazioni di una singola sequenza di noleggio protected void stampasequenza( int s [] ) double tot=0.0; for ( int i=0; i<n; ++i ) System.out. print ( g= +i+ /a= +s[i]+ /c= +c[s[i]][i]+ ); tot += c[s[i ]][ i ]; if ( i>0 ) if (s[ i ]!= s[ i 1]) tot+=p; System.out. println ( Totale= +tot); 3
Data una sequenza di noleggio s [], modifica s [] per restituire la successiva sequenza di noleggio. Ritorno false se s [] era l ultima sequenza della serie, true altrimenti. public boolean incrementasequenza(int s [] ) int i=0; while( i<n && s[i] == k 1 ) s[ i ] = 0; ++i; if ( i<n ) s[ i ] += 1; return true; else return false ; Stampa tutte le possibili sequenze di noleggio public void stampatuttesequenze( ) int [] s = new int[n]; // s[ i ] indica l auto da noleggiare il giorno i int i ; int c=0; // numero di sequenze di noleggio // Inizializziamo la sequenza iniziale (0, 0,... 0) for ( i=0; i<n; ++i ) s[ i ] = 0; do ++c; stampasequenza(s); while(incrementasequenza(s)); System.out. println ( Ci sono +c+ sequenze di noleggio ); Stampa la sequenza ottima di noleggio, Prima di invocare questa funzione, e necessario aver invocato l operazione CalcolaSequenzaOttima(). public void stampasequenzaottima( ) int [] s = new int[n]; int imin = 0; double cmin = cost [0][ n 1]; for ( int tmp=1; tmp<k; ++tmp) if ( cost [tmp][n 1] < cmin ) cmin = cost[tmp][n 1]; s[n 1] = imin; for ( int j=n 2; j>=0; j) imin=prev[imin][ j+1]; s[ j]=imin; stampasequenza(s); Questa funzione calcola la sequenza ottima di noleggio, e restituisce il costo ottimo. public double CalcolaSequenzaOttima( ) 4
int i, j ; // inizializziamo la tabella. I costi al giorno 1 sono noti, e // sono pari al costo di noleggio delle vetture // // Quindi: cost [ i ][0] = c[i ][0] per ogni i=0..k 1 for ( i=0; i<k; ++i) cost [ i ][0] = c[i ][0]; prev[ i ][0] = 1; // non ci sono auto precedenti al giorno 0 // calcoliamo ora i valori degli altri elementi della tabella // di programmazione dinamica, procedendo una colonna alla // volta, da sinistra verso destra. // // Il costo minimo di tutte le sequenze di noleggio fino al giorno // j, che noleggiano l auto i al giorno j e dato dal minimo // tra i costi seguenti : // Il costo delle sequenze di noleggio che hanno l auto i anche // al giorno precedente j 1 (cost[ i ][ j 1]), sommato al costo // dell auto i al giorno j (c[ i ][ j ]). In questo caso non c e // altro da aggiungere, perche non si pagano penali // Il costo minimo considerando le sequenze di noleggio che // al giorno ( j 1) noleggiano un auto diversa da i, pagando // quindi una penale. Tale costo minimo sara // min per ogni l!= i cost [ l ][ j 1] + c[i ][ j ] + P // // L equazione e // cost [ i ][ j ] = min cost[ i ][ j 1] + c[i ][ j ], // cost [ l ][ j 1] + c[i ][ j ] + P per ogni l!=i for ( j=1; j<n; ++j) for ( i=0; i<k; ++i) int imin = i; // auto al giorno j 1 che minimizza il // costo al giorno j double cmin = cost[i ][ j 1]+c[i][ j ]; // costo minimo al giorno j for ( int tmp=0; tmp<k; ++tmp) if ( tmp!= i ) // il caso tmp == i e gia // stato considerato con // l inizializzazione di imin e // cmin if ( cost [tmp][j 1] + P + c[i][ j ] < cmin ) cmin = cost[tmp][j 1] + P + c[i][ j ]; imin = tmp; // riempiamo la entry della tabella di programmazione // dinamica. cost [ i ][ j ] = cmin; prev[ i ][ j ] = imin; // Il risultato finale e il minimo valore dell ultima colonn // della tabella di programmazione dinamica double cmin = cost [0][ n 1]; for ( int tmp=1; tmp<k; ++tmp) if ( cost [tmp][n 1] < cmin ) cmin = cost[tmp][n 1]; return cmin; public static void main( String [] args ) 5
double [][] c = 1, 1, 1, 2, 0.5, 1, 7, 4, 0.9, 1.2, 0.1, 10 ; CINA solver = new CINA(3, 4, c, 0.5); System.out. println ( Elenco di tutte le sequenze di noleggio ); solver.stampatuttesequenze(); System.out. println ( \ncosto sequenza di noleggio ottima ); System.out. println ( solver.calcolasequenzaottima()); System.out. println ( \ntabelle di programmazione dinamica ); solver.stampatabella(); System.out. println ( \nsequenza di noleggio ottima ); solver.stampasequenzaottima(); Problema basato sul problema 10.7 del libro di testo Si supponga di avere n files F 1, F 2,... F n in cui il file i occupa w(f i ) MB. Supponiamo che tutti i w(f i ) siano interi (ossia, ciascun file occupa un multiplo intero di un MB). Vogliamo individuare, se esiste, un sottoinsieme S di F 1, F 2,... F n tale che la dimensione dei files presenti in questo sottoinsieme sia esattamente pari a 650MB: w(f ) = 650 F S In caso tale sottoinsieme S esista, vogliamo anche sapere quali sono i files che vi appartengono. Soluzione Consideriamo una matrice B(i, j) tale che ogni elemento della matrice sia un valore booleano (vero o falso). In particolare, B(i, j) è vero se e solo se esiste un sottoinsieme di files S di F 1, F 2,... F i la cui dimensione totale sia esattamente pari a j; ossia: w(f ) = j F S Per definizione si ha che B(1, w(f 1 )) = true e B(i, 0) = true per ogni i = 1, 2,... n (il sottoinsieme vuoto ha dimensione zero). Inoltre risulta B(i, j) = B(i 1, j) B (i 1, j w(f i )) La spiegazione è la seguente: esiste un sottoinsieme S di F 1, F 2,... F i la cui dimensione è j se vale una tra le seguenti proprietà esiste un sottoinsieme S di F 1, F 2,... F i 1 tale che F S w(f ) = j. In tal caso S = S è la soluzione e il file F i non ne fa parte. esiste un sottoinsieme S di F 1, F 2,... F i 1 tale che F S w(f ) = j w(f i). In tal caso la soluzione al nostro problema è S = S F i, e il file F i fa parte di S. Questo suggerisce di calcolare la matrice B utilizzando la programmazione dinamica. Si presti attenzione che l indice j w(f j ) potrebbe diventare negativo. In tal caso si assume che il corrispondente valore B (i 1, j w(f i )) sia falso. Una volta calcolata la matrice B(i, j) per ogni i = 1, 2,... n e j = 0,... 650, possiamo concludere the esiste un sottoinsieme di files la cui dimensione totale è 650MB se e solo se B(n, 650) ha valore true. Per determinare anche quali sono i files che fanno parte della soluzione (se esiste), è necessario usare un altra matrice booleana P (i, j), tale che P (i, j) vale true se e solo se il file i-isimo appartiene ad un sottoinsieme S di F 1, F 2,... F i la cui dimensione totale è pari a j (cioè F S w(f ) = j). Durante la costruzione di B(i, j) è possibile costruire la matrice P (i, j). L elenco dei files che appartengono alla soluzione può essere visualizzata con il seguente pseudocodice: 6
d:=650; i:=n; while (d>0) do if ( P(i,d) ) then stampa i; d := d - w(f_i); endif i := i-1; endwhile / Copiatutto. java determina se un insieme di files la cui dimensione complessiva e 1300MB puo essere suddivisa in due CDRom da 650MB ciascuno, e in caso affermativo stampa la suddivisione. Questo programma risolve il problema 10.7 p. 275 di Demetrescu, Finocchi, Italiano, Algoritmi e strutture dati (seconda edizione ), McGraw Hill, 2008. Version 0.1 del 2010/02/24 Autore: Moreno Marzolla (marzolla (at) cs.unibo. it ) This file has been released by the author in the Public Domain public class Copiatutto int [] w; // w[i ] e la dimensione ( in MB) del file i esimo int n; // numero di files boolean [][] B; // B[i ][ j ] = true sse e possibile riempire j MB di // spazio usando un sottoinsieme dei files 0, //... i boolean [][] P; // P[i ][ j ] = true sse il file i e nell insieme di // files la cui dimensione complessiva e j public Copiatutto( int w[] ) this.w = w; this.n = w.length; B = new boolean[n][651]; P = new boolean[n][651]; Stampa la lista dei files la cui dimensione complessiva e pari a 650MB. public void stampasoluzione( ) if (!B[n 1][650] ) return ; // non esiste soluzione int d=650; int i=n 1; System.out. println ( I seguenti files sono sul primo CD ROM ); while( d>0 ) if ( P[i ][ d] ) System.out. print ( i+ (s= +w[i]+ ) ); d =w[i]; i=i 1; System.out. println (); Risolve il seguente problema: dati n files le cui dimensioni 7
sono w[0], w[1],... w[n 1], esiste un sottoinsieme di tali files la cui dimensione sia esattamente pari a 650? Notare che se la dimensione complessiva di tutti i files e 1300MB, allora se questa funzione restituisce true e possibile copiare i files su due CDRom da 650MB senza spezzare nessun file. public boolean solve( ) int i, j ; // Inizializza B a false for ( i=0; i<n; ++i ) for ( j=0; j<651; ++j ) B[i ][ j ] = false ; P[i ][ j ] = false ; B[i ][0] = true; // Inizializza B[0][ w[0]] a true B[0][w[0]] = true; P [0][ w[0]] = true; for ( i=1; i<n; ++i ) for ( j=0; j<651; ++j) if ( B[i 1][j ] ) B[i ][ j ] = true; else if ( j >= w[i] && B[i 1][j w[i]] ) B[i ][ j ] = true; P[i ][ j ] = true; // aggiungi il file i esimo // alla soluzione // Restituisce la risposta return B[n 1][650]; public static void main( String [] args ) // Attenzione, la somma delle dimensioni NON e 1300MB; // l implementazione restituisce comunque true, poiche // risolve il problema che consiste nel trovare un sottinsieme // di files la cui dimensione complessiva e 650MB. int w[] = 330, 100, 310, 10, 80, 10 ; Copiatutto problema = new Copiatutto(w); if ( problema.solve ()) problema.stampasoluzione(); else System.out. println ( Il problema non ammette soluzione ); 8