0-1背包问题的三种解法

时间:2022-06-11 18:42:26

0-1背包问题除了耳熟能详的动态规划,还有回溯法和分支限界法

1. 动态规划

使用动态规划,要求原问题具有最优子结构性质,即原问题的最优解包含了其子问题的最优解;并且子问题有重叠的部分,会被重复计算。

0-1背包问题是要求将N个具有一定价值v_i、重量w_i的物品放入承重为C的背包中(放则x_i=1,不放则x_i=0),使得价值最大,即max sum_N(x_i*v_i)   s.t sum_N(x_i*w_i)<=C,其子问题可写为将i个具有一定价值v_i、重量w_i的物品放入承重为j的背包中(放则x_i=1,不放则x_i=0),使得价值最大,即max sum_i(x_i*v_i)   s.t sum_i(x_i*w_i)<=j。若原问题的最大价值记为c[N][C],则子问题的最大价值可记为c[i][j]。

动态规划的求解思路是自底向上,即先求解最小的子问题,再基于已经求解的子问题求解下一个子问题的最优解。为了利用已经求解的子问题,可以建一个表来记录。

对于0-1背包问题,可建立如下递归式

0-1背包问题的三种解法

主程序如下:

int KnapSack(int n, int w[ ], int v[ ]) {
     for (i=0; i<=n; i++)   //初始化第0列
           V[i][0]=0;
     for (j=0; j<=C; j++)   //初始化第0行
           V[0][j]=0;
   for (i=1; i<=n; i++)   //计算第i行,进行第i次迭代
           for (j=1; j<=C; j++)
                if (j<w[i])   V[i][j]=V[i-1][j];         
                else   V[i][j]=max(V[i-1][j], V[i-1][j-w[i]]+v[i]);

     j=C;    //求装入背包的物品
     for (i=n; i>0; i--){
         if (V[i][j]>V[i-1][j]) {
              x[i]=1;
              j=j-w[i];
         }
         else x[i]=0;
     }
     return V[n][C];    //返回背包取得的最大价值
}

 

2. 回溯法

回溯法类似深度优先搜索,一股脑往前冲,路走不通了就回头。常用于解决两种问题:寻找满足条件的子集(比如背包问题,x_i=0|1|...),或者排列的方式(比如TSP问题,1234还是2341还是....),根据这两种问题有子集树和排列树两种模板可用。 

0-1背包问题的三种解法

利用子集树求解0-1背包问题,主要还是如何剪枝。即,在什么情况下会选择不搜索左子树(放弃x_i=1),在什么情况下会选择不搜索右子树(放弃x_i=0)。

当选择x_i=1,背包就会超重时,就会放弃搜索左子树;当选择x_i=0,当前背包的价值和剩余未放入物品(不包括物品i,因为已经选择不放)的价值总和都小于目前最佳价值时,说明已经预测到这个没有前景了==,可以放弃继续搜索右子树了。

主程序如下:

int Bound(int i){
    int wr = c - wc;  // 背包剩余容量
    int vb = vc;        // vc为当前背包价值
    // 按单位价值递减顺序装入物品
    while(i <= n && w[i] <= wr){
        wr -= w[i]; vb += v[i]; ++i;
    }
    if(i <= n) vb += (v[i]/w[i])*wr; // 继续装满背包
    return vb; // 返回背包价值上界
}
backtrack(int i){
    if( i > n) {   // vc当前背包价值,m当前最优价值
        m = ( m < vc )? vc : m;   output(x);
    } else {  // wc当前背包重量
        if ( wc + w[i] <= C ) {  // 左子树(将 i 放入背包)
            x[i]= 1; wc += w[i]; vc += v[i];
            backtrack(i+1);
            x[i]= 0; wc -= w[i]; vc -= v[i];
        }
        if(Bound(i+1) > m){  // 右子树(拿出物品i)
            backtrack(i+1);
        }
    }
}

 

3. 优先队列式分支限界法  

分支限界类似广度优先搜索,其原理在《对比Dijkstra和优先队列式分支限界》中已经介绍。

对于0-1背包问题,每个结点的邻接点只有两个,即0和1,记为左子树和右子树。

关键在于什么时候剪枝,即什么情况下不访问左子树,什么情况下不访问右子树,这一点和回溯法类似,判断条件也是一样的。

区别在于:1. 分支限界法要求先对输入物品的单位重量价值做非减排序;2. 分支限界法采用的优先队列式分支限界法,上界值(也就是回溯法用到的Bound(i))越高排得越靠前。这样做的好处是避免了回溯法的盲目搜索。对于回溯法来说,每次结果都只和最好的目标值进行比较,关注的是结果,而分支限界对每一步都有期待(感觉好励志...)

主程序如下:

while (i != n+1) { // 非叶结点
      // 检查当前扩展结点的左儿子结点
      Typew wt = cw + w[i];
      if (wt <= c) { // 左儿子结点为可行结点
         if (cp+p[i] > bestp) bestp = cp+p[i];
         AddLiveNode(up, cp+p[i], cw+w[i], true, i+1);}
         up = Bound(i+1);
      // 检查当前扩展结点的右儿子结点
      if (up >= bestp) // 右子树可能含最优解
           AddLiveNode(up, cp, cw, false, i+1);
         //   取下一个扩展结点(略)}

 

 

PS:今晚参加了个就业分享会,大神和学渣齐聚一堂。感受到了人与人之间的差异,以及某种不可言说的命运的力量。以及人生的不可预知性,无法猜测最终的终点和人生轨迹会是什么样,也无法想象身边会有个什么样的人来到。

唯一能做的事,就是做好当下,好好珍惜。