Day44~01背包、416. 分割等和子集

107 阅读7分钟

摘要

本文主要介绍了01背包理论基础(一)、01背包理论基础(二)(滚动数组)和LeetCode416题分割等和子集。

1、01背包理论基础(一)

1.1 题目描述

有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。

1.2 思路

  • dp 数组以及下标的含义: dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少

  • 递推公式:

    • 第一种情况:放不下物品,0-i的物品, j容量可以放下的最大价值等于0-(i-1)的物品,j-1容量的价值

      • dp[i][j] = dp[i-1][j]
    • 第二种情况:可以放下物品,由dp[i - 1][j - weight[i]]推出,dp[i - 1][j - weight[i]] 为背包容量为j - weight[i]的时候不放物品i的最大价值,那么dp[i - 1][j - weight[i]] + value[i](物品i的价值),就是背包放物品i得到的最大价值

      • dp[i][j] = Math.max(dp[i-1][j], value[i] + dp[i-1][j-weight[i]])
    • 如果 j < items[i].weight 则放不下物品,反之可以放下物品

  • dp 数组如何初始化:

    • 首先从dp[i][j]的定义出发,如果背包容量j为0的话,即dp[i][0],无论是选取哪些物品,背包价值总和一定为0。

    • dp[0][j],即:i为0,存放编号0的物品的时候,各个容量的背包所能存放的最大价值 。

      • 当 j < weight[0]的时候,dp[0][j] 应该是 0,因为背包容量比编号0的物品重量还小
      • j >= weight[0]时,dp[0][j] 应该是 items[0].value,因为背包容量放足够放编号0物品
  • dp 数组遍历顺序: 先遍历物品,然后遍历背包重量

  • 打印 dp 数组

1、为什么 创建 dp 数组是 int[][] dp = new int[n][m + 1]

dp[i][j] 代表从 [0,i] 的物品任取,放入重量为 j 的背包的最大价值为 dp[i][j],因为 n=items.length 所以 i=0 代表第1个物品,所以 i 的取值范围是 [0, n-1],j = 0 代表背包重量为0,j = m代表背包的最大容量,所以j 的取值范围是[0, m],综上所述 int[][] dp = new int[n][m + 1]

2、为什么 i = 0 时,当 j >= items[0].weightdp[0][j] == items[0].value

因为 i 等于0 代表从一个物品中取,放入背包容量为 j 的背包,只有当前物品可取,所以容量 j 大于当前物品的重量时,最大价值为 items[0].value

3、为什么动态规划中需要判断 j < items[i].weight

首先如果j < items[i].weight 表示放不下当前物品,所以 dp[i][j] = dp[i-1][j],只有当 j >= items[i].weight 时,才可以放下当前物品,所以可以选择放下当前物品或不放下当前物品 dp[i][j] = Math.max(dp[i-1][j], value[i] + dp[i-1][j--weight[i]])

1.3 代码

public class Rucksack {
​
    public static void main(String[] args) {
        Item item1 = new Item(1, 15);
        Item item2 = new Item(3, 20);
        Item item3 = new Item(4, 30);
        Item[] items = {item1, item2, item3};
        int m = 4;
        rucksack(items, m);
    }
​
    // dp[i][j] 代表从 [0,i] 的物品任取,放入重量为 j 的背包的最大价值为 dp[i][j]
    // 不取物品  dp[i][j] = dp[i-1][j]
    // 取物品   dp[i][j] = Math.max(dp[i-1][j], items[i].value + dp[i-1][j-items[i].weight])
    // 如果j < j-items[i].weight 则不取物品,反之可以取物品
    // 初始化   dp[i][0] = 0; 如果 j >= items[0].weight; dp[0][j] = items[0].value
    public static void rucksack(Item[] items, int m) {
        int n = items.length;
        int[][] dp = new int[n][m + 1];
​
        for (int j = m; j >= 0; j--) {
            if (j < items[0].weight) {
                break;
            }
            dp[0][j] = items[0].value;
        }
​
        // 先遍历物品,然后遍历背包
        for (int i = 1; i < n; i++) {
            for (int j = 1; j <= m; j++) {
                if (j < items[i].weight) {
                    dp[i][j] = dp[i - 1][j];
                } else {
                    dp[i][j] = Math.max(dp[i - 1][j], items[i].value + dp[i - 1][j - items[i].weight]);
                }
            }
        }
​
        // 打印
        print(dp, n, m);
    }
​
    public static void print(int[][] dp, int n, int m) {
        for (int i = 0; i < n; i++) {
            for (int j = 0; j <= m; j++) {
                System.out.print(dp[i][j] + "\t");
            }
            System.out.println("\n");
        }
    }
​
    public static class Item {
​
        public int weight;
​
        public int value;
​
        public Item() {
        }
​
        public Item(int weight, int value) {
            this.weight = weight;
            this.value = value;
        }
    }
}

2、01背包理论基础(二)(滚动数组)

