• 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的背包,在装满背包的情况下,如何使得背包里的价值最大?

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

    ij 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;
        }
  • 相关阅读:
    Tomcat之the jre_home environment variable is not defined correctly this environment variable is need
    java集合框架之聚合操作stream
    java集合框架之比较器Comparator、Comparable
    java集合框架之HashCode
    java集合框架之几种set(HashSet LinkedHashSet TreeSet )
    java集合框架之HashMap和Hashtable的区别
    java集合框架之Collections
    java集合框架之Collection
    java集合框架之HashSet
    java集合框架之HashMap
  • 原文地址:https://www.cnblogs.com/yxym2016/p/12684203.html
Copyright © 2020-2023  润新知