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; 

}

 最近在做背包问题,今天写点东西总结一下。

        背包问题,常见的有三种类型:基本的0-1背包、完全背包和多重背包、二维背包 

        首先是基本的0-1背包问题。因为这里的物品一般指花瓶、玉器什么的,要么拿、要么不拿,只有0和1两种状态,所以也叫0-1背包。0-1背包虽然简单,却很重要,是“万法之源”,是其他几类问题的基础。 

        初学者有时会认为,0-1背包可以这样求解:计算每个物品的Vi/Wi,然后依据Vi/Wi的值,对所有的物品从大到小进行排序。其实这种贪心方法是错误的。如下表,有三件物品,背包的最大负重量是50,求可以取得的最大价值。 

        其实,0-1背包是DP的一个经典实例,可以用动态规划求解。

DP求解过程可以这样理解:对于前i件物品,背包剩余容量为j时,所取得的最大价值(此时称为状态3)只依赖于两个状态。

状态1:前i-1件物品,背包剩余容量为j。在该状态下,只要不选第i个物品,就可以转换到状态3。

状态2:前i-1件物品,背包剩余容量为j-w[i]。在该状态下,选第i个物品,也可以转换到状态3。

因为,这里要求最大价值,所以只要从状态1和状态2中选择最大价值较大的一个即可。 

状态转换方程:

dp( i,j ) = Max( dp( i-1, j ), dp( i-1, j-w[i] ) + v[i] )

dp( i,j )表示前i件物品,背包剩余容量为j时,所取得的最大价值。 

        还是结合上面的例子来说明吧。有三件物品,背包的最大负重量是50,求可以取得的最大价值。下图表示了DP自上而下的求解过程。

编程实现:

       一般来说,有了状态方程,直接编程实现就game over。dp( i,j ),用一个二维数组来实现,然后用一个两层循环就可以了。不过,有时选择的物品很多,背包的容量很大,这时要用二维数组往往是不现实的。这里有一个方法,可以进行空间压缩,然后使用一维数组实现。

       还是结合上面的例子,有三件物品,背包的最大负重量是5,求可以取得的最大价值。为了方面说明,物品weight依次为:1,2,3。二维数组下的求解顺序,物品数1--->n, 背包容量1--->w。如图,要使用一维数组,背包容量要采用倒序,即w--->1, 只有这样对于方程dp( j ) = Max( dp( j ), dp (j-w[i] ) + v[i] ),才能达到等式左边才表示i,而等式右边表示i-1的效果。POJ对于题目:3624。下面附代码。

        完全背包和多重背包。有了基本的0-1背包基础,下面的东西也就好理解了。 完全背包,指每个物品有无限多个。 多重背包,指每个物品的数量是有限的。当然,这时的问题不再是拿与不拿,而是拿多少的问题,当然不能超过背包容量。 

状态转换方程:

dp( i,j ) = Max( dp( i-1, j ), dp( i-1, j-k*w[i]) + k*v[i] ) ( 0 <= k <= c/ w[i] )

编码实现:

如果直接编码,用三层循环,往往会超时。这样有一种很有效的压缩方式:二进制压缩。把原来的物品按照2的n次方进行重新组合。用1、2、4、8…进行组合,可以组合出任意的数字。POJ题目:1276


POJ3624

[cpp]  view plain copy
  1. #include <iostream>  
  2. using namespace std;  
  3.   
  4. //***********************常量定义*****************************  
  5.   
  6. const int MAX_NUM = 3500;  
  7. const int MAX_WEIGHT = 14000;  
  8.   
  9. //*********************自定义数据结构*************************  
  10.   
  11.   
  12.   
  13. //********************题目描述中的变量************************  
  14.   
  15. int weight[MAX_NUM];  
  16. int value[MAX_NUM];  
  17.   
  18.   
  19. //**********************算法中的变量**************************  
  20.   
  21. //进行空间压缩,使用一维数组  
  22. int dp[MAX_WEIGHT];  
  23.   
  24.   
  25. //***********************算法实现*****************************  
  26.   
  27. void Solve( int n, int w )  
  28. {     
  29.     forint i=1; i<=n; i++ )  
  30.     {  
  31.         //因为使用了一维数组,所有j要按照递减顺序  
  32.         forint j=w; j>=weight[i]; j-- )  
  33.         {             
  34.             if( dp[j-weight[i]] + value[i] > dp[j] )  
  35.                 dp[j] = dp[j-weight[i]] + value[i];           
  36.         }  
  37.     }  
  38.     cout << dp[w] << endl;  
  39. }  
  40.   
  41.   
  42. //************************main函数****************************  
  43.   
  44. int main()  
  45. {  
  46.     //freopen( "in.txt", "r", stdin );    
  47.   
  48.     int n, w;  
  49.     cin >> n >> w;        
  50.   
  51.     forint i=1; i<=n; i++ )  
  52.     {  
  53.         cin >> weight[i] >> value[i];  
  54.     }  
  55.     Solve( n, w );  
  56.       
  57.     return 0;  
  58. }  

