LeetCode算法题7.1:动态规划1,0-1背包


前言

      本文章对0-1背包问题做整理和总结。

0-1背包

      给定一些物品,其重量由数组元素 w[i] 分别表示,物品价值由 v[i] 表示,背包容量 W,其中每种类型物品只有一种(重量和价值均相同),种类数为 w.length 或 v.length,将这些物品装入背包,最终能够得到的物品总价值为多少?

用回溯解决

      直接采用回溯方法来做,每一个物品可以选择是否放还是不放,这是一个组合问题,暴力求解参考参考代码如下:

public class Tttest {
    int curV=0,ans=0;
    public int solution(int[] w,int[] v,int W) { //回溯法来计算背包问题。
        solve(w,v,W,0,0);
        return ans;
    }
    public void solve(int[] w,int[] v,int W,int curW,int begin){
        for(int i=begin;i<w.length;i++){
            if(curW+w[i]>W)
                continue;
            curW+=w[i];
            curV+=v[i];
            ans=Math.max(curV,ans);
            solve(w,v,W,curW,i+1);
            curW-=w[i];
            curV-=v[i];
        }
    }
    public static void main(String[] args) {
        Tttest t=new Tttest();

        int[] weight = new int[] {2, 6, 1, 5, 4};
        int[] value = new int[] {5, 9, 2, 10, 11};
        int cap = 10;
        int ans=t.solution(weight,value,cap);
        System.out.println(ans);
    }
}

a,0-1背包解决思路

问题分析

      如果读者对 0-1 背包的有一些了解,那先忘掉吧。现在呢,将目光放在这个问题本身:每个物品有自身的重量和价值,求将 n 个物品放入大小为 W 的背包中所能获得的最大总价值?

      问题也很好理解,不就是放物品嘛,很自然的想到那就一个个往背包里中放入物品(这句话意味着物品放入是有序的),而每一个物品是否能够放入有两个约束条件:能否放入背包内,能否使得总价值最大。

      那么一个物品 i 能否放入和之前的物品放入结果有关系,注意:这里说 物品 i 放入背包,意味着 前 i-1 件物品都已经考虑过了是否放入背包。这是有序放入的含义。

      如果不能放入的话(或者说不选择放入的话),那么此时能得到的总价值就等于上一件物品放入时得到的总价值(即前 i-1 件物品放入背包所能得到的最大值);如果可以放入的话(或者说选择放入的话),因为在背包中放入 i 会导致背包可用容量的减少,所以总价值为 i 的价值为上一件物品放入容量为 W-w[i] 大小背包的总价值(前 i-1 件物品放入 W-w[i] 容量的背包) + v[i]。然后在放入和不放入时取最大值,便为放入物品 i 时的最大总价值。

      那么现在应该能得到此问题的状态描述了,关键字:当前物品 放入某个容量的背包上一件物品放入 某个容量的背包容量

      最优子结构,一个大问题由一些在描述上基本一样的子问题来解决,在此为:第几个物品,某个具体容量的背包。
      重叠子问题,很轻易地能够发现子问题的结果是需要被复用的。
      无后效性,在某个特定容量背包放入第 i 个物品所能得到的最大总价值是固定的,在解决该问题的任何时候都不会发生改变。

