Torna alla pagina di Algoritmi e strutture dati
:: Bin Packing ::
Problema
Il problema del Bin Packing è il seguente:
dati n oggetti di peso p1, ... , pn,
bisogna partizionare gli n oggetti in contenitori (bin) di capacità C,
in modo tale che il numero totale K di contenitori utilizzati sia minimo.
Il problema è più o meno simile a quello dello zaino, con la differenza che se in quest'ultimo dovevo portar via il maggior numero di oggetti in uno spazio limitato, nel Bin Packing devo traslocare tutto utilizzando il minor numero di contenitori possibili.
Questo problema può essere risolto progettando un algoritmo Greedy.
Soluzione
Primo passo
Come prima cosa devo definire la struttura delle variabili P che definiscono il problema e la struttura della soluzione S.
P è una struct composta da:
- n : numero degli oggetti (intero)
- pi : peso dell'oggetto i, con i che va da 1 a n (vettore di interi)
- C : capacità del singolo contenitore (intero)
Da notare che se n non fosse noto a priori, non potrei definire pi come vettore perché non saprei con quante posizioni dichiararlo. In questo caso dovrei utilizzare una lista.
S è una struct composta da:
- k : numero dei contenitori utilizzati (intero)
- bin[i] = h : un vettore in cui ad ogni oggetto i è associato il pacchetto h in cui esso è contenuto (con h compreso tra i e k) (vettore di interi)
Secondo passo
Per risolvere un problema di tipo Greedy bisogna utilizzare due criteri:
- il criterio di ottimo (o ottimalità), che indica come deve avvenire la selezione degli oggetti in modo che si ottimizzi la soluzione;
- costruzione della soluzione, che tiene conto solo di quei risultati che rispettano i vincoli del problema, che siano cioè ammissibili.
Criterio di ottimo
Considero gli oggetti per peso decrescente.
Il principio è che scegliendo per ultimi gli oggetti più piccoli, questi si collocheranno più facilmente negli spazi ancora liberi.
Costruzione soluzione
In generale, mentre costruisco la soluzione all'i-simo oggetto dovrò sapere:
S:
- k' : numero dei contenitori utilizzati fino a questo momento
- Bh = C - ∑pi : la capacità residua del contenitore, calcolata come differenza tra la capacità totale e la sommatoria dei pesi degli oggetti ivi già collocati
Quindi, indipendentemente dal criterio di ottimo utilizzato, dovrò selezionare il contenitore h' con Bh' ≥ pi (ovvero un contenitore con spazio sufficiente per metterci dentro l'oggetto). Posso avere ora tre casi:
- h' = NULL : se non c'è posto nei contenitori considerati, ne apro uno nuovo
- esiste un unico h' : allora metto l'oggetto in quel contenitore (Bin[i] = h')
- ho tanti h' tra cui scegliere : allora
- scelgo di prediligere l'aspetto computazionale (quindi l'efficienza dell'algoritmo), disinteressandomi del risultato migliore ma seguendo la logica del "fai più in fretta possibile". Soluzione: algoritmo NFD (vedi dopo)
- scelgo di prediligere l'efficacia dell'algoritmo, in modo che rispetti la funzione obiettivo, applicando ancora il criterio di ottimo. Soluzione: algoritmo BFD
- scelgo una via di mezzo tra le due soluzioni precedenti. Soluzione: algoritmo FFD.
Terzo passo
Ecco ora le implementazioni in pseudocodice degli algoritmi elencati prima.
NFD
NFD sta per Next Fit Decreasing, e prevede la scelta del contenitore k' corrente per metterci dentro l'oggetto. Se ci sta, bon. Se non ci sta ne apre un altro e ce lo mette dentro.
soluzione NFD (problema P)
{
int i, b; // i sono gli oggetti, b i contenitori
soluzione S;
inizializzaSol(S); // bin[i]=0, cap[b]=0, K=0
{ ordina gli oggetti per p'_i_' tali che p'_1_' ≥ ... ≥ p'_n_' }
S.k = 1; // apro il primo contenitore
S.bin[1] = 1;
S.cap[1] = p1; // metto il primo oggetto nel primo contenitore
i = 2; // considero il secondo oggetto
b = 1;
while( i <= P.n ) {
if(S.cap[b] < P.p'_i_') b++; //se i non ci sta nel contenitore, ne apro uno nuovo
S.bin[i] = b;
S.cap[b] -= P.p'_i_';
i++;
}
S.k = b;
return(S);
}
Per la complessità dell'algoritmo, consideriamo i vari apporti:
- inizializzazione: O(n) perché devo ripetere n volte l'assegnamento del valore
- ordinamento: se utilizzo un matchsort, allora avrò O(n logn)
- ciclo while: ripeto n-1 volte operazioni elementari, quindi O(n)
La complessità dell'algoritmo è dunque di O(n logn).
FFD
FFD sta per First Fit Decreasing, e prevede la scelta del primo contenitore k' che trova con abbastanza spazio libero per metter dentro l'oggetto. Il codice si distingue sostanzialmente dal precedente solo all'interno del ciclo while.
soluzione FFD (problema P)
{
int i, b; // i sono gli oggetti, b i contenitori
soluzione S;
inizializzaSol(S); // bin[i]=0, cap[b]=0, K=0
{ ordina gli oggetti per p'_i_' tali che p'_1_' ≥ ... ≥ p'_n_' }
S.k = 1; // apro il primo contenitore
S.bin[1] = 1;
S.cap[1] = p1; // metto il primo oggetto nel primo contenitore
i = 2; // considero il secondo oggetto
while( i <= P.n ) {
b = 1;
while(b < S.k && S.cap[b] < P.p'_i_') b++; // *nota
S.bin[i] = b;
S.cap[b] -= P.p'_i_';
if(S.k < b) S.k++; //aggiorna la lista dei contenitori usati
i++;
}
return(S);
}
nota: avviene una scansione di tutti i contenitori aperti, finché non se ne trova uno con abbastanza spazio libero da farci stare dentro i. Se non lo trova, l'ultimo b++
eseguito corrisponde all'apertura di un nuovo contenitore. La lista dei contenitori usati verrà aggiornata poco più sotto.
Per la complessità dell'algoritmo, consideriamo i vari apporti:
- inizializzazione: O(n) perché devo ripetere n volte l'assegnamento del valore
- ordinamento: se utilizzo un matchsort, allora avrò O(n logn)
- ciclo while: nel caso pessimo dovrò creare un nuovo contenitore per ogni oggetto, quindi ripeterò n volte n operazioni elementari. La complessità sarà di O(n2).
La complessità dell'algoritmo è dunque di O(n2).
BFD
BFD sta per Best Fit Decreasing, e prevede la scelta tra i contenitori aperti di quello che minimizza la capacità residua dopo l'inserimento dell'oggetto i. Ad esempio se ho un oggetto di peso 5 e ho un contenitore che lascerebbe libero 3 e un altro 8, sceglierò il primo).
soluzione BFD (problema P)
{
int i, b; // i sono gli oggetti, b i contenitori
soluzione S;
inizializzaSol(S); // bin[i]=0, cap[b]=0, K=0
{ ordina gli oggetti per p'_i_' tali che p'_1_' ≥ ... ≥ p'_n_' }
S.k = 1; // apro il primo contenitore
S.bin[1] = 1;
S.cap[1] = p1; // metto il primo oggetto nel primo contenitore
i = 2; // considero il secondo oggetto
while( i <= P.n ) {
b = 1;
m = 0;
M = C;
while(b <= S.k) {
if(S.cap[b] - P.p'_i_' < M) {
m = b;
M = S.cap[b] - p'_i_';
}
b++;
}
if(m == 0) { // se l'oggetto non ci sta in nessun contenitore..
S.k++; // ne apro uno nuovo
m = S.k;
}
S.bin[i] = m;
S.cap[n] -= P.p'_i_';
i++;
}
return(S);
}
Per la complessità dell'algoritmo, consideriamo i vari apporti:
- inizializzazione: O(n) perché devo ripetere n volte l'assegnamento del valore
- ordinamento: se utilizzo un matchsort, allora avrò O(n logn)
- ciclo while: nel caso pessimo dovrò creare un nuovo contenitore per ogni oggetto, quindi ripeterò n volte n operazioni elementari. La complessità sarà di O(n2).
La complessità dell'algoritmo è dunque di O(n2).
Torna alla pagina di Algoritmi e strutture dati