LeeCode 动态规划(二)

2023-07-10,,

01背包问题

题目描述

n 件物品和容量为 w 的背包,给你两个数组 weightsvalues ,分别表示第 i 件物品的重量和价值,每件物品只能使用一次,求解将哪些物品装入背包可使得物品价值总和最大?

建立模型

    确定dp数组及下标的含义,数组的含义为从第 [0, i - 1] 个物品中选择若干个物品,其重量和小于等于 j 的最大价值。
    初始化dp数组,dp[i][0] = 0 (0 < i < length),即背包容量为0的最大价值为0;dp[0][j] = 0,即没有物品选择时最大价值为0。
    确定递推公式:

    dp[i][j] = Math.max(dp[i - 1][j - weighti[i]] + values[i], dp[i - 1][j]) (weights[i] <= j),左边表示选择当前物品,右边表示不选择当前物品;

    dp[i][j] = dp[i - 1][j] (weights[i] > j),当前物品重量大于背包容量,无法选择。
    确定遍历顺序,外循环遍历物品i -> 0 ~ length-1,内循环遍历背包容量j -> 1 ~ bagSize,此情况下内外循环的顺序交换不影响结果。

代码实现

public int zeroOneBagProblem(int[] weights, int[] values, int bagSize) {
int n = weights.length;
int[][] dp = new int[n + 1][bagSize + 1]; /**
* 这两步初始化其实可以省略(定义数组时默认值为0)
* 但初次学习背包问题就做了显式初始化。
*/
for (int i = 0; i <= n; i++) {
dp[i][0] = 0;
} for (int j = 0; j <= bagSize; j++) {
dp[0][j] = 0;
} for (int i = 1; i <= n; i++) {
for (int j = 1; j <= bagSize; j++) {
if (j >= weights[i - 1]) {
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weights[i - 1]] + values[i - 1]);
}
else {
dp[i][j] = dp[i - 1][j];
}
}
} return dp[n][bagSize];
}

代码优化

状态压缩:对于递推公式 dp[i][j] = Math.max(dp[i - 1][j - weighti[i]] + values[i], dp[i - 1][j]) ,第 i 层只和第 i - 1 层的状态有关,可以只用一个一维数组表示。

public int zeroOneBagProblem(int[] weights, int[] values, int bagSize) {
int n = weights.length; /**
* 数组的含义为背包容量j的最大价值总和
*/
int[] dp = new int[bagSize + 1]; for (int i = 0; i < length; i++) {
/**
* 必须倒序遍历背包容量以保证 dp[j - weights[i]] 是上一层的状态,而不是当前层
* 否则会出现同一物品多次放入的状态
*/
for (int j = bagSize, j >= weight[i]; j--) {
dp[j] = Math.max(dp[j], dp[j - weights[i]] + values[i]);
}
} return dp[bagSize];
}

LeeCode 416:分割等和子集

题目描述

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

建立模型

本题表面是一个回溯的问题,对于每一个元素可以选择添加或者不添加到集合中,回溯过程中判断当前集合的元素和是否等于 sum / 2

如何把该问题转化为 01背包问题 呢?

分割数组得到两个等和的子集 \(\Leftrightarrow\) 能否从数组中选择一些元素使其和等于整个数组的一半,且每个元素只能选择一次。

物品 \(\rightarrow\) 数组中的元素,价值和重量都是 nums[i]

背包容量 \(\rightarrow\) sum / 2

确定dp数组及下标含义,数组的含义为从 nums[0] ~ nums[i] 中选取若干整数,是否存在和等于 j 的情况。
初始化dp数组,dp[i][0] = true \(\Rightarrow\) 不选任何元素可使和为0,dp[0][nums[0]] = true \(\Rightarrow\) 只有一个元素可选时,只选该元素对应的和为 true。
确定递推公式:
\[\begin{cases} dp[i][j] = dp[i - 1][j]\ ||\ dp[i - 1][j - nums[i]] \quad (j >= nums[i])\\ dp[i][j] = dp[i - 1][j] \quad (j < nums[i]) \end{cases}
\]
确定遍历顺序,外循环遍历物品i -> 1 ~ n,内循环遍历背包容量j -> 1 ~ sum / 2

代码实现