状态描述

      在之前的文章(https://blog.csdn.net/Little_ant_/article/details/124231043)中,对于凑零钱问题的描述为:凑出某个数的答案和凑出一些数 x,y,z…的答案有关,所以凑零钱问题的状态就为凑出某个数 n 的答案,即 f(n)。

      0-1背包问题的状态和凑零钱问题的区别是它具有两个影响因素而已,第几个物品,多大的背包。

      此问题的状态描述:第 i 个物品放入容量为 j 的背包所能得到的最大总价值。我们用符号 f(i,j)来表示。注意:刚才分析的时候,往背包放入物品是按序放入的哦!

      有时候它也被描述成前 i 个物品放入容量为 j 的背包所能得到的最大总价值。

状态转移

      按照刚才的分析过程,写出转移方程为:f(i,j)= max(f(i-1,j),f(i-1,j-w[i])+ v [i])。

a,推导-背包大小

      值得一提的是,在刚才的问题分析采用的自顶而下的方式,而后面的推导采用的均是自底而上。

      对于具体问题:w = {2, 6, 1, 5, 4},v = {5, 9, 2, 10, 11},W = 10,回顾一下前面的状态定义,我们从背包大小开始做推导:

      初始化:当背包大小为 0 时,没有任何一个物品能够装入背包,此时的总价值均为 0 ,所以 dp [i] [0] 也全为 0。

      当背包大小为 1 时,
w [0] : 无法放入,此时总价值为 0,dp[0][1]=0。
w [1] : 无法放入,此时总价值为 0,dp[1][1]=0。
w [2] : 可以放入:2,不放入:0。总价值最大嘛,所以选择放入,那么此时总价值为 2 ,dp[2][1]=2。
w [3] : 无法放入:此时总价值为之前放入 w [ 2 ] 时的总价值 2,dp[3][1]=2。(即为 dp[2][1]
w [4] : 无法放入:此时总价值为之前放入 w [ 3 ] 时的总价值 2,dp[4][1]=2。
那如果背包大小不为 10 而是 1 的话,此时可以得到最终答案为 dp[4][1]=2。这里的下标 4 表示将所有物品都考虑过了。

      当背包大小为 2 时,
w [0] : 可以放入:5,不放入:0。此时总价值为 5,dp[0][2]=5。
w [1] : 无法放入,此时总价值为之前放入 w [ 0 ] 时的总价值 5,dp[1][2]=5。
w [2] : 可以放入:需要在背包中腾出大小为 1 的空间,但此时背包已满,余量为 0 ,那么就拿出大小为 2 的 w [ 0 ],装入 w [ 2 ],此时总价值为 2,(= dp[1][1]+v[2])不放入:dp[1][2]=5。所以总价值dp[2][2]=5。
w [3] : 无法放入:此时总价值为之前放入 w [ 2 ] 时的总价值 5,dp[3][2]=5。
w [4] : 无法放入:此时总价值为之前放入 w [ 3 ] 时的总价值 5,dp[4][2]=5。

      当背包大小为 3 时,
w [0] : 可以放入:5,不放入:0。此时总价值为 5,dp[0][3]=5。
w [1] : 无法放入,此时总价值为之前放入 w [ 0 ] 时的总价值 5,dp[1][3]=5。
w [2] : 可以放入:此时总价值为 5+2=7,(= dp[1][2]+v[2])不放入:dp[1][3]=5。所以总价值dp[2][3]=7。
w [3] : 无法放入:此时总价值为之前放入 w [ 2 ] 时的总价值 7,dp[3][4]=7。
w [4] : 无法放入:此时总价值为之前放入 w [ 3 ] 时的总价值 7,dp[4][4]=7。

      最后的 dp 数组如下所示:

012345678910
000555555555
100555599141414
2025777911141616
302577101215171717
4025711131618182123

      最后的 dp[4][10] =23 为最终结果。

代码

      将上面思路转为为代码,如下:

public class Tttest {
    public int solution1(int[] w,int[] v,int W) {
        int num=w.length;
        int[][] dp=new int[num][W+1];

        for(int j=1;j<=W;j++){//当前背包容量为 j
            if(w[0]<=j)//由于第一件物品下标从 0 开始,在此需要做初始化。
                dp[0][j]=v[0];
            for(int i=1;i<num;i++){ //保存从 [0,i] 件物品放入背包 j 能得到的最大价值
                if(w[i]<=j)
                    dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);
                else
                    dp[i][j]=dp[i-1][j];
            }
        }
        return dp[num-1][W];
    }
    public static void main(String[] args) {
        Tttest t=new Tttest();
        //注意此时物品是从下标0开始的。
        int[] weight = new int[] {2, 6, 1, 5, 4};
        int[] value = new int[] {5, 9, 2, 10, 11};
        int cap = 10;
        int ans=t.solution1(weight,value,cap); //DP
//        int ans=t.solution(weight,value,cap); //回溯
        System.out.println(ans);
    }
}

b,推导-物品次序

      对问题:w = {2, 6, 1, 5, 4},v = {5, 9, 2, 10, 11},W = 10,我们从物品次序开始做推导:

      初始化:因为物品下标从 0 开始,所以跳过。但值得一提的是物品下标若从 1 开始,那么 dp[0][j] 均为0,因为此时并没有装入物品。不过所有 dp[i][0] 仍然均为 0,

      第一件物品 w[0]:
j = 0:无法放入,此时总价值为 0,dp[0][0]=0。(即所有 dp[i][0] 均为 0)
j = 1 : 无法放入,此时总价值为 0,dp[0][1]=0。
j = 2 : 可以放入,此时总价值为 5,dp[0][2]=5。
j = 3 … 10 : 可以放入,此时总价值为 5,dp[0][j]=5。

      在第一件物品的基础上放入第二件物品 w[1]:
j = 0:无法放入,此时总价值为 dp[0][0],所以dp[1][0]=0。(即所有 dp[i][0] 均为 0)
j = 1:无法放入,此时总价值为 dp[0][1](0),所以dp[1][0]=0。
j = 2 …5: 无法放入,此时总价值为 dp[0][j](5)。所以dp[1][j]=5。
j = 6 : 可以放入:放入的话需要腾出大小为 w[1] 的空间,要把 w[0] 拿出来,然后放入 w[1],此时总价值为 9(也就是 dp[0][0] + v[1]的值);不放入:dp[0][6]=5。取最大,所以dp[1][6]=9。
j = 7 : 可以放入:放入的话需要腾出大小为 w[1] 的空间,要把 w[0] 拿出来,然后放入 w[1],此时总价值为 9(也就是 dp[0][1] + v[1]的值);不放入:dp[0][7]=5。取最大,所以dp[1][7]=9。
j = 8 : 可以放入:放入的话需要腾出大小为 w[1] 的空间,此时空间足够,直接放入 w[1],此时总价值为 14(也就是 dp[0][2] + v[1]的值);不放入:dp[0][8]=5。取最大,所以dp[1][8]=14。
j = 9 . 10:空间足够,直接放入。dp[1][j]=14。

      在前两件物品的基础上放入第三件物品 w[2]:
j = 0:无法放入,此时总价值为 dp[1][0],所以dp[2][0]=0。(即所有 dp[i][0] 均为 0)
j = 1:可以放入:放入的话需要腾出大小为 w[2] 的空间,此时背包里没有东西,仅放入 w[2],总价值为 2(也就是 dp[1][0] + v[2]的值);不放入:dp[1][1]=0。取最大,所以dp[2][1]=0。
j = 2:可以放入:放入的话需要腾出大小为 w[2] 的空间,此时需要拿出 w[0],然后放入,总价值为 2(也就是 dp[1][1] + v[2]的值);不放入:dp[1][2]=5。取最大,所以dp[2][2]=5。
j = 3:可以放入:放入的话需要腾出大小为 w[2] 的空间,此时空间足够,直接放入,总价值为 7(也就是 dp[1][2] + v[2]的值);不放入:dp[1][3]=5。取最大,所以dp[2][3]=7。
j = 4 . 5:可以放入:放入的话需要腾出大小为 w[2] 的空间,此时空间足够,直接放入,总价值为 7(也就是 dp[1][j-1] + v[2]的值);不放入:dp[1][j]=5。取最大,所以dp[2][j]=7。
j = 6:可以放入:放入的话需要腾出大小为 w[2] 的空间,剩下的空间只能放入 w[0],然后放入 w[2],总价值为 7(也就是 dp[1][5] + v[2]的值);不放入:dp[1][6]=9。取最大,所以dp[2][6]=9。
j = 7:可以放入:放入的话需要腾出大小为 w[2] 的空间,剩下的空间可以放入 w[1],然后放入 w[2],总价值为 11(也就是 dp[1][6] + v[2]的值);不放入:dp[1][7]=9。取最大,所以dp[2][7]=11。
j = 8:可以放入:放入的话需要腾出大小为 w[2] 的空间,dp[1][7] + v[2]=11;不放入:dp[1][8]=14。取最大,所以dp[2][8]=14。
j = 9:可以放入:放入的话需要腾出大小为 w[2] 的空间,dp[1][8] + v[2]=16(能够看出当前容量能够同时放入 w[0],w[1],w[2]);不放入:dp[1][9]=14。取最大,所以dp[2][9]=16。
j = 10:可以放入:放入的话需要腾出大小为 w[2] 的空间,dp[1][9] + v[2]=16;不放入:dp[1][10]=14。取最大,所以dp[2][10]=16。

      在前三件物品的基础上放入第四件物品 w[3]:
j = 0:无法放入,此时总价值为 dp[2][0],所以dp[3][0]=0。(即所有 dp[i][0] 均为 0)
j = 1:无法放入,而 dp[2][1]=2。所以dp[3][1]=0。
j = 2:无法放入,而 dp[2][2]=5。所以dp[3][2]=5。
j = 3:无法放入,而 dp[2][3]=7。所以dp[3][3]=7。
j = 4:无法放入,而 dp[2][4]=7。所以dp[3][4]=7。
j = 5:可以放入,放入的话需要腾出大小为 w[3] 的空间,此时背包里没有东西,仅放入 w[3],总价值为 10(也就是 dp[2][0] + v[3]的值);不放入:dp[2][5]=7。取最大,所以dp[3][5]=10。
j = 6:可以放入,放入的话需要腾出大小为 w[3] 的空间,此时背包余量仅可放入 w[2],放入 w[3],总价值为 12(也就是 dp[2][1] + v[3]的值);不放入:dp[2][6]=9。取最大,所以dp[3][6]=12。
j = 7:可以放入:dp[2][2] + v[3]=15;不放入:dp[2][7]=11。取最大,所以dp[3][7]=17。
j = 8:可以放入:dp[2][3] + v[3]=17;不放入:dp[2][8]=14。取最大,所以dp[3][8]=17。
j = 9:可以放入:dp[2][4] + v[3]=17;不放入:dp[2][9]=16。取最大,所以dp[3][9]=17。
j = 10:可以放入:dp[2][5] + v[3]=17;不放入:dp[2][10]=16。取最大,所以dp[3][10]=17。

      在前四件物品的基础上放入第五件物品 w[4],读者可自行推导。

代码

      将上面思路转为为代码,参考代码如下:

	public void solution1(int[] w,int[] v,int W) {//打印出dp数组
        int num=w.length;
        int[][] dp=new int[num][W+1];

        for (int j=1;j<=W;j++)//对第一件物品 w[0] 单独做初始化
            if(w[0]<=j)
                dp[0][j]=v[0];
        for(int i=1;i<num;i++){
            for(int j=1;j<=W;j++){// j=0时,全为0.
                if(j>=w[i])
                    dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);
                else
                    dp[i][j]=dp[i-1][j];
            }
        }
        for(int i=0;i<num;i++){
            for(int j=0;j<=W;j++)
                System.out.printf("%2d ",dp[i][j]);
            System.out.println();
        }
    }
    public static void main(String[] args) {
        Tttest t=new Tttest();
        //注意此时物品是从下标0开始的。
        int[] weight = new int[] {2, 6, 1, 5, 4};
        int[] value = new int[] {5, 9, 2, 10, 11};
        int cap = 10;
        t.solution1(weight,value,cap); 
    }

      最终输出的 dp 数组和从背包大小开始做推导时一模一样,区别在于背包大小是一列一列进行推导,物品次序是一行一行推导,代码都是自底而上,pull 类型的算法实现。

