古詩詞大全網 - 成語查詢 - 0-1背包問題的多種解法代碼(動態規劃、貪心法、回溯法、分支限界法)

0-1背包問題的多種解法代碼(動態規劃、貪心法、回溯法、分支限界法)

壹.動態規劃求解0-1背包問題

/************************************************************************/

/* 0-1背包問題:

/* 給定n種物品和壹個背包

/* 物品i的重量為wi,其價值為vi

/* 背包的容量為c

/* 應如何選擇裝入背包的物品,使得裝入背包中的物品

/* 的總價值最大?

/* 註:在選擇裝入背包的物品時,對物品i只有兩種選擇,

/* 即裝入或不裝入背包。不能將物品i裝入多次,也

/* 不能只裝入部分的物品i。

/*

/* 1. 0-1背包問題的形式化描述:

/* 給定c>0, wi>0, vi>0, 0<=i<=n,要求找到壹個n元的

/* 0-1向量(x1, x2, ..., xn), 使得:

/* max sum_{i=1 to n} (vi*xi),且滿足如下約束:

/* (1) sum_{i=1 to n} (wi*xi) <= c

/* (2) xi∈{0, 1}, 1<=i<=n

/*

/* 2. 0-1背包問題的求解

/* 0-1背包問題具有最優子結構性質和子問題重疊性質,適於

/* 采用動態規劃方法求解

/*

/* 2.1 最優子結構性質

/* 設(y1,y2,...,yn)是給定0-1背包問題的壹個最優解,則必有

/* 結論,(y2,y3,...,yn)是如下子問題的壹個最優解:

/* max sum_{i=2 to n} (vi*xi)

/* (1) sum_{i=2 to n} (wi*xi) <= c - w1*y1

/* (2) xi∈{0, 1}, 2<=i<=n

/* 因為如若不然,則該子問題存在壹個最優解(z2,z3,...,zn),

/* 而(y2,y3,...,yn)不是其最優解。那麽有:

/* sum_{i=2 to n} (vi*zi) > sum_{i=2 to n} (vi*yi)

/* 且,w1*y1 + sum_{i=2 to n} (wi*zi) <= c

/* 進壹步有:

/* v1*y1 + sum_{i=2 to n} (vi*zi) > sum_{i=1 to n} (vi*yi)

/* w1*y1 + sum_{i=2 to n} (wi*zi) <= c

/* 這說明:(y1,z2,z3,...zn)是所給0-1背包問題的更優解,那麽

/* 說明(y1,y2,...,yn)不是問題的最優解,與前提矛盾,所以最優

/* 子結構性質成立。

/*

/* 2.2 子問題重疊性質

/* 設所給0-1背包問題的子問題 P(i,j)為:

/* max sum_{k=i to n} (vk*xk)

/* (1) sum_{k=i to n} (wk*xk) <= j

/* (2) xk∈{0, 1}, i<=k<=n

/* 問題P(i,j)是背包容量為j、可選物品為i,i+1,...,n時的子問題

/* 設m(i,j)是子問題P(i,j)的最優值,即最大總價值。則根據最優

/* 子結構性質,可以建立m(i,j)的遞歸式:

/* a. 遞歸初始 m(n,j)

/* //背包容量為j、可選物品只有n,若背包容量j大於物品n的

/* //重量,則直接裝入;否則無法裝入。

/* m(n,j) = vn, j>=wn

/* m(n,j) = 0, 0<=j<wn

/* b. 遞歸式 m(i,j)

/* //背包容量為j、可選物品為i,i+1,...,n

/* //如果背包容量j<wi,則根本裝不進物品i,所以有:

/* m(i,j) = m(i+1,j), 0<=j<wi

/* //如果j>=wi,則在不裝物品i和裝入物品i之間做出選擇

/* 不裝物品i的最優值:m(i+1,j)

/* 裝入物品i的最優值:m(i+1, j-wi) + vi

/* 所以:

/* m(i,j) = max {m(i+1,j), m(i+1, j-wi) + vi}, j>=wi

/*

/************************************************************************/

#define max(a,b) (((a) > (b)) ? (a) : (b))

#define min(a,b) (((a) < (b)) ? (a) : (b))

template <typename Type>

void Knapsack(Type* v, int *w, int c, int n, Type **m)

{

//遞歸初始條件

int jMax = min(w[n] - 1, c);

for (int j=0; j<=jMax; j++) {

m[n][j] = 0;

}

for (j=w[n]; j<=c; j++) {

m[n][j] = v[n];

}

//i從2到n-1,分別對j>=wi和0<=j<wi即使m(i,j)

for (int i=n-1; i>1; i--) {

jMax = min(w[i] - 1, c);

for (int j=0; j<=jMax; j++) {

m[i][j] = m[i+1][j];

}

for (j=w[i]; j<=c; j++) {

m[i][j] = max(m[i+1][j], m[i+1][j-w[i]]+v[i]);

}

}

m[1][c] = m[2][c];

if (c >= w[1]) {

m[1][c] = max(m[1][c], m[2][c-w[1]]+v[1]);

}

}