POJ1276

[cpp]  view plain copy
  1. #include <iostream>  
  2. using namespace std;  
  3.   
  4. //***********************常量定义*****************************  
  5.   
  6. const int MAX_NUM = 1005;  
  7. const int MAX_CASH_REQUEST = 100005;  
  8.   
  9.   
  10. //*********************自定义数据结构*************************  
  11.   
  12.   
  13.   
  14.   
  15. //********************题目描述中的变量************************  
  16.   
  17. int cashRequest;  
  18. int cashKind;  
  19.   
  20.   
  21. //**********************算法中的变量**************************  
  22. //dp[i][j]表示前i个物件,cashRequest == j时,所能获得的最大金额  
  23. int dp[MAX_CASH_REQUEST];  
  24. //使用二进制压缩,形成的新物件  
  25. int cnt;  
  26. int value[MAX_NUM];  
  27.   
  28.   
  29. //***********************算法实现*****************************  
  30.   
  31. void Solve()  
  32. {  
  33.     //采用0-1背包求解  
  34.     forint i=1; i<=cnt; i++ )  
  35.     {         
  36.         forint j=cashRequest; j>=value[i]; j-- )  
  37.         {  
  38.             dp[j] = dp[j] > dp[j-value[i]] + value[i] ? dp[j] : dp[j-value[i]] + value[i];  
  39.         }                 
  40.     }  
  41.     cout << dp[cashRequest] << endl;  
  42. }  
  43.   
  44.   
  45. //************************main函数****************************  
  46.   
  47. int main()  
  48. {  
  49.     //freopen( "in.txt", "r", stdin );  
  50.       
  51.     while( cin >> cashRequest >> cashKind )  
  52.     {  
  53.         //输入  
  54.         forint i=1; i<=cashKind; i++ )  
  55.         {  
  56.             int num, deno;  
  57.             cin >> num >> deno;  
  58.               
  59.             //二进制压缩  
  60.             forint j=1; j<=num; j*=2 )  
  61.             {  
  62.                 value[++cnt] = deno * j;  
  63.                 num -= j;  
  64.             }                 
  65.             if( num > 0 )    value[++cnt] = num * deno;            
  66.         }  
  67.   
  68.         //处理  
  69.         Solve();  
  70.           
  71.         //清空全局变量  
  72.         cnt = 0;  
  73.         memset( value, 0, sizeof(value) );  
  74.         memset( dp, 0, sizeof(dp) );  
  75.     }     
  76.   
  77.     return 0;  
  78. }  

简介

    背包问题已经是一个很经典而且讨论很广泛的算法问题了。最近学习到这一部分,打算结合自己思考和编码的过程做一个思考总结。这里主要讨论的0-1背包问题和部分背包问题解决方法背后其实隐藏了两种我们比较常见的算法解决思路,动态规划和贪婪算法。正好通过这两个问题的讨论可以好好的加深一下理解。

 

问题描述

    假设我们有n件物品,分别编号为1, 2...n。其中编号为i的物品价值为vi,它的重量为wi。为了简化问题,假定价值和重量都是整数值。现在,假设我们有一个背包,它能够承载的重量是W。现在,我们希望往包里装这些物品,使得包里装的物品价值最大化,那么我们该如何来选择装的东西呢?问题结构如下图所示:

    这个问题其实根据不同的情况可以归结为不同的解决方法。假定我们这里选取的物品每个都是独立的,不能选取部分。也就是说我们要么选取某个物品,要么不能选取,不能只选取一个物品的一部分。这种情况,我们称之为0-1背包问题。而如果我们可以使用部分的物品的话,这个问题则成为部分背包(fractional knapsack)问题。下面我们针对每种情况具体分析一下。

 

0-1背包问题

