算法训练营(day43)

动态规划理论基础

动态规划中每一个状态一定是由上一个状态推导出来的,这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优的。

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

动态规划中 dp[j] 是由 dp[j-weight[i]] 推导出来的,然后取 max(dp[j], dp[j - weight[i]] + value[i])

动态规划的解题步骤

  1. 确定 dp 数组(dp table)以及下标的含义
  2. 确定递推公式
  3. dp 数组如何初始化
  4. 确定遍历顺序
  5. 举例推导 dp 数组

01背包理论基础

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

动规五部曲分析

  1. 确定dp数组(dp table)以及下标的含义

    对于背包问题,有一种写法是使用二维数组,即**dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少**。

  2. 确定递推公式

    dp[i][j]由两种方式获得:

    • 不放物品i:由dp[i - 1][j]推出,即背包容量为 j ,里面不放物品 i 的最大价值,此时dp[i][j]就是dp[i - 1][j]。(其实就是当物品i的重量大于背包j的重量时,物品i无法放进背包中,所以被背包内的价值依然和前面相同。)
    • 放物品i:由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] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);

  1. dp数组的初始化

    dp[i][j]的定义出发:

    • dp[i][0],即背包容量j为0,无论是选取哪些物品,背包价值总和一定为0
    • dp[0][j],即存放物品编号i为0的时候,各个容量的背包所能存放的最大价值
动态规划-背包问题7
  1. 确定遍历顺序

    遍历的维度有两个:物品i背包重量j

    先遍历物品 还是 先遍历背包重量 其实都可以!! 但是先遍历物品更好理解

  2. 推导dp数组

01背包测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
public class BagProblem {
public static void main(String[] args) {
int[] weight = {1,3,4};
int[] value = {15,20,30};
int bagSize = 4;
testWeightBagProblem(weight,value,bagSize);
}

/**
* 动态规划获得结果
* @param weight 物品的重量
* @param value 物品的价值
* @param bagSize 背包的容量
*/
public static void testWeightBagProblem(int[] weight, int[] value, int bagSize){

// 创建dp数组
int goods = weight.length; // 获取物品的数量
int[][] dp = new int[goods][bagSize + 1];

// 初始化dp数组
// 创建数组后,其中默认的值就是0
for (int j = weight[0]; j <= bagSize; j++) {
dp[0][j] = value[0];
}

// 填充dp数组
for (int i = 1; i < weight.length; i++) {
for (int j = 1; j <= bagSize; j++) {
if (j < weight[i]) {
/**
* 当前背包的容量都没有当前物品i大的时候,是不放物品i的
* 那么前i-1个物品能放下的最大价值就是当前情况的最大价值
*/
dp[i][j] = dp[i-1][j];
} else {
/**
* 当前背包的容量可以放下物品i
* 那么此时分两种情况:
* 1、不放物品i
* 2、放物品i
* 比较这两种情况下,哪种背包中物品的最大价值最大
*/
dp[i][j] = Math.max(dp[i-1][j] , dp[i-1][j-weight[i]] + value[i]);
}
}
}

// 打印dp数组
for (int i = 0; i < goods; i++) {
for (int j = 0; j <= bagSize; j++) {
System.out.print(dp[i][j] + "\t");
}
System.out.println("\n");
}
}
}

滚动数组

滚动数组,就是把二维dp降为一维dp,我们重温一下:dp[i][j] 里的i和j表达的是i是物品,j是背包容量

  1. 在使用二维数组的时候,递推公式:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
  2. 如果把dp[i - 1]那一层拷贝到dp[i]上,表达式完全可以是:**dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i]);**
  3. 再简化一下表达式,只用一个一维数组 dp[j]

dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j],那么dp[0]就应该是0,因为背包容量为0所背的物品的最大价值就是0。

所以递归公式为:

1
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

滚动数组遍历背包顺序

二维dp遍历的时候,背包容量是从小到大,而一维dp遍历的时候,背包是从大到小

倒序遍历是为了保证物品i只被放入一次!!!

一维dp遍历一定要 先物品后背包容量 吗?

答案:是的!!!

一维dp的写法中,背包容量一定是要倒序遍历的,如果遍历背包容量放在上一层,那么每个 dp[j] 就只会放入一个物品,即:背包里只放入了一个物品。

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

(个人理解:一维dp就是先放大的,大的塞进去还有位置就再补小的,大的塞不进就换小一号的塞,保证自己不会犯糊涂,塞了两个一样小的充当一个大的用)

1049. 最后一块石头的重量 II

题目链接:https://leetcode.cn/problems/last-stone-weight-ii/description/

有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。

每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 xy,且 x <= y。那么粉碎的可能结果如下:

  • 如果 x == y,那么两块石头都会被完全粉碎;
  • 如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x

最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0

解题思路

解题过程:动态规划

按照动规五部曲来分析:

  1. 确定dp数组(dp table)以及下标的含义

​ **dp[j]表示容量为j的背包,最多可以背最大重量为dp[j]**。

  1. 确定递推公式

​ 递归公式为: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);

​ 化简为一维数组的 01背包的递推公式为:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

​ 本题的递推公式为:**dp[j] = max(dp[j], dp[j - stones[i]] + stones[i])**

  1. dp数组的初始化

    dp[i][j]的定义出发:

    • dp[i][0],即背包容量j为0,无论是选取哪些物品,背包价值总和一定为0
    • dp[0][j],即存放物品编号i为0的时候,各个容量的背包所能存放的最大价值

