一、題目 :
分別用蠻力法、動態規划法、回溯法和分支限界法求解0/1背包問題。
注:0/1背包問題:給定種物品和一個容量為的背包,物品的重量是,其價值為,背包問題是如何使選擇裝入背包內的物品,使得裝入背包中的物品的總價值最大。其中,每種物品只有全部裝入背包或不裝入背包兩種選擇。
二、所用算法的基本思想及復雜度分析:
1.蠻力法求解0/1背包問題:
1)基本思想:
對於有n種可選物品的0/1背包問題,其解空間由長度為n的0-1向量組成,可用子集數表示。在搜索解空間樹時,深度優先遍歷,搜索每一個結點,無論是否可能產生最優解,都遍歷至葉子結點,記錄每次得到的裝入總價值,然后記錄遍歷過的最大價值。
2)代碼:
#include <iostream>
#include<cstdio>
using namespace std;
#define N 100
struct goods{
int sign;//物品序號
int wight;//物品重量
int value;//物品價值
};
int n,bestValue,cv,cw,C;//物品數量,價值最大,當前價值,當前重量,背包容量
int X[N],cx[N];//最終存儲狀態,當前存儲狀態
struct goods goods[N];
int Force(int i){
if(i>n-1){
if(bestValue < cv && cw + goods[i].wight <= C){
for(int k=0;k<n;k++)
X[k] = cx[k];//存儲最優路徑
bestValue = cv;
}
return bestValue;
}
cw = cw + goods[i].wight;
cv = cv + goods[i].value;
cx[i] = 1;//裝入背包
Force(i+1);
cw = cw-goods[i].wight;
cv = cv-goods[i].value;
cx[i] = 0;//不裝入背包
Force(i+1);
return bestValue;
}
int main()
{
printf("物品種類n:");
scanf("%d",&n);
printf("背包容量C:");
scanf("%d",&C);
for(int i=0;i<n;i++){
printf("物品%d的重量w[%d]及其價值v[%d]:",i+1,i+1,i+1);
scanf("%d%d",&goods[i].wight,&goods[i].value);
}
int sum1 = Force(0);
printf("蠻力法求解0/1背包問題:\nX=[");
for(int i=0;i<n;i++){
cout << X[i]<<" ";
}
printf("] 裝入總價值%d\n",sum1);
return 0;
}
P.S.蠻力法使用的是遞歸,遞歸的使用經常會寄幾個看不懂T_T,這里再提一下遞歸的問題(以后不要再總是看不懂啦,這樣會顯得自己很辣雞的有木有!!!)
force(0),向下運行,到force(1),進入force(1),一直到force(n+1),i>n,return 結果,跳出force(n+1),在force(n)處從跳出的地方繼續向下走,就是進入減減減的環節了,然后繼續向下,還是一樣,加到n+1時就會跳出來當前的force,調到前一個force,繼續向下,循環進行。
3)復雜度分析:
蠻力法求解0/1背包問題的時間復雜度為:2^n
2.動態規划法求解0/1背包問題:
1)基本思想:
令表示在前個物品中能夠裝入容量為的背包中的物品的最大值,則可以得到如下動態函數:




2)代碼:
#include <iostream>
#include<cstdio>
#define N 100
#define MAX(a,b) a < b ? b : a
using namespace std;
struct goods{
int sign;//物品序號
int wight;//物品重量
int value;//物品價值
};
int n,bestValue,cv,cw,C;//物品數量,價值最大,當前價值,當前重量,背包容量
int X[N],cx[N];//最終存儲狀態,當前存儲狀態
struct goods goods[N];
int KnapSack(int n,struct goods a[],int C,int x[]){
int V[N][10*N];
for(int i = 0; i <= n; i++)//初始化第0列
V[i][0] = 0;
for(int j = 0; j <= C; j++)//初始化第0行
V[0][j] = 0;
for(int i = 1; i <= n; i++)
for(int j = 1; j <= C; j++)
if(j < a[i-1].wight)
V[i][j] = V[i-1][j];
else
V[i][j] = MAX(V[i-1][j],V[i-1][j-a[i-1].wight] + a[i-1].value);
for(int i = n,j = C; i > 0; i--){
if(V[i][j] > V[i-1][j]){
x[i-1] = 1;
j = j - a[i-1].wight;
}
else
x[i-1] = 0;
}
return V[n][C];
}
int main()
{
printf("物品種類n:");
scanf("%d",&n);
printf("背包容量C:");
scanf("%d",&C);
for(int i = 0; i < n; i++){
printf("物品%d的重量w[%d]及其價值v[%d]:",i+1,i+1,i+1);
scanf("%d%d",&goods[i].wight,&goods[i].value);
}
int sum2 = KnapSack(n,goods,C,X);
printf("動態規划法求解0/1背包問題:\nX=[");
for(int i = 0; i < n; i++)
cout<<X[i]<<" ";//輸出所求X[n]矩陣
printf("] 裝入總價值%d\n", sum2);
return 0;
}
3)復雜度分析:
動態規划法求解0/1背包問題的時間復雜度為:n*C
3.回溯法求解0/1背包問題:
1)基本思想:
回溯法:為了避免生成那些不可能產生最佳解的問題狀態,要不斷地利用限界函數(bounding function)來處死那些實際上不可能產生所需解的活結點,以減少問題的計算量。這種具有限界函數的深度優先生成法稱為回溯法。
對於有n種可選物品的0/1背包問題,其解空間由長度為n的0-1向量組成,可用子集數表示。在搜索解空間樹時,只要其左兒子結點是一個可行結點,搜索就進入左子樹。當右子樹中有可能包含最優解時就進入右子樹搜索。