初步分析

    对于这个问题,一开始确实有点不太好入手。一堆的物品,每一个都有一定的质量和价值,我们能够装入的总重量有限制,该怎么来装使得价值最大呢?对于这n个物品,每个物品我们可能会选,也可能不选,那么我们总共就可能有2^n种组合选择方式。如果我们采用这种办法来硬算的话,则整体的时间复杂度就达到指数级别的,肯定不可行。

    现在我们换一种思路。既然每一种物品都有价格和重量,我们优先挑选那些单位价格最高的是否可行呢?比如在下图中,我们有3种物品,他们的重量和价格分别是10, 20, 30 kg和60, 100, 120。

 

    那么按照单位价格来算的话,我们最先应该挑选的是价格为60的元素,选择它之后,背包还剩下50 - 10 = 40kg。再继续前面的选择,我们应该挑选价格为100的元素,这样背包里的总价值为60 + 100 = 160。所占用的重量为30, 剩下20kg。因为后面需要挑选的物品为30kg已经超出背包的容量了。我们按照这种思路能选择到的最多就是前面两个物品。如下图:

    按照我们前面的期望,这样选择得到的价值应该是最大的。可是由于有一个背包重量的限制,这里只用了30kg,还有剩下20kg浪费了。这会是最优的选择吗?我们看看所有的选择情况:

    很遗憾,在这几种选择情况中,我们前面的选择反而是带来价值最低的。而选择重量分别为20kg和30kg的物品带来了最大的价值。看来,我们刚才这种选择最佳单位价格的方式也行不通。 

动态规划 

    既然前面两种办法都不可行,我们再来看看有没有别的方法。我们再来看这个问题。我们需要选择n个元素中的若干个来形成最优解,假定为k个。那么对于这k个元素a1, a2, ...ak来说,它们组成的物品组合必然满足总重量<=背包重量限制,而且它们的价值必然是最大的。因为它们是我们假定的最优选择嘛,肯定价值应该是最大的。假定ak是我们按照前面顺序放入的最后一个物品。它的重量为wk,它的价值为vk。既然我们前面选择的这k个元素构成了最优选择,如果我们把这个ak物品拿走,对应于k-1个物品来说,它们所涵盖的重量范围为0-(W-wk)。假定W为背包允许承重的量。假定最终的价值是V,剩下的物品所构成的价值为V-vk。这剩下的k-1个元素是不是构成了一个这种W-wk的最优解呢?

    我们可以用反证法来推导。假定拿走ak这个物品后,剩下的这些物品没有构成W-wk重量范围的最佳价值选择。那么我们肯定有另外k-1个元素,他们在W-wk重量范围内构成的价值更大。如果这样的话,我们用这k-1个物品再加上第k个,他们构成的最终W重量范围内的价值就是最优的。这岂不是和我们前面假设的k个元素构成最佳矛盾了吗?所以我们可以肯定,在这k个元素里拿掉最后那个元素,前面剩下的元素依然构成一个最佳解。

    现在我们经过前面的推理已经得到了一个基本的递推关系,就是一个最优解的子解集也是最优的。可是,我们该怎么来求得这个最优解呢?我们这样来看。假定我们定义一个函数c[i, w]表示到第i个元素为止,在限制总重量为w的情况下我们所能选择到的最优解。那么这个最优解要么包含有i这个物品,要么不包含,肯定是这两种情况中的一种。如果我们选择了第i个物品,那么实际上这个最优解是c[i - 1, w-wi] + vi。而如果我们没有选择第i个物品,这个最优解是c[i-1, w]。这样,实际上对于到底要不要取第i个物品,我们只要比较这两种情况,哪个的结果值更大不就是最优的么?

    在前面讨论的关系里,还有一个情况我们需要考虑的就是,我们这个最优解是基于选择物品i时总重量还是在w范围内的,如果超出了呢?我们肯定不能选择它,这就和c[i-1, w]一样。

    另外,对于初始的情况呢?很明显c[0, w]里不管w是多少,肯定为0。因为它表示我们一个物品都不选择的情况。c[i, 0]也一样,当我们总重量限制为0时,肯定价值为0。

    这样,基于我们前面讨论的这3个部分,我们可以得到一个如下的递推公式:

    有了这个关系,我们可以更进一步的来考虑代码实现了。我们有这么一个递归的关系,其中,后面的函数结果其实是依赖于前面的结果的。我们只要按照前面求出来最基础的最优条件,然后往后面一步步递推,就可以找到结果了。

    我们再来考虑一下具体实现的细节。这一组物品分别有价值和重量,我们可以定义两个数组int[] v, int[] w。v[i]表示第i个物品的价值,w[i]表示第i个物品的重量。为了表示c[i, w],我们可以使用一个int[i][w]的矩阵。其中i的最大值为物品的数量,而w表示最大的重量限制。按照前面的递推关系,c[i][0]和c[0][w]都是0。而我们所要求的最终结果是c[n][w]。所以我们实际中创建的矩阵是(n + 1) x (w + 1)的规格。下面是该过程的一个代码参考实现:

Java代码   收藏代码
  1. public class DynamicKnapSack {  
  2.     private int[] v;  
  3.     private int[] w;  
  4.     private int[][] c;  
  5.     private int weight;  
  6.   
  7.     public DynamicKnapSack(int length, int weight, int[] vin, int[] win) {  
  8.         v = new int[length + 1];  
  9.         w = new int[length + 1];  
  10.         c = new int[length + 1][weight + 1];  
  11.         this.weight = weight;  
  12.         for(int i = 0; i < length + 1; i++) {  
  13.             v[i] = vin[i];  
  14.             w[i] = win[i];  
  15.         }  
  16.     }  
  17.   
  18.     public void solve() {  
  19.        for(int i = 1; i < v.length; i++) {  
  20.             for(int k = 1; k <= weight; k++) {  
  21.                 if(w[i] <= k) {  
  22.                     if(v[i] + c[i - 1][k - w[i]] > c[i - 1][k])  
  23.                         c[i][k] = v[i] + c[i - 1][k - w[i]];  
  24.                     else  
  25.                         c[i][k] = c[i - 1][k];  
  26.                 } else  
  27.                     c[i][k] = c[i - 1][k];  
  28.             }  
  29.         }  
  30.     }  
  31.   
  32.     public void printResult() {  
  33.         for(int i = 0; i < v. length; i++) {  
  34.             for(int j = 0; j <= weight; j++)  
  35.                 System.out.print(c[i][j] + " ");  
  36.             System.out.println();  
  37.         }  
  38.     }  
  39.       
  40.     public static void main(String[] args) {  
  41.         int[] v = {060100120};  
  42.         int[] w = {0102030};  
  43.         int weight = 50;  
  44.         DynamicKnapSack knapsack = new DynamicKnapSack(3, weight, v, w);  
  45.         knapsack.solve();  
  46.         knapsack.printResult();  
  47.     }  
  48. }  

    这部分代码里关键的就是solve方法。里面两个遍历循环,i表示从1到n的范围,对应于我们递归方法里描述的c(i, w)中到第i位。而k表示的是当前的重量限制。下面是程序运行的输出结果:

Java代码   收藏代码
  1. 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0   
  2. 0 0 0 0 0 0 0 0 0 0 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60   
  3. 0 0 0 0 0 0 0 0 0 0 60 60 60 60 60 60 60 60 60 60 100 100 100 100 100 100 100 100 100 100 160 160 160 160 160 160 160 160 160 160 160 160 160 160 160 160 160 160 160 160 160   
  4. 0 0 0 0 0 0 0 0 0 0 60 60 60 60 60 60 60 60 60 60 100 100 100 100 100 100 100 100 100 100 160 160 160 160 160 160 160 160 160 160 180 180 180 180 180 180 180 180 180 180 220  

     最右下角的数值220就是c[1, 50]的解。

    至此,我们对于这种问题的解决方法已经分析出来了。它的总体时间复杂度为O(nw) ,其中w是设定的一个重量范围,因此也可以说它的时间复杂度为O(n)。

 

部分背包问题

    和前面使用动态规划方法解决问题不一样。因为这里是部分背包问题,我们可以采用前面讨论过的一个思路。就是每次选择最优单位价格的物品,直到达到背包重量限制要求。

    以前面的示例来看,我们按照这种方式选择的物品结果应该如下图:

    现在,我们从实现的角度再来考虑一下。我们这里的最优解是每次挑选性价比最高的物品。对于这一组物品来说,我们需要将他们按照性价比从最高到最低的顺序来取。我们可能需要将他们进行排序。然后再依次取出来放入背包中。假定我们已经有数组v,w,他们已经按照性价比排好序了。一个参考代码的实现如下:

 

Java代码   收藏代码
  1. public double selectMax() {  
  2.         double maxValue = 0.0;  
  3.         int sum = 0;  
  4.         int i;  
  5.         for(i = 0; i < v.length; i++) {  
  6.             if(sum + w[i] < weight) {  
  7.                 sum += w[i];  
  8.                 maxValue += v[i];  
  9.             } else  
  10.                 break;  
  11.         }  
  12.         if(i < v.length && sum < weight) {  
  13.            maxValue += (double)(weight - sum) / w[i] * v[i];  
  14.         }  
  15.   
  16.         return maxValue;  
  17.     }  

   这里省略了对数组v, w的定义。关键点在于我们选择了若干了物品后要判断是否装满了背包重量。如果没有,还要从后面的里面挑选一部分。所以有一个if(i < v.length && sum < weight)的判断。

    在实现后我们来看该问题这种解法的时间复杂度,因为需要将数组排序,我们的时间复杂度为O(nlgn)。