public boolean canPartition(int[] nums) {
// 考虑特殊情况
if (nums.length == 1) {
return false;
} int sum = 0;
int max_value = 0;
for (int num : nums) {
sum += num;
max_value = Math.max(num, max_value);
} if (sum % 2 != 0 || max_value > sum / 2) {
return false;
} int target = sum / 2;
int n = nums.length; // 创建 dp 数组
boolean[][] dp = new boolean[n][target + 1];
for (int i = 0; i < n; i++) {
dp[i][0] = true;
}
dp[0][nums[0]] = true; for (int i = 1; i < n; i++) {
for (int j = 1; j <= target; j++) {
if (j >= num[i]) {
dp[i][j] = dp[i - 1][j] || dp[i - 1][j - num[i]];
}
else {
dp[i][j] = dp[i - 1][j];
}
}
} return dp[n - 1][target];
} /**
* 一维dp数组实现
*/
public boolean canPartition(int[] nums) {
// 考虑特殊情况
if (nums.length == 1) {
return false;
} int sum = 0;
int max_value = 0;
for (int num : nums) {
sum += num;
max_value = Math.max(num, max_value);
} if (sum % 2 != 0 || max_value > sum / 2) {
return false;
} int target = sum / 2;
int n = nums.length; // 创建 dp 数组
boolean[] dp = new boolean[target + 1];
for (int i = 0; i < n; i++) {
dp[0] = true;
} for (int i = 0; i < n; i++) {
for (int j = target; j >= nums[i]; j--) {
dp[j] = dp[j] || dp[j - nums[i]];
}
} return dp[target];
}

LeeCode 1049:最后一块石头的重量II

题目描述

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

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

如果 x == y,那么两块石头都会被完全粉碎;

如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。

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

建立模型

问题转化 \(\Rightarrow\) 01背包问题

该问题描述等价于选取若干石头使其重量小于等于 sum/2 的最大值sumstones 数组石头的重量总和。选取石头的重量越接近 sum / 2 ,最近剩余的石头重量就越小。

物品 \(\rightarrow\) 石头,价值和重量都是 stones[i],且每个石头只能选择一次

背包容量 \(\rightarrow\) sum/2

代码实现

public int lastStoneWeightII(int[] stones) {
int sum = 0;
for (int stone : stones) {
sum += stone;
} int target = sum / 2;
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];
}

LeeCode 494:目标和

题目描述

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

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

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

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

建立模型

问题转化 \(\Rightarrow\) 01背包问题

该问题描述等价于在数组中选择若干个元素使其和等于 (sum - target) / 2 的不同方法数。我们只需要给这些元素添加负号,数组中其它所有元素添加正号,则可使这个表达式的运算结果为 target

物品 \(\rightarrow\) 整数数组,重量和价值都是nums[i],且每个数字只能选择一次

背包容量 \(\rightarrow\) (sum - target) / 2

代码实现

public int findTargetSumWays(int[] nums, int target) {
int sum = 0;
for (int num : nums) {
sum += num;
} if (sum < Math.abs(target) || (sum - target) % 2 != 0) {
return 0;
} int cur = (sum - target) / 2; // 数组的含义为从数组中选择元素使其和为 j 的方法数。
int[] dp = new int[cur + 1]; // dp[0][0] = 1, 表示没有任何物品可以选择且和为0的方法数
dp[0] = 1 for (int i = 0; i < nums.length; i++) {
for (int j = cur; j >= nums[i]; j--) {
dp[j] += dp[j - nums[i]];
}
} return dp[cur];
}

LeeCode 474:一和零

题目描述

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

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

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

建立模型

问题转化 \(\Rightarrow\) 01背包问题

本题在经典背包问题的基础上,背包容量被划分为两个维度(0的容量和1的容量),做法是和01背包问题完全一致的。

确定dp数组及下标的含义,数组的含义为最多有 j0k1 的最大子集长度
初始化dp数组 dp[0][0] = 0
确定递推公式:
\[dp[j][k] = Math.max(dp[j][k], dp[j - countZero][k - countOne])
\]
确定遍历顺序,外循环遍历物品i->0 ~ length-1,内循环遍历背包容量。

代码实现

public int findMaxForm(String[] strs, int m, int n) {
int[][] dp = new int[m + 1][n + 1]; for (int i = 0; i < strs.length; i++) {
String s = strs[i];
int countZero = getOneZero(s)[0], countOne = getOneZero(s)[1]; for (int j = m; j >= countZero; j--) {
for (int k = n; k >= countOne; k--) {
dp[j][k] = Math.max(dp[j][k], dp[j - countZero][k - countOne] + 1);
}
}
} return dp[m][n];
} public int[] getOneZero(String s) {
int[] res = new int[2];
for (int i = 0; i < s.length(); i++) {
res[s.charAt(i) - '0'] += 1;
} return res;
}

LeeCode 动态规划(二)的相关教程结束。

《LeeCode 动态规划(二).doc》

下载本文的Word格式文档,以方便收藏与打印。