2)代碼:
#include <iostream>
#include<cstdio>
#include<string.h>
#include<algorithm>
using namespace std;
#define N 100
struct goods{
int wight;//物品重量
int value;//物品價值
};
int n,bestValue,cv,cw,C;//物品數量,價值最大,當前價值,當前重量,背包容量
int X[N],cx[N];//最終存儲狀態,當前存儲狀態
struct goods goods[N];
int BackTrack(int i){
if(i > n-1){
if(bestValue < cv){
for(int k = 0; k < n; k++)
X[k] = cx[k];//存儲最優路徑
bestValue = cv;
}
return bestValue;
}
if(cw + goods[i].wight <= C){//進入左子樹
cw += goods[i].wight;
cv += goods[i].value;
cx[i] = 1;//裝入背包
BackTrack(i+1);
cw -= goods[i].wight;
cv -= goods[i].value;//回溯,進入右子樹
}
cx[i] = 0;//不裝入背包
BackTrack(i+1);
return bestValue;
}
bool m(struct goods a, struct goods b){
return (a.value/a.wight) > (b.value/b.wight);
}
int KnapSack3(int n, struct goods a[], int C,int x[N]){
memset(x,0,sizeof(x));
sort(a,a+n,m);//將各物品按單位重量價值降序排列
BackTrack(0);
return bestValue;
}
int main()
{
printf("物品種類n:");
scanf("%d",&n);
printf("背包容量C:");
scanf("%d",&C);
for(int i = 0; i < n; i++){
printf("物品%d的重量w[%d]及其價值v[%d]:",i+1,i+1,i+1);
scanf("%d%d",&goods[i].wight,&goods[i].value);
}
int sum3 = KnapSack3(n,goods,C,X);
printf("回溯法求解0/1背包問題:\nX=[");
for(int i = 0; i < n; i++)
cout << X[i] <<" ";//輸出所求X[n]矩陣
printf("] 裝入總價值%d\n",sum3);
return 0;
}
4.分支限界法求解背包問題:
1)基本思想:
分支限界法類似於回溯法,也是在問題的解空間上搜索問題解的算法。一般情況下,分支限界法與回溯法的求解目標不同。回溯法的求解目標是找出解空間中滿足約束條件的所有解,而分支限界法的求解目標則是找出滿足約束條件的一個解,或是在滿足約束條件的解中找出使某一目標函數值達到極大或極小的解,即在某種意義下的最優解。
首先,要對輸入數據進行預處理,將各物品依其單位重量價值從大到小進行排列。
在下面描述的優先隊列分支限界法中,節點的優先級由已裝袋的物品價值加上剩下的最大單位重量價值的物品裝滿剩余容量的價值和。
算法首先檢查當前擴展結點的左兒子結點的可行性。如果該左兒子結點是可行結點,則將它加入到子集樹和活結點優先隊列中。當前擴展結點的右兒子結點一定是可行結點,僅當右兒子結點滿足上界約束時才將它加入子集樹和活結點優先隊列。當擴展到葉節點時為問題的最優值。
2)代碼:
#include<iostream>
#include<algorithm>
using namespace std;
#define N 100 //最多可能物體數
struct goods //物品結構體
{
int sign; //物品序號
int w; //物品重量
int p; //物品價值
}a[N];
bool m(goods a,goods b)
{
return (a.p/a.w)>(b.p/b.w);
}
int max(int a,int b)
{
return a<b?b:a;
}
int n,C,bestP=0,cp=0,cw=0;
int X[N],cx[N];
struct KNAPNODE //狀態結構體
{
bool s1[N]; //當前放入物體
int k; //搜索深度
int b; //價值上界
int w; //物體重量
int p; //物體價值
};
struct HEAP //堆元素結構體
{
KNAPNODE *p;//結點數據
int b; //所指結點的上界
};
//交換兩個堆元素
void swap(HEAP &a, HEAP&b)
{
HEAP temp = a;
a = b;
b = temp;
}
//堆中元素上移
void mov_up(HEAP H[], int i)
{
bool done = false;
if(i!=1){
while(!done && i!=1){
if(H[i].b>H[i/2].b){
swap(H[i], H[i/2]);
}else{
done = true;
}
i = i/2;
}
}
}
//堆中元素下移
void mov_down(HEAP H[], int n, int i)
{
bool done = false;
if((2*i)<=n){
while(!done && ((i = 2*i) <= n)){
if(i+1<=n && H[i+1].b > H[i].b){
i++;
}
if(H[i/2].b<H[i].b){
swap(H[i/2], H[i]);
}else{
done = true;
}
}
}
}
//往堆中插入結點
void insert(HEAP H[], HEAP x, int &n)
{
n++;
H[n] = x;
mov_up(H,n);
}
//刪除堆中結點
void del(HEAP H[], int &n, int i)
{
HEAP x, y;
x = H[i]; y = H[n];
n --;
if(i<=n){
H[i] = y;
if(y.b>=x.b){
mov_up(H,i);
}else{
mov_down(H, n, i);
}
}
}
//獲得堆頂元素並刪除
HEAP del_top(HEAP H[], int&n)
{
HEAP x = H[1];
del(H, n, 1);
return x;
}
//計算分支節點的上界
void bound( KNAPNODE* node,int M, goods a[], int n)
{
int i = node->k;
float w = node->w;
float p = node->p;
if(node->w>M){ // 物體重量超過背包載重量
node->b = 0; // 上界置為0
}else{
while((w+a[i].w<=M)&&(i<n)){
w += a[i].w; // 計算背包已裝入載重
p += a[i++].p; // 計算背包已裝入價值
}
if(i<n){
node->b = p + (M - w)*a[i].p/a[i].w;
}else{
node -> b = p;
}
}
}
//用分支限界法實現0/1背包問題
int KnapSack4(int n,goodsa[],int C, int X[])
{
int i, k = 0; // 堆中元素個數的計數器初始化為0
int v;
KNAPNODE *xnode, *ynode, *znode;
HEAP x, y, z, *heap;
heap = new HEAP[n*n]; // 分配堆的存儲空間
for( i=0; i<n; i++){
a[i].sign=i; //記錄物體的初始編號
}
sort(a,a+n,m); // 對物體按照價值重量比排序
xnode = new KNAPNODE; // 建立父親結點
for( i=0; i<n; i++){ // 初始化結點
xnode->s1[i] = false;
}
xnode->k = xnode->w = xnode->p = 0;
while(xnode->k<n) {
ynode = new KNAPNODE; // 建立結點y
*ynode = *xnode; //結點x的數據復制到結點y
ynode->s1[ynode->k] = true; // 裝入第k個物體
ynode->w += a[ynode->k].w; // 背包中物體重量累計
ynode->p += a[ynode->k].p; // 背包中物體價值累計
ynode->k ++; // 搜索深度++
bound(ynode, C, a, n); // 計算結點y的上界
y.b = ynode->b;
y.p = ynode;
insert(heap, y, k); //結點y按上界的值插入堆中
znode = new KNAPNODE; // 建立結點z
*znode = *xnode; //結點x的數據復制到結點z
znode->k++; // 搜索深度++
bound(znode, C, a, n); //計算節點z的上界
z.b = znode->b;
z.p = znode;
insert(heap, z, k); //結點z按上界的值插入堆中
delete xnode;
x = del_top(heap, k); //獲得堆頂元素作為新的父親結點
xnode = x.p;
}
v = xnode->p;
for( i=0; i<n; i++){ //取裝入背包中物體在排序前的序號
if(xnode->s1[i]){
X[a[i].sign] =1 ;
}else{
X[a[i].sign] = 0;
}
}
delete xnode;
delete heap;
return v; //返回背包中物體的價值
}
/*測試以上算法的主函數*/
int main()
{
goods b[N];
printf("物品種數n: ");
scanf("%d",&n); //輸入物品種數
printf("背包容量C: ");
scanf("%d",&C); //輸入背包容量
for (int i=0;i<n;i++) //輸入物品i的重量w及其價值v
{
printf("物品%d的重量w[%d]及其價值v[%d]: ",i+1,i+1,i+1);
scanf("%d%d",&a[i].w,&a[i].p);
b[i]=a[i];
}
int sum4=KnapSack4(n,a,C,X);//調用分支限界法求0/1背包問題
printf("分支限界法求解0/1背包問題:\nX=[ ");
for(i=0;i<n;i++)
cout<<X[i]<<" ";//輸出所求X[n]矩陣
printf("] 裝入總價值%d\n",sum4);
return 0;
}

