0-1揹包問題的多種演算法設計與分析
0-1揹包問題的多種演算法設計與分析
0-1揹包問題描述:給定一組共n個物品,每種物品都有自己的重量wi(i=1~n)和價值vi(i=1~n),在限定的總重量(揹包的容量C)內,如何選擇才能使得選擇物品的總價值之和最高。選擇最優的物品子集放置於給定揹包中,最優子集對應n元解向量(x1,…xn), xi∈{0或1},因此命名為0-1揹包問題。
輸入:為方便這種大規模輸入資料的除錯,採用檔案輸入,標準輸出(檔案輸出當然也可)的形式。資料輸入的格式如下:每組測試資料包含n+1行,第1行為C和n,表示揹包容量為C且有n個物品,接下來n行是這n個物品的重量wi和價值vi。揹包容量和物品重量都為整數。
輸出:輸出n+1行。第1行為所選物品的最大價值之和,接下來n行為裝入揹包的物品所對應的n元最優解向量(x1,…xn), xi∈{0或1},但每行以"i xi"形式輸出。
任務:選用三種演算法解決問題。
2. 多種演算法詳細設計
1) 遞迴
虛擬碼:
//遞迴函式
f(int i)
{
if(i >= n){ if(cp > best) best = cp; }
if(c - cw > w[i]){ cw += w[i];cp += v[i];f(i+1); cw -= w[i];cp -= v[i];f(i+1); }
else f(i+1);
}
//主函式
main()
{
//呼叫freopen函式,採用檔案輸入
freopen("input.txt","r",stdin);
freopen("output.txt","w",stdout);
scanf( 揹包最大重量與物品個數 );
for(i=0;i<n;i++) scanf("%d%d",&w[i],&v[i]); //從檔案中逐個讀取數值
start = clock(); //呼叫clock函式計算演算法開始時間
f(0); //遞迴入口
printf( 最大物品價值 );
printf( 物品存放狀態 );
finish = clock(); //呼叫clock函式計算演算法結束時間
printf( 演算法使用時間 );
}
資料型別的定義:
#define SIZE 整數
clock_t start, finish; //演算法開始與結束的時間變數
int cw=0, cp=0; //cw是遞迴到當前的物品重量,cp是遞迴到當前的物品價 值
int s; //獲得的最大價值
int w[SIZE]; //每個物品的重量
int v[SIZE]; //每個物品的價值
int x[SIZE]; //物品存放狀態
int n; //物品個數
int C; //揹包最大容量
主程式與模組之間的層次(呼叫)關係:
main() 先呼叫 f(),f() 再遞迴呼叫自身,返回最大價值,儲存最優解
演算法簡略分析:
對於揹包容量為c ,n件物品的狀態,記為[c, n],根據是否將第i件物品放入揹包。若放入揹包可以劃分成兩種狀態,則揹包狀態變為[c-w[i],n-1]。若不放入揹包,則狀態變為[c,n-1]。一直到容量c等於0或到第1件物品。然後對比兩種狀態,取較大的結果返回。期間用 x[n] 記錄第n件物品是否放入揹包,用以求出最優解向量。
2) 動態規劃
虛擬碼:
//比較函式
int max(int a,int b)
{
if(a>=b) return a;
else return b;
}
//動態規劃處理揹包問題
int KnapSack(int n,int w[],int v[],int x[],int c)
{
//動態規劃中的分治體現在用兩個迴圈對物品的個數和揹包最大重量進行劃分,求 解子問題
//當物品為1個時,最大重量從0到C變化,求得其中最大價值;當物品為2個時, 最大重量從0到C變化,求得其中最大價值;當物品為3個時……
for(i=0;i<=n-1;i++)
{
for(j=0;j<=c;j++) if(j<w[i]) V[i][j]=V[i-1][j];
else V[i][j]=max(V[i-1][j],V[i-1][j-w[i]]+v[i]); //j-w[i]保證總重量不超過C
}
j=c;
for(i=n-1;i>=0;i--)
{
if(V[i][j]>V[i-1][j]){ x[i]=1; j=j-w[i]; }
else x[i]=0;
}
return V[n-1][c];
}
//主函式
main()
{
//呼叫freopen函式,採用檔案輸入
freopen("input.txt","r",stdin);
freopen("output.txt","w",stdout);
scanf( 揹包最大重量與物品個數 );
for(i=0;i<n;i++) scanf("%d%d",&w[i],&v[i]); //從檔案中逐個讀取數值
start = clock(); //呼叫clock函式計算演算法開始時間
s=KnapSack(n,w,v,x,c); //動態規劃入口
printf( 最大物品價值 );
printf( 物品存放狀態 );
finish = clock(); //呼叫clock函式計算演算法結束時間
printf( 演算法使用時間 );
}
資料型別的定義:
#define SIZE 整數
#define WEIGHT 整數
clock_t start, finish; //演算法開始與結束的時間變數
int V[SIZE][WEIGHT] = {0}; //前i個物品裝入容量為j的揹包中獲得的最大價值, 初始化為0
int s; //獲得的最大價值
int w[SIZE]; //每個物品的重量
int v[SIZE]; //每個物品的價值
int x[SIZE]; //物品存放狀態
int n; //物品個數
int c; //揹包最大容量
主程式與模組之間的層次(呼叫)關係:
main() 呼叫 KnapSack() ,返回最大價值,儲存最優解;
KnapSack() 在比較的時候呼叫 max(),返回較大值。
演算法簡略分析:
①V(i,0)=V(0,j)=0
②V(i,j)=V(i-1,j) j<wi
V(i,j)=max{V(i-1,j) ,V(i-1,j-wi)+vi) } j>wi
①式表明:如果第i個物品的重量大於揹包的容量,則裝人前i個物品得到的最大價值和裝入前i-1個物品得到的最大價是相同的,即物品i不能裝入揹包;②式表明:如果第i個物品的重量小於揹包的容量,則會有一下兩種情況:
a.如果把第i個物品裝入揹包,則揹包物品的價值等於第i-1個物品裝入容量位j-wi 的揹包中的價值加上第i個物品的價值vi;
b.如果第i個物品沒有裝入揹包,則揹包中物品價值就等於把前i-1個物品裝入容量為j的揹包中所取得的價值。
顯然,取二者中價值最大的作為把前i個物品裝入容量為j的揹包中的最優解。
3) 貪心演算法
虛擬碼:
//交換兩個浮點數
void swapD(double *vlo, double *vhi)
{
double temp;
temp = *vlo; *vlo = *vhi; *vhi = temp;
}
//交換兩個整數
void swapI(int *vlo, int * vhi)
{
int temp;
temp = *vlo; *vlo = *vhi; *vhi = temp;
}
//起泡排序
int bubble(int lo, int hi)
{
int sorted = 1;
while(++lo < hi)
if(d[lo] > d[hi])
{
sorted = 0;
swapD(&d[lo], &d[hi]);
swapI(&v[lo], &v[hi]);
swapI(&w[lo], &w[hi]);
}
return sorted;
}
//主函式
int main()
{
//呼叫freopen函式,採用檔案輸入
freopen("input.txt","r",stdin);
freopen("output.txt","w",stdout);
scanf( 揹包最大重量與物品個數 );
for(i=0;i<n;i++)
{
scanf("%d%d",&w[i],&v[i]); //從檔案中逐個讀取數值
d[i] = 1.0*v[i]/w[i]; //將每個物品的價值與重量作比值
}
start = clock(); //呼叫clock函式計算演算法開始時間
int k = n; while(!bubble(-1, k--)); //起泡排序入口
//求最大價值或近似解
for(i = n-1; i >= 0; i--)
{
if(c-w[i] >= 0){c -= w[i];s += v[i];}
}
printf( 最大物品價值 );
printf( 物品存放狀態 );
finish = clock(); //呼叫clock函式計算演算法結束時間
printf( 演算法使用時間 );
}
資料型別的定義:
clock_t start, finish; //計算時間的變數
double d[10000]; //儲存每個物品的價值與重量的比值
int s; //獲得的最大價值
int w[SIZE]; //每個物品的重量
int v[SIZE]; //每個物品的價值
int x[SIZE]; //物品存放狀態
int n; //物品個數
int c; //揹包最大容量
主程式與模組之間的層次(呼叫)關係:
main() 呼叫 bubble() 進行排序,返回排好序的d[];
bubble() 呼叫 swapD() 交換浮點數,呼叫 swap() 交換整數。
演算法簡略分析:
思想比較簡單,從i件物品中挑出價值密度最大的一件,判斷是否能裝入揹包,能的話就裝入,然後從剩下的物品中找出價值密度最大的一件判斷,依此類推直到最後一件物品。因為此演算法並沒有從整體考慮問題,即有可能到最後由於揹包剩餘的空間較多,所以有其他揹包利用率較高的解總價值更高,因此此演算法的出的解是近似解,並不是最優解,但考慮到演算法執行效率,效果相對來說比動態規劃要好一些。
3. 多種演算法除錯和測試
1) 除錯過程中遇到的問題是如何解決的?
問題中大多數都是自己的粗心導致結果不正確,還有對演算法瞭解不夠透徹,但經過一番仔細的糾正之後都通過了測試用例。在求解向量的過程參考了書上的求解過程,知道通過輔助儲存用的陣列m來求得解向量,便解決了問題。個別比較煩人的問題就請教了同學,借鑑了他們的思路,讓我們更快地解決了bug。
2) 演算法的時空分析和是否有改進的設想?
遞迴:因為對於每個物品都有2種狀態需要判斷,所以時間複雜度應該為O(2^n)。因為不需要輔助陣列,所以空間要求不高。
動態規劃:動態規劃是在遞迴基礎上增加了輔助陣列的演算法,具體的時間複雜度很難計算,不過從測試的結果來看,要比遞迴快不少。但是相應的增加大小為n*c的陣列,考慮到資料較大時c可能會很大,所以空間消耗還是不少的。
貪心演算法:因為是通過對比物品的價值密度來計算的,相當於是對物品的價值密度來進行排序,所以時間複雜度為O(n^2)。空間上因為用了一個數字來儲存物品價值密度相關資訊,為3*n,相對於動態規劃來說節省了很多空間。
3) 測試用例選擇是否得當?
本實驗所用的測試資料參考了老師所給的測試用例,有5組資料,資料量分別為50、100、200、500、1000、5000。
4) 除錯的經驗和體會。
因為實驗的資料比較多,所以除錯時要仔細,很多時候都是因為幾個小的粗心導致結果錯誤,為此需要付出較多的時間跟精力來找出這些錯誤,結果是很得不償失的。所以應該在打程式碼階段就仔細的看程式碼,保證沒有細節上的錯誤。排除了粗心導致的錯誤,如果是方法上的錯誤,個人感覺應該參考書上或網路上別人寫的程式碼,其他人寫的好的地方可以借鑑一下,並以此修改自己的程式碼,使演算法更加完善。
4. 多種演算法對比
從執行時間、尋找是否為最優解、能夠求解的問題規模等方面進行列表對比和分析:
演算法 資料(n) |
50 |
100 |
200 |
500 |
1000 |
5000 |
結果 |
遞迴 |
120s |
最優解 |
|||||
動態規劃 |
<1ms |
5ms |
16ms |
100ms |
500ms |
16s |
最優解 |
貪心演算法 |
<1ms |
<1ms |
<1ms |
2ms |
3ms |
60ms |
近似解 |
表1
從表1中看出從一開始遞迴的效率就非常差,而動態與貪心在小量資料中效率並無多大差別,在大量資料貪心演算法有著無與倫比的效率。
5. 多種演算法實現清單
見資料夾中源程式