template <typename Type>

void TraceBack(Type **m, int *w, int c, int n, int* x)

{

for (int i=1; i<n; i++) {

if(m[i][c] == m[i+1][c]) x[i] = 0;

else {

x[i] = 1;

c -= w[i];

}

}

x[n] = (m[n][c])? 1:0;

}

int main(int argc, char* argv[])

{

int n = 5;

int w[6] = {-1, 2, 2, 6, 5, 4};

int v[6] = {-1, 6, 3, 5, 4, 6};

int c = 10;

int **ppm = new int*[n+1];

for (int i=0; i<n+1; i++) {

ppm[i] = new int[c+1];

}

int x[6];

Knapsack<int>(v, w, c, n, ppm);

TraceBack<int>(ppm, w, c, n, x);

return 0;

}

二.貪心算法求解0-1背包問題

1.貪心法的基本思路:

——從問題的某壹個初始解出發逐步逼近給定的目標,以盡可能快的地求得更好的解。當達到某算法中的某壹步不能再繼續前進時,算法停止。

該算法存在問題:

1).不能保證求得的最後解是最佳的;

2).不能用來求最大或最小解問題;

3).只能求滿足某些約束條件的可行解的範圍。

實現該算法的過程:

從問題的某壹初始解出發;

while 能朝給定總目標前進壹步 do

求出可行解的壹個解元素;

由所有解元素組合成問題的壹個可行解;

2.例題分析

1).[背包問題]有壹個背包,背包容量是M=150。有7個物品,物品可以分割成任意大小。

要求盡可能讓裝入背包中的物品總價值最大,但不能超過總容量。

物品 A B C D E F G

重量 35 30 60 50 40 10 25

價值 10 40 30 50 35 40 30

分析:

目標函數: ∑pi最大

約束條件是裝入的物品總重量不超過背包容量:∑wi<=M( M=150)

(1)根據貪心的策略,每次挑選價值最大的物品裝入背包,得到的結果是否最優?

(2)每次挑選所占空間最小的物品裝入是否能得到最優解?

(3)每次選取單位容量價值最大的物品,成為解本題的策略。

<程序代碼:>(環境:c++)

#include<iostream.h>

#define max 100 //最多物品數

void sort (int n,float a[max],float b[max]) //按價值密度排序

{

int j,h,k;

float t1,t2,t3,c[max];

for(k=1;k<=n;k++)

c[k]=a[k]/b[k];

for(h=1;h<n;h++)

for(j=1;j<=n-h;j++)

if(c[j]<c[j+1])

{t1=a[j];a[j]=a[j+1];a[j+1]=t1;

t2=b[j];b[j]=b[j+1];b[j+1]=t2;

t3=c[j];c[j]=c[j+1];c[j+1]=t3;

}

}

void knapsack(int n,float limitw,float v[max],float w[max],int x[max])

{float c1; //c1為背包剩余可裝載重量

int i;

sort(n,v,w); //物品按價值密度排序

c1=limitw;

for(i=1;i<=n;i++)

{

if(w[i]>c1)break;

x[i]=1; //x[i]為1時,物品i在解中

c1=c1-w[i];

}

}

void main()

{int n,i,x[max];

float v[max],w[max],totalv=0,totalw=0,limitw;

cout<<"請輸入n和limitw:";

cin>>n >>limitw;

for(i=1;i<=n;i++)

x[i]=0; //物品選擇情況表初始化為0

cout<<"請依次輸入物品的價值:"<<endl;

for(i=1;i<=n;i++)

cin>>v[i];

cout<<endl;

cout<<"請依次輸入物品的重量:"<<endl;

for(i=1;i<=n;i++)

cin>>w[i];

cout<<endl;

knapsack (n,limitw,v,w,x);

cout<<"the selection is:";

for(i=1;i<=n;i++)

{

cout<<x[i];

if(x[i]==1)

totalw=totalw+w[i];

}

cout<<endl;

cout<<"背包的總重量為:"<<totalw<<endl; //背包所裝載總重量

cout<<"背包的總價值為:"<<totalv<<endl; //背包的總價值

}

三.回溯算法求解0-1背包問題

1.0-l背包問題是子集選取問題。

壹般情況下,0-1背包問題是NP難題。0-1背包