一点改进:

    在前面我们挑选按照性价比排好序的物品时,排序消耗了主要的时间。在这里,我们是否真的需要去把这些物品排序呢?在某些情况下,我们只要选择一堆物品,保证他们物品重量在指定范围内。如果我们一次挑出来一批这样的物品,而且他们满足这样的条件是不是更好呢?这一种思路是借鉴快速排序里对元素进行划分的思路。主要过程如下:

1. 求每个元素的单位价值,pi = vi /wi。然后数组按照pi进行划分,这样会被分成3个部分,L, M, N。其中L < M < N。这里L表示单位价值小于某个指定值的集合,M是等于这个值的集合,而N是大于这个值的集合。

2. 我们可以首先看N的集合,因为这里都是单位价值高的集合。我们将他们的重量累加,如果WN的重量等于我们期望的值W,则N中间的结果就是我们找到的结果。

3. 如果WN的重量大于W,我们需要在N集合里做进一步划分。

4. 如果WN的重量小于W,我们需要在N的基础上再去L的集合里划分,找里面大的一部分。

这样重复步骤1到4.

    这里和快速排序的思路基本上差不多,只是需要将一个分割的集合给记录下来。其时间复杂度也更好一点,为O(N)。这里就简单的描述下思路,等后续再将具体的实现代码给补上。

总结

    我们这里讨论的两种背包问题因为问题的不同其本质解决方法也不同。对于0-1背包来说,他们构成了一个最优解问题的基础。我们可以通过从最小的结果集递推出最终最优结果。他们之间构成了一个递归的关系。而对于部分背包问题来说,我们可以考虑用贪婪算法,每次选择当前看来最优的结果。最终也构成了一个最优的结果。一个小小的前提变化,问题解决的思路却大不同。里面的思想值得反复体会。

   1、问题描述

     给定n种物品和一背包。物品i的重量是wi,其价值为vi,背包的容量为C。问:应如何选择装入背包的物品,使得装入背包中物品的总价值最大?

     形式化描述:给定c >0, wi >0, vi >0 , 1≤i≤n.要求找一n元向量(x1,x2,…,xn,), xi∈{0,1}, ∋ ∑ wi xi≤c,且∑ vi xi达最大.即一个特殊的整数规划问题。

       2、最优性原理

     设(y1,y2,…,yn)是 (3.4.1)的一个最优解.则(y2,…,yn)是下面相应子问题的一个最优解:

     证明:使用反证法。若不然,设(z2,z3,…,zn)是上述子问题的一个最优解,而(y2,y3,…,yn)不是它的最优解。显然有                                     ∑vizi > ∑viyi   (i=2,…,n)      且                           w1y1+ ∑wizi<= c      因此                       v1y1+ ∑vizi (i=2,…,n) > ∑ viyi, (i=1,…,n)       说明(y1,z2, z3,…,zn)是(3.4.1)0-1背包问题的一个更优解,导出(y1,y2,…,yn)不是背包问题的最优解,矛盾。

       3、递推关系

    设所给0-1背包问题的子问题

     

     的最优值为m(i,j),即m(i,j)是背包容量为j,可选择物品为i,i+1,…,n时0-1背包问题的最优值。由0-1背包问题的最优子结构性质,可以建立计算m(i,j)的递归式:

     注:(3.4.3)式此时背包容量为j,可选择物品为i。此时在对xi作出决策之后,问题处于两种状态之一:     (1)背包剩余容量是j,没产生任何效益;     (2)剩余容量j-wi,效益值增长了vi ;      算法具体代码如下:

[cpp]  view plain copy
  1. //3d10-1 动态规划 背包问题  
  2. #include "stdafx.h"  
  3. #include <iostream>   
  4. using namespace std;   
  5.   
  6. const int N = 4;  
  7.   
  8. void Knapsack(int v[],int w[],int c,int n,int m[][10]);  
  9. void Traceback(int m[][10],int w[],int c,int n,int x[]);  
  10.   
  11. int main()  
  12. {  
  13.     int c=8;  
  14.     int v[]={0,2,1,4,3},w[]={0,1,4,2,3};//下标从1开始  
  15.     int x[N+1];  
  16.     int m[10][10];  
  17.   
  18.     cout<<"待装物品重量分别为:"<<endl;  
  19.     for(int i=1; i<=N; i++)  
  20.     {  
  21.         cout<<w[i]<<" ";  
  22.     }  
  23.     cout<<endl;  
  24.   
  25.     cout<<"待装物品价值分别为:"<<endl;  
  26.     for(int i=1; i<=N; i++)  
  27.     {  
  28.         cout<<v[i]<<" ";  
  29.     }  
  30.     cout<<endl;  
  31.   
  32.     Knapsack(v,w,c,N,m);  
  33.   
  34.     cout<<"背包能装的最大价值为:"<<m[1][c]<<endl;  
  35.   
  36.     Traceback(m,w,c,N,x);  
  37.     cout<<"背包装下的物品编号为:"<<endl;  
  38.     for(int i=1; i<=N; i++)  
  39.     {  
  40.         if(x[i]==1)  
  41.         {  
  42.             cout<<i<<" ";  
  43.         }  
  44.     }  
  45.     cout<<endl;  
  46.   
  47.     return 0;  
  48. }  
  49.   
  50. void Knapsack(int v[],int w[],int c,int n,int m[][10])  
  51. {  
  52.     int jMax = min(w[n]-1,c);//背包剩余容量上限 范围[0~w[n]-1]  
  53.     for(int j=0; j<=jMax;j++)  
  54.     {  
  55.         m[n][j]=0;  
  56.     }  
  57.   
  58.     for(int j=w[n]; j<=c; j++)//限制范围[w[n]~c]  
  59.     {  
  60.         m[n][j] = v[n];  
  61.     }  
  62.   
  63.     for(int i=n-1; i>1; i--)  
  64.     {  
  65.         jMax = min(w[i]-1,c);  
  66.         for(int j=0; j<=jMax; j++)//背包不同剩余容量j<=jMax<c  
  67.         {  
  68.             m[i][j] = m[i+1][j];//没产生任何效益  
  69.         }  
  70.   
  71.         for(int j=w[i]; j<=c; j++) //背包不同剩余容量j-wi >c  
  72.         {  
  73.             m[i][j] = max(m[i+1][j],m[i+1][j-w[i]]+v[i]);//效益值增长vi   
  74.         }  
  75.     }  
  76.     m[1][c] = m[2][c];  
  77.     if(c>=w[1])  
  78.     {  
  79.         m[1][c] = max(m[1][c],m[2][c-w[1]]+v[1]);  
  80.     }  
  81. }  
  82.   
  83. //x[]数组存储对应物品0-1向量,0不装入背包,1表示装入背包  
  84. void Traceback(int m[][10],int w[],int c,int n,int x[])  
  85. {  
  86.     for(int i=1; i<n; i++)  
  87.     {  
  88.         if(m[i][c] == m[i+1][c])  
  89.         {  
  90.             x[i]=0;  
  91.         }  
  92.         else  
  93.         {  
  94.             x[i]=1;  
  95.             c-=w[i];  
  96.         }  
  97.     }  
  98.     x[n]=(m[n][c])?1:0;  
  99. }  

     算法执行过程对m[][]填表及Traceback回溯过程如图所示:

      从m(i,j)的递归式容易看出,算法Knapsack需要O(nc)计算时间; Traceback需O(n)计算时间;算法总体需要O(nc)计算时间。当背包容量c很大时,算法需要的计算时间较多。例如,当c>2^n时,算法需要Ω(n2^n)计算时间。

         4、算法的改进      由m(i,j)的递归式容易证明,在一般情况下,对每一个确定的i(1≤i≤n),函数m(i,j)是关于变量j的阶梯状单调不减函数。跳跃点是这一类函数的描述特征。在一般情况下,函数m(i,j)由其全部跳跃点唯一确定。如图所示。

     对每一个确定的i(1≤i≤n),用一个表p[i]存储函数m(i,j)的全部跳跃点。表p[i]可依计算m(i,j)的递归式递归地由表p[i+1]计算,初始时p[n+1]={(0,0)}。       一个例子:n=3,c=6,w={4,3,2},v={5,2,1}。

     函数m(i,j)是由函数m(i+1,j)与函数m(i+1,j-wi)+vi作max运算得到的。因此,函数m(i,j)的全部跳跃点包含于函数m(i+1,j)的跳跃点集p[i+1]与函数m(i+1,j-wi)+vi的跳跃点集q[i+1]的并集中。易知,(s,t)∈q[i+1]当且仅当wi<=s<=c且(s-wi,t-vi)∈p[i+1]。因此,容易由p[i+1]确定跳跃点集q[i+1]如下:

q[i+1]=p[i+1]⊕(wi,vi)={(j+wi,m(i,j)+vi)|(j,m(i,j))∈p[i+1]}

    另一方面,设(a,b)和(c,d)是p[i+1]q[i+1]中的2个跳跃点,则当c>=a且d<b时,(c,d)受控于(a,b),从而(c,d)不是p[i]中的跳跃点。除受控跳跃点外,p[i+1]q[i+1]中的其他跳跃点均为p[i]中的跳跃点。

    由此可见,在递归地由表p[i+1]计算表p[i]时,可先由p[i+1]计算出q[i+1],然后合并表p[i+1]和表q[i+1],并清除其中的受控跳跃点得到表p[i]。

      例:n=5,c=10,w={2,2,6,5,4},v={6,3,5,4,6}。跳跃点的计算过程如下:

    初始时p[6]={(0,0)},(w5,v5)=(4,6)。因此,q[6]=p[6]⊕(w5,v5)={(4,6)}。 p[5]={(0,0),(4,6)}。q[5]=p[5]⊕(w4,v4)={(5,4),(9,10)}。从跳跃点集p[5]与q[5]的并集p[5]q[5]={(0,0),(4,6),(5,4),(9,10)}中看到跳跃点(5,4)受控于跳跃点(4,6)。将受控跳跃点(5,4)清除后,得到

     p[4]={(0,0),(4,6),(9,10)}      q[4]=p[4]⊕(6,5)={(6,5),(10,11)}      p[3]={(0,0),(4,6),(9,10),(10,11)}      q[3]=p[3]⊕(2,3)={(2,3),(6,9)}      p[2]={(0,0),(2,3),(4,6),(6,9),(9,10),(10,11)}      q[2]=p[2]⊕(2,6)={(2,6),(4,9),(6,12),(8,15)}      p[1]={(0,0),(2,6),(4,9),(6,12),(8,15)}      p[1]的最后的那个跳跃点(8,15)给出所求的最优值为m(1,c)=15。

    具体代码实现如下:

[cpp]  view plain copy
  1. //3d10-2 动态规划 背包问题 跳跃点优化  
  2. #include "stdafx.h"  
  3. #include <iostream>   
  4. using namespace std;   
  5.   
  6. const int N = 4;  
  7.   
  8. template<class Type>  
  9. int Knapsack(int n,Type c,Type v[],Type w[],int **p,int x[]);  
  10. template<class Type>  
  11. void Traceback(int n,Type w[],Type v[],Type **p,int *head,int x[]);  
  12.   
  13. int main()  
  14. {  
  15.     int c=8;  
  16.     int v[]={0,2,1,4,3},w[]={0,1,4,2,3};//下标从1开始  
  17.     int x[N+1];  
  18.   
  19.     int **p = new int *[50];  
  20.     for(int i=0;i<50;i++)    
  21.     {    
  22.         p[i] = new int[2];  
  23.     }   
  24.   
  25.     cout<<"待装物品重量分别为:"<<endl;  
  26.     for(int i=1; i<=N; i++)  
  27.     {  
  28.         cout<<w[i]<<" ";  
  29.     }  
  30.     cout<<endl;  
  31.   
  32.     cout<<"待装物品价值分别为:"<<endl;  
  33.     for(int i=1; i<=N; i++)  
  34.     {  
  35.         cout<<v[i]<<" ";  
  36.     }  
  37.     cout<<endl;  
  38.   
  39.     cout<<"背包能装的最大价值为:"<<Knapsack(N,c,v,w,p,x)<<endl;  
  40.   
  41.     cout<<"背包装下的物品编号为:"<<endl;  
  42.     for(int i=1; i<=N; i++)  
  43.     {  
  44.         if(x[i]==1)  
  45.         {  
  46.             cout<<i<<" ";  
  47.         }  
  48.     }  
  49.     cout<<endl;  
  50.   
  51.     for(int i=0;i<50;i++)    
  52.     {    
  53.         delete p[i];  
  54.     }   
  55.   
  56.     delete[] p;  
  57.   
  58.     return 0;  
  59. }  
  60.   
  61. template<class Type>  
  62. int Knapsack(int n,Type c,Type v[],Type w[],int **p,int x[])  
  63. {  
  64.     int *head = new int[n+2];  
  65.     head[n+1]=0;  
  66.   
  67.     p[0][0]=0;//p[][0]存储物品重量  
  68.     p[0][1]=0;//p[][1]存储物品价值,物品n的跳跃点(0,0)  
  69.   
  70.     // left 指向p[i+1]的第一个跳跃点,right指向最后一个  
  71.     //拿书上的例子来说,若计算p[3]=0;则left指向p[4]的第一跳跃点(0 0)right指向(9,10)  
  72.     int left = 0,right = 0,next = 1;//next即下一个跳跃点要存放的位置  
  73.     head[n]=1;//head[n]用来指向第n个物品第一个跳跃点的位置  
  74.   
  75.     for(int i=n; i>=1; i--)  
  76.     {  
  77.         int k = left;//k指向p[ ]中跳跃点,移动k来判断p[]与p[]+(w v)中的受控点  
  78.         for(int j=left; j<=right; j++)  
  79.         {  
  80.             if(p[j][0]+w[i]>c) break;//剩余的空间不能再装入i,退出for循环;  
  81.             Type y = p[j][0] + w[i],m = p[j][1] + v[i];//计算p[ ]+(w v)  
  82.   
  83.             //若p[k][0]较小则(p[k][0]  p[k][1])一定不是受控点,将其作为p[i]的跳跃点存储  
  84.             while(k<=right && p[k][0]<y)  
  85.             {  
  86.                 p[next][0]=p[k][0];  
  87.                 p[next++][1]=p[k++][1];  
  88.             }  
  89.   
  90.             //如果 p[k][0]==y而m<p[k][1],则(y m)为受控点不存  
  91.             if(k<=right && p[k][0]==y)  
  92.             {  
  93.                 if(m<p[k][1])//对(p[k][0]   p[k][1])进行判断  
  94.                 {  
  95.                     m=p[k][1];  
  96.                 }  
  97.                 k++;  
  98.             }  
  99.   
  100.             // 若p[k][0]>=y且m> =p[k][1],判断是不是当前i的最后一个跳跃点的受控点  
  101.             //若不是则为i的跳跃点存储  
  102.             if(m>p[next-1][1])  
  103.             {  
  104.                 p[next][0]=y;  
  105.                 p[next++][1]=m;  
  106.             }  
  107.   
  108.             //若是,则对下一个元素进行判断。  
  109.             while(k<=right && p[k][1]<=p[next-1][1])  
  110.             {  
  111.                 k++;  
  112.             }  
  113.         }  
  114.   
  115.         while(k<=right)  
  116.         {  
  117.             p[next][0]=p[k][0];  
  118.             p[next++][1]=p[k++][1];//将i+1剩下的跳跃点作为做为i的跳跃点存储  
  119.         }  
  120.   
  121.         left = right + 1;  
  122.         right = next - 1;  
  123.   
  124.         // 第i-1个物品第一个跳跃点的位置  head[0]指第0个物品第一个跳跃点的位置  
  125.         head[i-1] = next;  
  126.     }  
  127.   
  128.     Traceback(n,w,v,p,head,x);  
  129.     return p[next-1][1];  
  130. }  
  131.   
  132. //x[]数组存储对应物品0-1向量,0不装入背包,1表示装入背包  
  133. template<class Type>  
  134. void Traceback(int n,Type w[],Type v[],Type **p,int *head,int x[])  
  135. {  
  136.     //初始化j,m为最后一个跳跃点对应的第0列及第1列  
  137.     //如上例求出的 最后一个跳跃点为(8 15)j=8,m=15  
  138.     Type j = p[head[0]-1][0],m=p[head[0]-1][1];  
  139.     for(int i=1; i<=n; i++)  
  140.     {  
  141.         x[i]=0;// 初始化数组;  
  142.         for(int k=head[i+1]; k<=head[i]-1;k++)// 初始k指向p[2]的第一个跳跃点(0 0)  
  143.         {  
  144.             //判断物品i是否装入,如上例与跳跃点(6 9)相加等于(8 15)所以1装入  
  145.             if(p[k][0]+w[i]==j && p[k][1]+v[i]==m)  
  146.             {  
  147.                 x[i]=1;//物品i被装入,则x[i]置1  
  148.                 j=p[k][0];// j和m值置为满足if条件的跳跃点对应的值  
  149.                 m=p[k][1];// 如上例j=6,m=9  
  150.                 break;//再接着判断下一个物品  
  151.             }  
  152.         }  
  153.     }  
  154. }  

     上述算法的主要计算量在于计算跳跃点集p[i](1≤i≤n)。由于q[i+1]=p[i+1]⊕(wi,vi),故计算q[i+1]需要O(|p[i+1]|)计算时间。合并p[i+1]和q[i+1]并清除受控跳跃点也需要O(|p[i+1]|)计算时间。从跳跃点集p[i]的定义可以看出,p[i]中的跳跃点相应于xi,…,xn的0/1赋值。因此,p[i]中跳跃点个数不超过2^(n-i+1)。由此可见,算法计算跳跃点集p[i]所花费的计算时间为从而,改进后算法的计算时间复杂性为O(2^n)。当所给物品的重量wi(1≤i≤n)是整数时,|p[i]|≤c+1,(1≤i≤n)。在这种情况下,改进后算法的计算时间复杂性为O(min{nc,2^n})。

    运行结果如图:

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值