[算法描述]
0-1背包问题是子集选取问题。一般情况下,0-1背包问题是NP完全问题。0-1背包问题的解空间可以用子集树表示。解0-1背包问题的回溯法与解装载问题的回溯法十分相似。在搜索解空间树时,只要其左儿子节点是一个可行的节点,搜索就进入其左子树;而当右子树中有可能包含最优解时才进入右子树搜索,否则将右子树剪去。设r是当前剩余物品价值总和;cp是当前价值;bestp是当前最优价值。当cp+r<=bestp时,可剪去右子树。计算右子树中解的上界的更好的办法是,将剩余物品依其单位重量价值排序,然后依次装入物品,直至装不下时,再装入该物品的一部分而装满背包,由此得到的价值是右子树的上界。
0–1背包的一个实例:n=5, c=10, w={2, 2, 6, 5, 4}, v(p)={6, 3, 5, 4, 6}的0-1背包问题的最优解和最优值。
[回溯法基本思想]
确定了解空间的组织结构后,【回溯法从开始节点(根节点)出发,以深度优先搜索整个解空间。这个开始的节点为活节点,同时成为当前的扩展节点。在当前的扩展节点处,搜素向纵深方向移至一个新节点。这个新节点就成为新的活节点,并成为当前扩展节点。如果当前节点处不能再向纵深方向移动,则当前扩展节点为死节点。此时,应往回移动到最近的一个活节点处。回溯法以这种方式递归的在解空间中搜素,直至找到所有符合要求的解或解空间中已无活节点。】(即深度优先搜素)
【优化方法】
剪枝(一):当前决策放入的物品总重量已经大于背包容量时,没有必要继续决策,此时可以将其左子树剪掉。
剪枝(二):如果将当前扩展节点后剩下的所有物品都装入还没有目前已求得的最优值大的话,就不在进行决策了,直接返回。
递归回溯时,在当前扩展节点处会通过设置约束函数和限界函数。不满足条件的,剪去相应的子树
【0-1背包算法分析】
对于0-1背包问题,可用一颗完全二叉树表示其解空间,针对上述实例(n=5),解空间树有32个可能的解,解空间树如下图所示。


经剪枝(二)(红框框)先按照单位价值对物品进行从达到下排序。然后按照背包问题计算当前节点的上界,仅当进入右子树时才计算,以判断是否可将右子树剪去,进入左子树时不需要计算上界,因为其上界与其父节点相同。

当前节点的上界 详细计算如下图所示:

这点理解很重要(区别于动态规划)
【0-1背包代码描述】
template
class Knap{ friend Typep Knapsack(Typep*,Typew*,Typew,int); private: Typep Bound(int i); void Backtrace(int i); Typew c;// 背包容量 int n;// 物品数 Typew *W;// 物品重量数数组 Typep *p;// 物品价值数组 Typew cw;// 当前重量 Typep cp;// 当前价值 Typep bestp;// 当前最优值 }; template
void Knap
::Backtrace(int i){ if(i > n){ //到达叶子结点 bestp=cp; return; } if(cw + w[i] <= c){ // 进入左子树 cw += w[i]; cp += p[i]; Backtrace(i+1); cw -= w[i]; cp -= p[i]; } if(Bound(i+1) > bestp) // 进入右子树 Backtrace(i+1); } template
Typep Knap
::Bound(int i){ // 计算上界 Typew cleft = c - cw; // 剩余容量 Typep b = cp; while(i <= n&&w[i] <= cleft){ // 以物品单位重量价值递减装入物品 cleft -= w[i]; b += p[i]; i++; } if(i <= n) // 装满背包 b += p[i]*cleft/w[i]; return b; } 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
Typep Knapsack(Typep p[],Typew w[],Typew c,int n){ // 为Knap::Backtrace初始化 Typew W = 0; Typep 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 += p[i]; W += w[i]; } if(W <= c) return P; // 装入所有物品 Sort(Q,n); // 按物品单位重量价值排序(从大到小) Knap
K; K.p = new Typep[n+1]; K.w = new Typew[n+1]; for(int i=1;i
【实例算法(害,偷个懒)】
// n=5, c=10, w={2, 2, 6, 5, 4}, v={6, 3, 5, 4, 6}的0-1背包问题的最优解和最优值。 #include
using namespace std; #define N 10 int w[N];//重量 int v[N];//价值 int x[N];//1表放入背包,0表不放入 int n,c;//n:物品个数 c:背包的最大容量 int cw=0;//当前物品总重 int cv=0;//当前物品总价值 int bestp=0;//当前最大价值 int bestx[N];//最优解 //回溯函数 k表示当前处在第几层做选择,k=1时表示决定是否将第一个物品放入背包 void backtrack(int k) {//叶子节点,输出结果 if(k>n){//找到一个更优的解 if(cv>bestp){//保存更优的值和解 bestp = cv; for(int i=1; i<=n; i++) bestx[i] = x[i]; } } else{//遍历当前节点的子节点 for(int i=0; i<=1; i++){ x[k]=i; if(i==0){ backtrack(k+1); } else{//约束条件:当前物品是否放的下 if((cw+w[k])<=c){ cw += w[k]; cv += v[k]; backtrack(k+1); cw -= w[k]; cv -= v[k]; } } } } } int main() { cout<<"请输入物品的个数:"; cin>>n; cout<<"请输入每个物品的重量及价值:"<
>w[i]>>v[i]; } cout<<"请输入背包的限制容量:"; cin>>c; backtrack(1); cout<<"最优值是:"<
结果:

【时间和空间复杂度分析】
计算上界Bound()函数需要O(n)时间,在最坏的情况下有O(2^n)个右子节点需要计算上界,所以解0-1背包的回溯算法法BackTrack需要的计算时间为O(n2^n)。实际上,最坏情况下,剪枝并不会改变时间复杂度。个人感觉0-1背包还是动态规划算法比较好吧。
发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/176605.html原文链接:https://javaforall.net