​ 本题dp的上限是 石头总重量的一半,因为重量不可能为负数,所以 dp的初始化定为0即可

  1. 确定遍历顺序

​ 使用一维dp数组,物品遍历的for循环放在外层,遍历背包的for循环放在内层,且内层for循环倒序遍历!

1
2
3
4
5
for(int i = 0; i < len; i++){
for(int j = target; j >= nums[i]; j--){
dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i]);
}
}
  1. 推导dp数组

详细代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public int lastStoneWeightII(int[] stones) {
int sum = 0;
for(int s : stones){
sum += s;
}
int target = sum >> 1;
int[] dp = new int[target + 1];

for(int i = 0; i < stones.length; i++){
for(int j = target; j >= stones[i]; j--){
dp[j] = Math.max(dp[j], dp[j - stones[i]] + stones[i]);
}
}
return sum - 2 * dp[target];
}
}

494. 目标和

题目链接:https://leetcode.cn/problems/target-sum/description/

给你一个整数数组 nums 和一个整数 target

向数组中的每个整数前添加 '+''-' ,然后串联起所有整数,可以构造一个 表达式

  • 例如,nums = [2, 1] ,可以在 2 之前添加 '+' ,在 1 之前添加 '-' ,然后串联起来得到表达式 "+2-1"

返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。

解题思路

解题过程:动态规划

按照动规五部曲来分析:

  1. 确定dp数组(dp table)以及下标的含义

​ 对于背包问题,**dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少**。

​ 本题的dp[j] 表示:填满 j(包括j)这么大容积的包,有 dp[j] 种方法

  1. 确定递推公式

​ 对于求组合类问题,动态规划的公式为:dp[j] += dp[j - nums[i]]

  1. dp数组的初始化

    从递推公式可以看出,对于求组合类问题在初始化的时候dp[0] 一定要初始化为1,因为dp[0]是在公式中一切递推结果的起源,如果dp[0]是0的话,递推结果将都是0。

  2. 确定遍历顺序

    使用一维dp数组,物品遍历的for循环放在外层,遍历背包的for循环放在内层,且内层for循环倒序遍历!

1
2
3
4
5
for(int i = 0; i < len; i++){
for(int j = target; j >= nums[i]; j--){
dp[j] += dp[j - nums[i]]
}
}
  1. 推导dp数组

详细代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Solution {
public int findTargetSumWays(int[] nums, int target) {
int sum = 0;
for(int num : nums){
sum += num;
}
if(target < 0 && sum < -target){
return 0;
}
if((target + sum) % 2 != 0){
return 0;
}
int size = (target + sum) / 2;
if(size < 0){
size = -size;
}
int[] dp = new int[size + 1];
dp[0] = 1;

for(int i = 0; i < nums.length; i++){
for(int j = size; j >= nums[i]; j--){
dp[j] += dp[j - nums[i]];
}
}
return dp[size];
}
}

474. 一和零

题目链接:https://leetcode.cn/problems/ones-and-zeroes/description/

给你一个二进制字符串数组 strs 和两个整数 mn

请你找出并返回 strs 的最大子集的长度,该子集中 最多m0n1

如果 x 的所有元素也是 y 的元素,集合 x 是集合 y子集

解题思路

解题过程:动态规划

按照动规五部曲来分析:

  1. 确定dp数组(dp table)以及下标的含义

    dp[i][j]:最多有 i 个0和 j 个1的strs的最大子集的大小为 dp[i][j]

  2. 确定递推公式

    动态规划的递归公式为: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);

    dp[i][j] 可以由前一个strs里的字符串推导出来,strs里的字符串有 zeroNum 个0,oneNum 个1。

    dp[i][j] 就可以是 dp[i - zeroNum][j - oneNum] + 1。然后我们在遍历的过程中,取 dp[i][j] 的最大值。

    得到本题的递推公式:dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);

  3. dp数组的初始化

    dp[i][j]的定义出发:

    • dp[i][0],即背包容量j为0,无论是选取哪些物品,背包价值总和一定为0
    • dp[0][j],即存放物品编号i为0的时候,各个容量的背包所能存放的最大价值

    因为本题物品价值不会是负数,初始值只需设置为0,保证递推的时候 dp[i][j]不会被初始值覆盖即可

  4. 确定遍历顺序

    本题物品就是strs里的字符串,背包容量就是题目描述中的m和n。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
for(String str : strs){
oneNum = 0;
zeroNum = 0;
for(char ch : str.toCharArray()){
if(ch == '0'){
zeroNum++;
}else {
oneNum++;
}
}
for(int i = m; i >= zeroNum; i--){
for(int j = n; j >= oneNum; j--){
dp[i][j] = Math.max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);
}
}
}
  1. 推导dp数组

详细代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
public int findMaxForm(String[] strs, int m, int n) {
int[][] dp = new int[m + 1][n + 1];
int oneNum, zeroNum;

for(String str : strs){
oneNum = 0;
zeroNum = 0;
for(char ch : str.toCharArray()){
if(ch == '0'){
zeroNum++;
}else {
oneNum++;
}
}
for(int i = m; i >= zeroNum; i--){
for(int j = n; j >= oneNum; j--){
dp[i][j] = Math.max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);
}
}
}
return dp[m][n];
}
}