問題的解空間可用子集樹表示。解0-1背包問題的回溯法與裝載問題的回溯法十分類

似。在搜索解空間樹時,只要其左兒子結點是壹個可行結點,搜索就進入其左子樹。當

右子樹有可能包含最優解時才進入右子樹搜索。否則將右子樹剪去。設r是當前剩余

物品價值總和;cp是當前價值;bestp是當前最優價值。當cp+r≤bestp時,可剪去右

子樹。計算右子樹中解的上界的更好方法是將剩余物品依其單位重量價值排序,然後

依次裝入物品,直至裝不下時,再裝入該物品的壹部分而裝滿背包。由此得到的價值是

右子樹中解的上界。

2.解決辦法思路:

為了便於計算上界,可先將物品依其單位重量價值從大到小排序,此後只要順序考

察各物品即可。在實現時,由bound計算當前結點處的上界。在搜索解空間樹時,只要其左兒子節點是壹個可行結點,搜索就進入左子樹,在右子樹中有可能包含最優解是才進入右子樹搜索。否則將右子樹剪去。

回溯法是壹個既帶有系統性又帶有跳躍性的的搜索算法。它在包含問題的所有解的解空間樹中,按照深度優先的策略,從根結點出發搜索解空間樹。算法搜索至解空間樹的任壹結點時,總是先判斷該結點是否肯定不包含問題的解。如果肯定不包含,則跳過對以該結點為根的子樹的系統搜索,逐層向其祖先結點回溯。否則,進入該子樹,繼續按深度優先的策略進行搜索。回溯法在用來求問題的所有解時,要回溯到根,且根結點的所有子樹都已被搜索遍才結束。而回溯法在用來求問題的任壹解時,只要搜索到問題的壹個解就可以結束。這種以深度優先的方式系統地搜索問題的解的算法稱為回溯法,它適用於解壹些組合數較大的問題。

2.算法框架:

a.問題的解空間:應用回溯法解問題時,首先應明確定義問題的解空間。問題的解空間應到少包含問題的壹個(最優)解。

b.回溯法的基本思想:確定了解空間的組織結構後,回溯法就從開始結點(根結點)出發,以深度優先的方式搜索整個解空間。這個開始結點就成為壹個活結點,同時也成為當前的擴展結點。在當前的擴展結點處,搜索向縱深方向移至壹個新結點。這個新結點就成為壹個新的活結點,並成為當前擴展結點。如果在當前的擴展結點處不能再向縱深方向移動,則當前擴展結點就成為死結點。換句話說,這個結點不再是壹個活結點。此時,應往回移動(回溯)至最近的壹個活結點處,並使這個活結點成為當前的擴展結點。回溯法即以這種工作方式遞歸地在解空間中搜索,直至找到所要求的解或解空間中已沒有活結點時為止。

3.運用回溯法解題通常包含以下三個步驟:

a.針對所給問題,定義問題的解空間;

b.確定易於搜索的解空間結構;

c.以深度優先的方式搜索解空間,並且在搜索過程中用剪枝函數避免無效搜索;

#include<iostream>

using namespace std;

class Knap

{

friend int Knapsack(int p[],int w[],int c,int n );

public:

void print()

{

for(int m=1;m<=n;m++)

{

cout<<bestx[m]<<" ";

}

cout<<endl;

};

private:

int Bound(int i);

void Backtrack(int i);

int c;//背包容量

int n; //物品數

int *w;//物品重量數組

int *p;//物品價值數組

int cw;//當前重量

int cp;//當前價值

int bestp;//當前最優值

int *bestx;//當前最優解

int *x;//當前解

};

int Knap::Bound(int i)

{

//計算上界

int cleft=c-cw;//剩余容量

int b=cp;

//以物品單位重量價值遞減序裝入物品

while(i<=n&&w[i]<=cleft)

{

cleft-=w[i];

b+=p[i];

i++;

}

//裝滿背包

if(i<=n)

b+=p[i]/w[i]*cleft;

return b;

}

void Knap::Backtrack(int i)

{

if(i>n)

{

if(bestp<cp)

{

for(int j=1;j<=n;j++)

bestx[j]=x[j];

bestp=cp;

}

return;

}

if(cw+w[i]<=c) //搜索左子樹

{

x[i]=1;

cw+=w[i];

cp+=p[i];

Backtrack(i+1);

cw-=w[i];

cp-=p[i];

}

if(Bound(i+1)>bestp)//搜索右子樹

{

x[i]=0;

Backtrack(i+1);

}

}

class Object

{

friend int Knapsack(int p[],int w[],int c,int n);

public:

int operator<=(Object a)const

{

return (d>=a.d);

}

private:

int ID;

float d;

};

int Knapsack(int p[],int w[],int c,int n)

