1、问题说明
0-1背包问题是:从n个重量分别为wi、价值分别为pi的物品中选取部分物品装入总容量为c的背包中,使背包中物品总重量不超过背包的总容量且所物品的总价值最高,即在满足承重条件下使价值最大。假设用xi = 1表示物品i装入背包中,xi = 0表示物品i不装入背包,因此该问题需要求出xt的值,即各物品装入与否的情况。
1、分而治之算法
分而治之的思想类似于软件的模块化设计方法,它将一个大问题分解成多个小问题,然后由小问题的解获得大问题的解。通常,由分而治之算法所得到的小问题与原来的大问题具有相同的类型,因此当分解后的小问题相对而言还是较大时,可根据需要对小问题继续采用分而治之的方法来求解。
结论:该算法不适用于该问题,因为无法通过子问题获取大问题的解。
现在假设一种情况分析:
w[12, 10, 8, 4] w_left[12, 10] w_right[8, 4]
pi[10, 10, 10, 10] pi_left[10, 10] pi_right[10, 10]
c = 30
现在把c分成两部分承重都是15,w和p也从中间分开;
得到left最优c承重方案是12 = 12,right得到 12 = 8 + 4;
左右相加的到承重是24 = 12+12;
但是明显可以选择30 = 12 + 10 + 8;
无法根据分拆的问题得到大问题的解;
2、贪婪算法
0-1背包问题可有几种贪婪策略。
第一种为价值贪婪准则,即每次都从剩余物品中选择价值最大的物品装入背包。在此规则下,物品按照其价值由大到小依次装入背包,直到物品重量超过背包的最大容量。这种策略不能保证得到最优解。例如,n = 2,w=[100, 10, 10],p=[20, 15,15],c=105。当利用价值贪婪准则时,获得的解为x= [1, 0, 0],这种方案的总价值为20。而最优解为[0, 1, 1],其总价值为30。
第二种为重量贪婪准则,即每次都从剩余物品中选择重量最小的物品装入背包。这种策略虽然对前面的例子可产生最优解,但在一般情况下不一定能得到最优解。例如,n = 2, w = [10, 20],p = [5, 100],c = 25。当利用重量贪婪准则时,获得的解为x = [1, 0],比最优解[0, 1]要差。
第三种为价值密度pi/wi准则,即每次从剩余物品中选择pi/wi值最大的物品装入背包。这种策略也不能保证得到最优解。
结论:该算法不适用于求解0-1背包问题,可用于求解一个比较合理的解空间判断条件,例如可以使用价值密度求一个解,然后使用其他方法判断如果明确还不如这个解的分支就可以不在继续下去了。
3、动态规划
动态规划(dynamic programming)与分治方法相似,都是通过组合子问题的解来求解愿问题。
分治方法是将问题划分为互不相交的子问题,递归的求解子问题,再将它们的解组合起来,求出愿问题的解。
动态规划应用与子问题重叠的情况,即不同的子问题具有公共的子子问题。
结论:可用于求解0-1背包问题;
代码
f和x的迭代计算
f[i][y] 记录当移动到索引i是不同的剩余承重y对应可用的权重和
void Knapsack(T p[ ], int w[ ], int c, int n, T** f)
{//对于所有i和y计算f[i][y]
//初始化f[n-1][ ]
for(int y=0; y<=c; y++) f[n-1][y]=0; //初始化为0
for(y=w[n-1]; y<=c; y++) f[n-1][y]=p[n-1]; //剩余可包含最后一个物品的设置为其权重
//计算剩下的f
for(int i=n-1; i >1; i--) {
for(int y=0; y<=c; y++) f[i-1][y]=f[i][y]; //初始化设置为下一个的剩余权重
for(y=w[i-1]; y<=c; y++)
f[i-1][y]=max(f[i][y], f[i][y-w[i-1]]+p[i-1]);//加入当前权重获取一个最大值 动态规划就在这里体现
}
f[0][c]=f[1][c];
if(c>=w[0]) f[0][c]=max(f[1][c], f[1][c-w[0]]+p[0]);
}
template<class T>
void Traceback(T **f, int w[ ], int c, int n, int x[ ])
{//计算x 包含物品序列
for(int i=0; i<n-1; i++)
if(f[i][c]==f[i+1][c]) x[i]=0; else { x[i]=1; c-=w[i]; }
x[n-1]=(f[n-1][c])? 1:0;
}
4、回溯法
回溯是一个既带有系统性又带有跳跃性的搜索法。其基本思想是,按照深度优先策略从根结点出发搜索解空间树,当搜索到任一结点时,先判断该结点是否包含问题的解,如果不包含,则跳过以该结点为根的子树的搜索,逐层向根结点回溯;否则进入该子树,继续按深度优先策略进行搜索。如果要找到问题的所有解,则必须回溯到根,并搜索完根结点的所有子树才结束,而如果只求解问题的一个解,则只要搜索到问题的一个解便可结束。
此回溯过程也可通过结点扩展的方式进行描述:在一个问题的解空间中从根结点开始,开始结点既是一个活结点又是一个扩展结点,如果能从当前的扩展移动到一个新结点,那么这个新结点将变成一个活结点和新的扩展,旧的扩展仍是一个活结点;如果不能移到一个新结点,当前的扩展就“死”了(即不再是一个活结点),那么便只能返回到最近被考察的活结点(回溯),这个活结点变成了新的扩展。当找到了答案或者回溯尽了所有的活结点时,搜索过程结束。
回溯算法的一个特点是在搜索执行的同时产生解空间。在搜索期间的任何时刻,仅保留从开始结点到当前结点的路径,因此回溯算法的空间需求为O(从开始结点起最长路径的长度)。这个特性非常重要,因为解空间的大小通常是最长路径长度的指数或阶乘,如果要存储全部解空间的话,再多的空间也不够用。
代码
基本类定义
template<class Tw, class Tp>
class Knap {
friend Tp Knapsack(Tp *, Tw *, Tw, int);
private:
Tp Bound(int i);
void Knapsack(int i);
Tw c; //背包容量
int n; //对象数目
Tw *w; //对象重量的数组
Tp *p; //对象收益的数组
Tw cw; //当前背包的重量
Tp cp; //当前背包的收益
Tp bestp; //迄今最大的收益
} ;
返回子树中最优叶子的上限值
template<class Tw, class Tp>
Tp Knap<Tw, Tp>:: Bound(int i)
{//返回子树中最优叶子的上限值Return upper bound on value of
//best leaf in subtree.
Tw cleft=c-cw; //剩余容量
Tp 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;
}
递归调用执行各分支
template<class Tw, class Tp>
void Knap<Tw, Tp>:: Knapsack(int i)
{//从第i层结点搜索
if(i>n) { bestp=cp; return; } //在叶结点上
//检查子树
if(cw+w[i]<=c) {//尝试x[i]=1
cw+=w[i]; cp+=p[i]; Knapsack(i+1); cw-=w[i]; cp-=p[i]; }
if(Bound(i+1)> bestp) Knapsack(i+1); //尝试x[i]=0
}
用到的计算密度的对象定义
class Object {
friend int Knapsack(int *, int *, int, int);
public:
int operator<=(Object a) const
{ return(d>=a.d); }
private:
int ID; //对象号
float d; //收益密度
};
主函数
template<class Tw, class Tp>
Tp Knapsack(Tp p[ ], Tw w[ ], Tw c, int n)
{//返回最优装包的值
//初始化
Tw W=0; //记录重量之和
Tp P=0; //记录收益之和
//定义一个按收益密度排序的对象数组
Object *Q=new Object [n];
for(int i=1; i<=n; i++) {
//收益密度的数组
Q[i-1].ID=i; Q[i-1].d=1.0*p[i]/w[i]; //注意到p[0],w[0]不使用
P+=p[i]; W+=w[i];
}
if(W<=c) return P; //可容纳所有对象
MergeSort(Q, n); //按密度排序
//创建K n a p的成员
Knap<Tw, Tp> K; K.p=new Tp [n+1]; K.w=new Tw [n+1];
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.Knapsack(1);
delete [ ] Q; delete [ ] K.w; delete [ ] K.p;
return K.bestp;
}
5、分枝定界法
分枝定界法和前面介绍的回溯法相似,也是在解空间中求出问题的解,但不同的是,分枝定界法一般采用广度优先或最小耗费方法来搜索解空间的树结构。相对而言,分枝定界法的解空间比回溯法的大得多,因此当内存容量有限时,回溯法成功的可能性更大。
分枝定界法是另一种系统地搜索解空间的方法,其解空间树也为一棵有序树,如排序树或子集树。它与回溯法的主要区别在于对当前扩展结点所采用的扩展方式不同。在分枝定界法中,每一个活结点有且仅有一次机会变成扩展结点,活结点一旦变为扩展结点,就会一次性地生成其所有的孩子结点(即新结点),在这些孩子结点中,那些不可能导出可行解或最优解的结点将被舍弃,其余的孩子结点被加入活结点表中。然后从表中选择一个结点作为下一个扩展结点,并重复上述结点的扩展过程,直到找到所需要的解或活动表为空时结束。
从活结点表中选择下一个扩展结点通常有两种方式。
①先进先出(FIFO)法。即从活结点表中取出结点的顺序与加入结点的顺序相同,因此活结点表的性质与队列的相同。
②最小耗费或最大收益法。由于解空间中每个结点都有一个对应的耗费或收益,可将活结点表组织成一个优先队列,并根据结点的耗费所确定的结点优先级别来选取下一个结点。如果查找的是具有最小耗费的解,则活结点表可用最小堆来建立,下一个扩展结点就是具有最小耗费的活结点;如果查找的是具有最大收益的解,则可用最大堆来构造活结点表,下一个扩展结点便是具有最大收益的活结点。
代码:使用最大堆
类基本数据定义
template<class Tw, class Tp>
class Knap {
public:
friend Tp Knapsack(Tp *, Tw *, Tw, int);
Tp MaxProfitKnapsack();
Knap(int c, int*w, int *p, int num);
private:
Tp Bound(int i);
void Knapsack(int i);
Tw c; //背包容量
int n; //对象数目
Tw *w; //对象重量的数组
Tp *p; //对象收益的数组
Tw cw; //当前背包的重量
Tp cp; //当前背包的收益
int *bestx;
} ;
活结点
template<class Tw, class Tp>
class HeapNode1{
public:
operator Tw() const { return uprofit; }
bbnode *ptr; //活结点指针
int level; //活结点所在层
Tp uprofit; Tw weight; Tp profit;
};
AddLiveNode3
template<class Tw, class Tp>
void AddLiveNode3(MaxHeap<HeapNode1<Tw, Tp>>&H, bbnode *E, Tpup, Tpcp, Tw cw, bool ch, int lev)
{//向最大堆H中增添一个层为lev,上限重量为wt的活结点
//新结点是E的一个孩子
//当且仅当新结点是左孩子时,ch为true
bbnode *b=new bbnode; b->parent=E; b->LChild=ch;
HeapNode1<Tw, Tp> N;
N.uprofit=up; N.profit=cp; N.weight=cw; N.level=lev; N.ptr=b; H.Insert(N);
}
Knap初始化
template<class Tw, class Tp>
Knap<Tw, Tp>:: Knap(int cc, int*ww, int *pp, int num)
{ n=num; c=cc; w=ww; p=pp; cw=0; cp=0; }
主函数
template<class Tw, class Tp>
Tp Knap<Tw, Tp>:: MaxProfitKnapsack()
{//返回背包最优装载的收益
//bestx[i]=1当且仅当物品i属于最优装载
//使用最大收益分枝定界算法
//定义一个最多可容纳1000个活结点的最大堆
MaxHeap<HeapNode1<Tp, Tw>>*H;
bbnode * E;
H=new MaxHeap<HeapNode1<Tp, Tw> >(1000);
bestx=new int [n+1]; //为bestx分配空间
//初始化层1
int i=1; E=0; cw=cp=0; Tp bestp=0; //目前的最优收益
Tp up=Bound(1); //在根为E的子树中最大可能的收益
//搜索子集空间树
//搜索可能收益最大的节点,每个循环添加两个堆节点 包含当前i节点和不包含当前i节点
while(i!=n+1) { //不是叶子
//检查左孩子
Tw wt=cw+w[i];
if(wt<=c) { //可行的左孩子
if(cp+p[i]>bestp) bestp=cp+p[i];
AddLiveNode3(*H, E, up, cp+p[i], cw+w[i], true, i+1);
}
up=Bound(i+1);
//检查右孩子
if(up>=bestp) //右孩子有希望
AddLiveNode3(*H, E, up, cp, cw, false, i+1);
//取下一个扩展结点
HeapNode1<Tp, Tw> N;
H->DeleteMax(N); //不能为空
E=N.ptr; cw=N.weight; cp=N.profit; up=N.uprofit; i=N.level; //E i指出下一个节点的父节点和索引位置
}
//沿着从扩展结点E到根的路径构造bestx[ ]
for(int j=n; j>0; j--) { bestx[j]=E->LChild; E=E->parent; }
return cp;
}
参考《数据结构、算法与应用c++语言描述》