2.1 思路

  • dp 数组以及下标的含义: 在一维dp数组中,dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j]

  • 递推公式

    • 第一种情况:放不下物品,0-i的物品, j容量可以放下的最大价值等于0-(i-1)的物品,j-1容量的价值

      • dp[j] = dp[j]
    • 第二种情况:可以放下物品,由dp[j - weight[i]]推出,dp[j - weight[i]] 为背包容量为j - weight[i]的时候不放物品i的最大价值,那么dp[j - weight[i]] + value[i](物品i的价值),就是背包放物品i得到的最大价值

      • dp[j] = Math.max(dp[j], value[i] + dp[j-weight[i]])
    • 如果 j < items[i].weight 则放不下物品,反之可以放下物品

  • dp 数组如何初始化:

    • 首先从[j]的定义出发,如果背包容量j为0的话,即dp[0],无论是选取哪些物品,背包价值总和一定为0。

    • dp[j],即:i为0,存放编号0的物品的时候,各个容量的背包所能存放的最大价值 。

      • 当 j < weight[0]的时候,dp[j] 应该是 0,因为背包容量比编号0的物品重量还小
      • j >= weight[0]时,dp[j] 应该是 items[0].value,因为背包容量放足够放编号0物品
  • dp 数组遍历顺序:先遍历物品,然后遍历背包,但背包是倒序遍历
  • 打印 dp 数组

1、为什么会出现一维dp数组?

使用二维数组的时候,递推公式:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]),使用一维数组时,递推公式:dp[j] = Math.max(dp[j], value[i] + dp[j-weight[i]])。只用dp[j](一维数组,也可以理解是一个滚动数组),这就是滚动数组的由来,需要满足的条件是上一层可以重复利用,直接拷贝到当前层

2、为什么一维dp数据,遍历背包是倒序遍历?

倒序遍历的原因是,本质上还是一个对二维数组的遍历,并且右下角的值依赖上一层左上角的值,因此需要保证左边的值仍然是上一层的,从右向左覆盖

2.2 代码

public class Rucksack2 {
​
    public static void main(String[] args) {
        Item item1 = new Item(1, 15);
        Item item2 = new Item(3, 20);
        Item item3 = new Item(4, 30);
        Item[] items = {item1, item2, item3};
        int m = 4;
        rucksack(items, m);
    }
​
    // 滚动数组就是把上一层数组拷贝下来了 
    // dp[j] 放入重量为 j 的背包的最大价值为 dp[j]
    // 不放物品 dp[j] = dp[j]
    // 放物品  dp[j] = Math.max(dp[j], items[i].value + dp[j-items[i].weight])
    // 如果j < j-items[i].weight] 则不取物品,反之可以取物品
    // 初始化,如果 j >= items[0].weight; dp[j] = items[0].value
    public static void rucksack(Item[] items, int m) {
        int n = items.length;
        int[] dp = new int[m + 1];
​
        for (int j = m; j >= 0; j--) {
            if (j < items[0].weight) {
                break;
            }
            dp[j] = items[0].value;
        }
​
        // 先遍历物品,然后遍历背包,但背包是倒序遍历
        for (int i = 1; i < n; i++) {
            for (int j = m; j >= 1; j--) {
                if (j < items[i].weight) {
                    dp[j] = dp[j];
                } else {
                    dp[j] = Math.max(dp[j], items[i].value + dp[j - items[i].weight]);
                }
            }
        }
​
        // 打印
        print(dp, n, m);
    }
​
    public static void print(int[] dp, int n, int m) {
        for (int j = 0; j <= m; j++) {
            System.out.print(dp[j] + "\t");
        }
    }
​
    public static class Item {
​
        public int weight;
​
        public int value;
​
        public Item() {
        }
​
        public Item(int weight, int value) {
            this.weight = weight;
            this.value = value;
        }
    }
}

3、416.分割等和子集

3.1 思路

1、套用01背包需要确定哪些步骤?

  • 确定背包的体积
  • 确定物品的重量和价值
  • 是否不可重入放入

2、如果套用01背包问题到本题中?

dp[j] 表示装满容量为j的背包的最大价值为dp[j],背包的容量是target = sum / 2,物品的重量和价值都为 num[i],因为放入物品到背包中是不会超过背包的体积的,所以如果装满背包,即dp[target] = target,则满足条件,可以分割等和子集

3.2 代码

    public boolean canPartition(int[] nums) {
        int sum = getSum(nums);
        int target = sum / 2;
        if(sum % 2 != 0) {
            return false;
        }
​
        int[] dp = new int[target+1];
        for(int j=target; j>=1; j--) {
            if(j < nums[0]) {
                break;
            }
            dp[j] = nums[0];
        }
        
        for(int i=1; i<nums.length; i++) {
            for(int j=target; j>=1; j--) {
                if(j < nums[i]) {
                    dp[j] = dp[j];
                } else {
                    dp[j] = Math.max(dp[j], dp[j-nums[i]] + nums[i]);
                }
            }
        }
        return dp[target] == target;
    }
​
    public int getSum(int[] nums) {
        int sum = 0;
        for(int num : nums) {
            sum += num;
        }
        return sum;
    }

参考资料

代码随想录-01背包理论基础(一)

代码随想录-01背包理论基础(二)

代码随想录-416. 分割等和子集