0-1背包问题的学习及LeetCode相关习题练习

0-1背包问题:

n件物品,它们装入背包所占的容量分别为w1、w2……wn;它们所拥有的价值分别为v1、v2 ……vn;
有一个总容量为C的背包;

在装满背包的情况下,如何使得包内的总价值最大?

该问题的特点是:每个物品仅有一个,可以选择放或者不放,也就是说每个物品只能使用一次。

思路:

1.首先定义一个状态转移数组dp,dp[i][j]表示前i件物品放入容量为j的背包中所能得到的最大价值;

2.寻找数组元素之间的关系式,也就是状态转移方程,我们将第i件物品是否放入背包中这个子问题拿出来进行分析,首先要明确的是我们的一切目标都是使得在既有的背包容量下,能够得到最大的价值,

所以对于第i件物品,如果放入能够使得在现有的容量下背包价值最大,则dp[i][j] = dp[i-1][j-w] + v;如果不能,则就不把第i件物品放入背包,那么在dp[i][j] = dp[i-1][j],即前i件物品放入容量为j的背包中所得到的

最大价值就是前i-1件物品放入容量为j的背包中所得的的最大价值。

  总结一下,状态转移方程就是: dp[i][j] = max{dp[i][j] = dp[i-1][j-w] + v,dp[i][j] = dp[i-1][j]}

3.确定初始值,dp[0][0]表示前0件物品放入容量为0的背包中的最大价值,那么就是0,而对于多有i=0和j=0的元素,其值都是0;

模拟过程:

举一个例子来模拟程序整个的执行过程;

i 1 2 3
w 1 2 3
v 6 9 13

现在有三件物品,这些物品的价值和所占容量如上表所示,有一个容量为5的背包,在装满背包的情况下,如何使得背包里的价值最大?

通过一个表格来显示状态转移数组的内部情况:

i\j 0 1 2 3 4 5
0 0 0 0 0 0 0
1 0 6 6 6 6 6
2 0 6 9 15 15 15
3 0 6 9 15 19 22

 

该表格表示dp数组内部的元素值,程序的执行过程如下:

  i = 1     ==> w = 1,v = 6 :

1 dp[1][1] = max(dp[0][1],dp[0][0]+6)
2 dp[1][2] = max(dp[0][2],dp[0][1]+6)
3 dp[1][3] = max(dp[0][3],dp[0][2]+6)
4 dp[1][4] = max(dp[0][4],dp[0][3]+6)
5 dp[1][5] = max(dp[0][5],dp[0][4]+6)

 

 

 

 

 

 

 

  i = 2  ==> w = 2,v=9:

   …………

  以此类推,可以得到状态转移表格中的数据。

 

优化使用空间:

通过状态转移方程 dp[i][j] = max{dp[i-1][j-w] + v,dp[i-1][j]} 我们可以发现,背包从前i件物品所能得到的最大价值只和前i-1件件物品所能得到的最大价值有关,所以可以将状态转移数组简化成一维数组,只存储在既有的容量下所能得到的

最大价值,但是内循环的容量变化顺序应该翻转一下,即容量应该从最大的总量依次向下变小,否则在正序计算的时候会发生错误计算,也就是会隐形的放大在当前容量下,能够放入背包的物品的选择范围。比如对于优化后的方程dp[j] = max{dp[j-w] + v,dp[j]}, 

dp[j]实际是dp[i][j],而dp[j-w]和dp[j]实际是dp[i-1][j-w]和dp[i-1][j];如果正序计算的话那么,dp[j-w]和dp[j]实际是dp[i][j-w]和dp[i][j],所得出的语义结论就成了前i件物品在既有背包容量j的情况下,如果将第i件物品放入背包,最大价值是当前物品价值加上前i件物品在

容量j-w下的最大价值,这是不正确的;对于不放入背包的情况也是一样的,第i件物品你都不让入背包了,实际的最大价值怎么可能还是前i件物品在既有容量j下所得的的最大价值。所以内循环应该倒序计算。


实现的代码如下:

public int knapsacks(int W,int N,int[] weights,int[] values){
            int[][] dp = new int[N+1][W+1];
        /*
            i: 代表当前的物品总数量
            j: 代表当前的背包总体积
         */
        dp[0][0] = 0;
        dp[0][1] = 0;
        dp[1][0] = 0;
        for (int i = 1; i <= N; i++) {
            /*
                w: 代表第i个物品的体积
                v: 代表第i个物品的价值
             */
            int w = weights[i-1];
            int v = values[i-1];
            for (int j = 1; j <= W; j++) {
                if (j>=w){
                    dp[i][j] = Math.max(dp[i-1][j],dp[i-1][j-w]+v);
                }else {
                    // 如果当前这个第i个物品的体积比当前背包的总体积要大,那说明不能放入背包,
                    // 直接就是考虑前i-1个物品放入背包,所能得到的最大价值
                    dp[i][j] = dp[i-1][j];
                }
            }
        }
        return dp[N][W];
    }

