> 技术文档 > Leetcode+Java+动态规划IV

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  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 和容量 v int 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 = 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=55=2+2+15=2+1+1+15=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]; }}