Leetcode+Java+动态规划IV
494.目标和
给你一个非负整数数组
nums
和一个整数target
。向数组中的每个整数前添加
'+'
或'-'
,然后串联起所有整数,可以构造一个 表达式 :
- 例如,
nums = [2, 1]
,可以在2
之前添加'+'
,在1
之前添加'-'
,然后串联起来得到表达式"+2-1"
。返回可以通过上述方法构造的、运算结果等于
target
的不同 表达式 的数目。示例 1:
输入:nums = [1,1,1,1,1], target = 3 输出:5 解释:一共有 5 种方法让最终目标和为 3 。 -1 + 1 + 1 + 1 + 1 = 3 +1 - 1 + 1 + 1 + 1 = 3 +1 + 1 - 1 + 1 + 1 = 3 +1 + 1 + 1 - 1 + 1 = 3 +1 + 1 + 1 + 1 - 1 = 3示例 2:
输入:nums = [1], target = 1 输出:1提示:
1 <= nums.length <= 20
0 <= nums[i] <= 1000
0 <= sum(nums[i]) <= 1000
-1000 <= target <= 1000
原理
- 问题分解:
- 转化为 0-1 背包问题:
- 物品为 nums[i],重量和价值均为 nums[i]。
- 背包容量为 bagSize = (sum + target) / 2。
- 目标是计算和为 bagSize 的子集方案数。
- 定义 dp[j] 为和为 j 的子集方案数。
- 状态转移:
- 对于每个数字 nums[i]:
- 不选:dp[j] 不变。
- 选(需 j >= nums[i]):dp[j] += dp[j - nums[i]](累加方案数)。
- 使用一维数组,从大到小遍历 j,确保 dp[j - nums[i]] 使用上一轮状态。
- 初始化:
- dp[0] = 1:空子集和为 0,有 1 种方案。
- 其他 dp[j] = 0(默认)。
- 边界条件:
- 如果 |target| > sum,无解,返回 0。
- 如果 (sum + target) % 2 == 1,无整数解,返回 0。
- 如果 bagSize < 0,无解(由 |target| <= sum 保证)。
- 时间和空间复杂度:
- 时间复杂度:O(n * bagSize),n 为数组长度,bagSize <= (sum + target) / 2 <= 2000。
- 空间复杂度:O(bagSize),使用一维 DP 数组。
代码
class Solution {public int findTargetSumWays(int[] nums, int target) {// 计算总和int sum = 0;for (int num : nums) {sum += num;}// 检查边界条件if (Math.abs(target) > sum || (sum + target) % 2 == 1) {return 0;}// 计算背包容量int bagSize = (sum + target) / 2;// 创建 DP 数组int[] dp = new int[bagSize + 1];dp[0] = 1;// 遍历每个数字for (int num : nums) {if (num > bagSize) continue; // 跳过无效数字for (int j = bagSize; j >= num; j--) {dp[j] += dp[j - num];}}return dp[bagSize];}
}
474.一和零
给你一个二进制字符串数组
strs
和两个整数m
和n
。请你找出并返回
strs
的最大子集的长度,该子集中 最多 有m
个0
和n
个1
。如果
x
的所有元素也是y
的元素,集合x
是集合y
的 子集 。示例 1:
输入:strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3 输出:4 解释:最多有 5 个 0 和 3 个 1 的最大子集是 {"10","0001","1","0"} ,因此答案是 4 。 其他满足题意但较小的子集包括 {"0001","1"} 和 {"10","1","0"} 。{"111001"} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3 。示例 2:
输入:strs = ["10", "0", "1"], m = 1, n = 1 输出:2 解释:最大的子集是 {"0", "1"} ,所以答案是 2 。提示:
1 <= strs.length <= 600
1 <= strs[i].length <= 100
strs[i]
仅由'0'
和'1'
组成1 <= m, n <= 100
原理
- 问题分解:
- 定义 dp[j][k] 为在最多 j 个 0 和 k 个 1 的限制下,能选择的最大字符串数量(子集长度)。
- 对于每个字符串(有 zeros 个 0 和 ones 个 1):
- 不选:dp[j][k] 不变。
- 选(需 j >= zeros 且 k >= ones):dp[j][k] = max(dp[j][k], dp[j - zeros][k - ones] + 1)。
- 状态转移:
- dp[j][k] = max(dp[j][k], dp[j - zeros][k - ones] + 1)。
- 初始化:
- dp[0][0] = 0(无 0 和 1 时,子集长度为 0)。
- 其他 dp[j][k] 默认初始化为 0(Java 数组默认值)。
- 遍历顺序:
- 使用二维 DP 数组,需从大到小遍历 j 和 k,以避免覆盖上一轮的状态(类似一维 0-1 背包的逆序遍历)。
- 每个字符串的 zeros 和 ones 通过遍历字符计算。
- 时间和空间复杂度:
- 时间复杂度:O(L + N * m * n),其中:
- L 是 strs 中所有字符串的总长度(计算 zeros 和 ones,L <= 600 * 100 = 6 * 10^4)。
- N 是字符串数量(N <= 600),m * n 是 DP 数组遍历的次数(m, n <= 100)。
- 空间复杂度:O(m * n),使用 m+1 * n+1 的 DP 数组。
代码
class Solution {public int findMaxForm(String[] strs, int m, int n) {// 创建二维 DP 数组int[][] dp = new int[m + 1][n + 1];// 预计算每个字符串的 0 和 1 数量int[] zeros = new int[strs.length];int[] ones = new int[strs.length];for (int i = 0; i < strs.length; i++) {for (char c : strs[i].toCharArray()) {if (c == '0') zeros[i]++;else ones[i]++;}}// 遍历每个字符串for (int i = 0; i < strs.length; i++) {if (zeros[i] > m || ones[i] > n) continue;// 从大到小遍历for (int j = m; j >= zeros[i]; j--) {for (int k = n; k >= ones[i]; k--) {dp[j][k] = Math.max(dp[j][k], dp[j - zeros[i]][k - ones[i]] + 1);}}}return dp[m][n];}
}
完全背包理论基础
题目描述
小明是一位科学家,他需要参加一场重要的国际科学大会,以展示自己的最新研究成果。他需要带一些研究材料,但是他的行李箱空间有限。这些研究材料包括实验设备、文献资料和实验样本等等,它们各自占据不同的重量,并且具有不同的价值。小明的行李箱所能承担的总重量是有限的,问小明应该如何抉择,才能携带最大价值的研究材料,每种研究材料可以选择无数次,并且可以重复选择。
输入描述
第一行包含两个整数,n,v,分别表示研究材料的种类和行李所能承担的总重量接下来包含 n 行,每行两个整数 wi 和 vi,代表第 i 种研究材料的重量和价值
输出描述
输出一个整数,表示最大价值。
输入示例
4 5
1 2
2 4
3 4
4 5
输出示例
10
提示信息
第一种材料选择五次,可以达到最大值。数据范围:
1 <= n <= 10000;
1 <= v <= 10000;
1 <= wi, vi <= 10^9.
原理
- 问题分解:
- 定义 dp[i][j] 为前 i 种物品(索引 0 到 i-1)在容量 j 下的最大价值。
- 状态转移:
- 不选第 i-1 种物品:dp[i][j] = dp[i - 1][j]。
- 选第 i-1 种物品(可多次):dp[i][j] = max(dp[i][j], dp[i][j - weight[i-1]] + value[i-1])。
- 合并为:dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i-1]] + value[i-1])(if j >= weight[i-1])。
- 初始化:
- dp[0][j] = k * value[0],其中 k = j / weight[0](第一种物品可重复选择)。
- dp[i][0] = 0(容量为 0,价值为 0)。
- 一维 DP 优化:
- 由于每种物品可重复选择,可以用一维数组 dp[j],表示容量 j 下的最大价值。
- 状态转移:dp[j] = max(dp[j], dp[j - weight[i]] + value[i])(if j >= weight[i])。
- 遍历 j 从小到大(与 0-1 背包相反),因为需要使用当前物品的最新状态。
- 时间和空间复杂度:
- 二维 DP:
- 时间复杂度:O(n * v),遍历 n 种物品和 v 个容量。
- 空间复杂度:O(n * v)。
- 一维 DP:
- 时间复杂度:O(n * v)。
- 空间复杂度:O(v)。
代码
import java.util.Scanner;public class Main {public static void main(String[] args) {Scanner scanner = new Scanner(System.in);// 读取种类 n 和容量 vint n = scanner.nextInt();int v = scanner.nextInt();// 创建数组存储重量和价值int[] weight = new int[n];long[] value = new long[n]; // 使用 long 存储价值// 读取输入for (int i = 0; i < n; i++) {weight[i] = scanner.nextInt();value[i] = scanner.nextLong();}// 创建二维 DP 数组long[][] dp = new long[n][v + 1];// 初始化第一种物品for (int j = 0; j <= v; j++) {dp[0][j] = (j / weight[0]) * value[0]; // 尽量多次选择第一种物品}// 动态规划for (int i = 1; i < n; i++) {for (int j = 0; j <= v; j++) {// 不选第 i 种物品dp[i][j] = dp[i - 1][j];// 选第 i 种物品(可多次)if (j >= weight[i]) {dp[i][j] = Math.max(dp[i][j], dp[i][j - weight[i]] + value[i]);}}}System.out.println(dp[n - 1][v]);scanner.close();}
}
518.零钱兑换II
给你一个整数数组
coins
表示不同面额的硬币,另给一个整数amount
表示总金额。请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回
0
。假设每一种面额的硬币有无限个。
题目数据保证结果符合 32 位带符号整数。
示例 1:
输入:amount = 5, coins = [1, 2, 5] 输出:4 解释:有四种方式可以凑成总金额: 5=5 5=2+2+1 5=2+1+1+1 5=1+1+1+1+1示例 2:
输入:amount = 3, coins = [2] 输出:0 解释:只用面额 2 的硬币不能凑成总金额 3 。示例 3:
输入:amount = 10, coins = [10] 输出:1提示:
1 <= coins.length <= 300
1 <= coins[i] <= 5000
coins
中的所有值 互不相同0 <= amount <= 5000
原理
- 问题分解:
- 定义 dp[j] 为凑成金额 j 的组合数。
- 状态转移:
- 对于每种硬币 coins[i]:
- 不使用该硬币:dp[j] 不变。
- 使用该硬币(需 j >= coins[i]):dp[j] += dp[j - coins[i]](累加组合数)。
- 遍历 j 从小到大(完全背包特性),因为需要使用当前硬币的最新状态。
- 初始化:
- dp[0] = 1:金额 0 有 1 种组合(空组合)。
- 其他 dp[j] = 0(默认)。
- 时间和空间复杂度:
- 时间复杂度:O(n * amount),遍历 n 种硬币,每种硬币检查最多 amount 个金额。
- 空间复杂度:O(amount),使用一维 DP 数组。
- 数据范围:
- 1 <= coins.length <= 300
- 1 <= coins[i] <= 5000
- 0 <= amount <= 5000
- 结果符合 32 位带符号整数(2^31 - 1 ≈ 2.1 * 10^9)。
代码
class Solution {public int change(int amount, int[] coins) {// 创建一维 DP 数组int[] dp = new int[amount + 1];// 初始化:金额 0 有 1 种组合dp[0] = 1;// 遍历每种硬币for (int coin : coins) {// 从 coin 开始遍历金额for (int j = coin; j <= amount; j++) {dp[j] += dp[j - coin];}}return dp[amount];}
}