优化空间后的代码:

    public int knapsacks2(int W,int N,int[] weights,int[] values){
        int[] dp = new int[W + 1];
        for (int i = 1; i <= N; i++) {
            int w = weights[i - 1], v = values[i - 1];
            for (int j = W; j >= 1; j--) {
                if (j >= w) {
                    dp[j] = Math.max(dp[j], dp[j - w] + v);
                }
            }
        }
        return dp[W];
    

 

LeetCode练习:

第416题:

给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

注意:

每个数组中的元素不会超过 100
数组的大小不会超过 200

思路:

对于此题,如果能够分割成两个子集,同时这两个子集的元素和相等,那么前提是这个数组的所有元素的和是一个偶数,否则一定不能分割成两个元素和相等的子集;

如果所有的元素和是一个偶数,那么符合要求,我们接下来的目标就是寻找一个子集,这个子集的元素和应该是数组所有元素和的一半,就只要能够找到一个子集合它的所有元素和是 sum / 2,那么返回true,暂且把sum/2命名为target;

经过分析可以直到,这是一个典型的0-1背包问题,target相等于背包的容量,只不过我们在这里不是如何使得背包装满的情况下,使得其价值最大,而只要能够装满即可,所以采用动态规划的解决方案如下:

  1.定义一个状态转移数组dp,dp[i][j]表示前i个元素中能否找到和为j的元素的子集,dp数组的类型是布尔类型

  2.寻找状态转移方程:对于第i个元素,如果其放入“背包”中,那么dp[i][j]值应该是dp[i][j] = dp[i-1][j-nums[i]];如果不放入“背包”中,那么dp[i][j] = dp[i-1][j];

  3.确定初始值,dp[0][0] = true,因为对于前0个元素,其和就是0,所以是能够找到和为0的子集合的。

代码如下:

public boolean canPartition(int[] nums) {
        if (nums.length == 1 && nums[0] != 0)
            return false;
        int sum = 0;
        for (int num : nums) {
            sum += num;
        }
        if (sum % 2 !=0)
            return false;
        int target = sum / 2;
        boolean[][] dp = new boolean[nums.length+1][target+1];
        dp[0][0] = true;
     // 元素从第1个到第n个
     // 容量从0到target
for (int i = 1; i <= nums.length; i++) { for (int j = 0; j <= target; j++) { if (j >= nums[i-1]) dp[i][j] = dp[i-1][j] || dp[i-1][j - nums[i-1]]; else dp[i][j] = dp[i-1][j]; } } return dp[nums.length][target]; }

优化空间后的代码:

public boolean canPartition2(int[] nums) {
        if (nums.length == 1 && nums[0] != 0)
            return false;
        int sum = 0;
        for (int num : nums) {
            sum += num;
        }
        if (sum % 2 !=0)
            return false;
        int target = sum / 2;
        boolean[] dp = new boolean[target+1];
        dp[0] = true;
        for (int i = 0; i < nums.length; i++) {
            for (int j = target; j >= nums[i]; j--) {
                if (j >= nums[i])
                    dp[j] = dp[j] || dp[j - nums[i]];
            }
        }
        return dp[target];
    }

 继续优化,通过分析状态转移方程我们可以直到,我们要的最后的结果是dp[target],而最终的dp[target]是从dp[target - nums[nums.length-1]]得到,以此类推,对于dp[i]我们知道,推导出它的公式是 dp[i] = dp[target - sum(nums[i..nums.length-1])],所以我们可以对内循环的边界进行进一步的优化,从而减少循环的次数,优化的边界值如下:

  bound = Math.max(nums[i],target - sumarray(nums,i))

修改后的程序如下所示:

    public boolean canPartition3(int[] nums) {
        if (nums.length == 1)
            return false;
        int sum = sumarray(nums,0);
        if (sum % 2 !=0)
            return false;
        int target = sum / 2;
        boolean[] dp = new boolean[target+1];
        dp[0] = true;
        for (int i = 0; i < nums.length; i++) {
            int bound = Math.max(nums[i],target - sum);
            for (int j = target; j >= bound; j--) {
                dp[j] = dp[j] || dp[j - nums[i]];
            }
            sum = sum - nums[i];
        }
        return dp[target];
    }
    private int sumarray(int[] nums,int index){
        int res = 0;
        for (int i = index; i < nums.length; i++) {
            res += nums[i];
        }
        return res;
    }
posted @ 2020-04-12 11:08  有心有梦  阅读(5243)  评论(2编辑  收藏  举报