c,物品次序从1开始

      对于上面的示例,如果物品次序从 1 开始,那么在代码中就不需要单独处理第一件物品 w[0] 了。以物品次序为例,算法如下:

代码
public class Tttest {
    public void solution1(int[] w,int[] v,int W) {//打印出dp数组
        int num=w.length;
        int[][] dp=new int[num][W+1];

        for(int i=1;i<num;i++){
            for(int j=1;j<=W;j++){
                if(j>=w[i])
                    dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);
                else
                    dp[i][j]=dp[i-1][j];
            }
        }
        for(int i=0;i<num;i++){
            for(int j=0;j<=W;j++)
                System.out.printf("%2d ",dp[i][j]);
            System.out.println();
        }
    }
    public static void main(String[] args) {
        Tttest t=new Tttest();
        //注意此时物品是从下标1开始的。
        int[] weight = new int[] {0,2, 6, 1, 5, 4};
        int[] value = new int[] {0,5, 9, 2, 10, 11};
        int cap = 10;
        t.solution1(weight,value,cap);
    }
}

      此时的 dp 数组如下所示:

012345678910
000000000000
100555555555
200555599141414
3025777911141616
402577101215171717
5025711131618182123

      第一行表示当没有物品放入任意大小的背包时的总价值,为0。如果物品次序从 0 开始,那么这里数组下标就得取 -1,而数组又不能越界,所以才会对 w[0] 进行额外的处理。
      第一列表示将前 i 个物品放入大小为 0 的背包时的总价值,为0。

总结

      暂时结束。行文仓促,或有纰漏,欢迎指正!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值