{

//為Knap::Backtrack初始化

int W=0;

int P=0;

int i=1;

Object *Q=new Object[n];

for(i=1;i<=n;i++)

{

Q[i-1].ID=i;

Q[i-1].d=1.0*p[i]/w[i];

P+=p[i];

W+=w[i];

}

if(W<=c)

return P;//裝入所有物品

//依物品單位重量排序

float f;

for( i=0;i<n;i++)

for(int j=i;j<n;j++)

{

if(Q[i].d<Q[j].d)

{

f=Q[i].d;

Q[i].d=Q[j].d;

Q[j].d=f;

}

}

Knap K;

K.p = new int[n+1];

K.w = new int[n+1];

K.x = new int[n+1];

K.bestx = new int[n+1];

K.x[0]=0;

K.bestx[0]=0;

for( i=1;i<=n;i++)

{

K.p[i]=p[Q[i-1].ID];

K.w[i]=w[Q[i-1].ID];

}

K.cp=0;

K.cw=0;

K.c=c;

K.n=n;

K.bestp=0;

//回溯搜索

K.Backtrack(1);

K.print();

delete [] Q;

delete [] K.w;

delete [] K.p;

return K.bestp;

}

void main()

{

int *p;

int *w;

int c=0;

int n=0;

int i=0;

char k;

cout<<"0-1背包問題——回溯法 "<<endl;

cout<<" by zbqplayer "<<endl;

while(k)

{

cout<<"請輸入背包容量(c):"<<endl;

cin>>c;

cout<<"請輸入物品的個數(n):"<<endl;

cin>>n;

p=new int[n+1];

w=new int[n+1];

p[0]=0;

w[0]=0;

cout<<"請輸入物品的價值(p):"<<endl;

for(i=1;i<=n;i++)

cin>>p[i];

cout<<"請輸入物品的重量(w):"<<endl;

for(i=1;i<=n;i++)

cin>>w[i];

cout<<"最優解為(bestx):"<<endl;

cout<<"最優值為(bestp):"<<endl;

cout<<Knapsack(p,w,c,n)<<endl;

cout<<"[s] 重新開始"<<endl;

cout<<"[q] 退出"<<endl;

cin>>k;

}

四.分支限界法求解0-1背包問題

1.問題描述:已知有N個物品和壹個可以容納M重量的背包,每種物品I的重量為WEIGHT,壹個只能全放入或者不放入,求解如何放入物品,可以使背包裏的物品的總效益最大。

2.設計思想與分析:對物品的選取與否構成壹棵解樹,左子樹表示不裝入,右表示裝入,通過檢索問題的解樹得出最優解,並用結點上界殺死不符合要求的結點。

#include <iostream.h>

struct good

{

int weight;

int benefit;

int flag;//是否可以裝入標記

};

int number=0;//物品數量

int upbound=0;

int curp=0, curw=0;//當前效益值與重量

int maxweight=0;

good *bag=NULL;

void Init_good()

{

bag=new good [number];

for(int i=0; i<number; i++)

{

cout<<"請輸入第件"<<i+1<<"物品的重量:";

cin>>bag[i].weight;

cout<<"請輸入第件"<<i+1<<"物品的效益:";

cin>>bag[i].benefit;

bag[i].flag=0;//初始標誌為不裝入背包

cout<<endl;

}

}

int getbound(int num, int *bound_u)//返回本結點的c限界和u限界

{

for(int w=curw, p=curp; num<number && (w+bag[num].weight)<=maxweight; num++)

{

w=w+bag[num].weight;

p=w+bag[num].benefit;

}

*bound_u=p+bag[num].benefit;

return ( p+bag[num].benefit*((maxweight-w)/bag[num].weight) );

}

void LCbag()

{

int bound_u=0, bound_c=0;//當前結點的c限界和u限界

for(int i=0; i<number; i++)//逐層遍歷解樹決定是否裝入各個物品

{

if( ( bound_c=getbound(i+1, &bound_u) )>upbound )//遍歷左子樹

upbound=bound_u;//更改已有u限界,不更改標誌

if( getbound(i, &bound_u)>bound_c )//遍歷右子樹

//若裝入,判斷右子樹的c限界是否大於左子樹根的c限界,是則裝入

{

upbound=bound_u;//更改已有u限界

curp=curp+bag[i].benefit;

curw=curw+bag[i].weight;//從已有重量和效益加上新物品

bag[i].flag=1;//標記為裝入

}

}

}

void Display()

{

cout<<"可以放入背包的物品的編號為:";

for(int i=0; i<number; i++)

if(bag[i].flag>0)

cout<<i+1<<" ";

cout<<endl;

delete []bag;

}