一: 背包问题
1.1 01 背包
题目链接:01 背包
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
// 不做空间优化,用 dp 表存下所有状态
Scanner sc = new Scanner(System.in);
// 读入 n 和 V
int n = sc.nextInt();
int V = sc.nextInt();
// 定义物品的体积 v 和价值 w
int[] v = new int[n + 1]; // 体积数组
int[] w = new int[n + 1]; // 价值数组
for (int i = 1; i <= n; i++) {
v[i] = sc.nextInt();
w[i] = sc.nextInt();
}
// dp 数组,用于存储当前背包容量下的最大价值
int[][] dp = new int[n + 1][V + 1];
// 解决第一个问题(最大价值问题)
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= V; j++) {
dp[i][j] = dp[i - 1][j]; // 不选当前物品
if (j >= v[i]) {
dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - v[i]] + w[i]); // 选当前物品
}
}
}
System.out.println(dp[n][V]);
// 解决第二个问题(是否能正好装满背包)
// 清空 dp 数组
for (int i = 0; i <= n; i++) {
for (int j = 0; j <= V; j++) {
dp[i][j] = 0;
}
}
// 初始化背包容量为 0 的状态为 -1,表示不可达
for (int j = 1; j <= V; j++) {
dp[0][j] = -1; // 0 物品,容量 j 不可能达到
}
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= V; j++) {
dp[i][j] = dp[i - 1][j]; // 不选当前物品
if (j >= v[i] && dp[i - 1][j - v[i]] != -1) {
dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - v[i]] + w[i]);
}
}
}
// 如果 dp[n][V] == -1,表示无法正好装满背包,输出 0;否则输出最大价值
System.out.println(dp[n][V] == -1 ? 0 : dp[n][V]);
sc.close();
}
}
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
// 利用滚动数组进行空间优化
Scanner sc = new Scanner(System.in);
// 读入 n 和 V
int n = sc.nextInt();
int V = sc.nextInt();
// 定义物品的体积 v 和价值 w
int[] v = new int[n + 1]; // 物品体积数组
int[] w = new int[n + 1]; // 物品价值数组
for (int i = 1; i <= n; i++) {
v[i] = sc.nextInt();
w[i] = sc.nextInt();
}
// dp 数组,用于存储当前背包容量下的最大价值
int[] dp = new int[V + 1];
// 解决第一个问题(01背包问题)
// 从后往前遍历背包容量,以确保每个物品只使用一次
for (int i = 1; i <= n; i++) {
for (int j = V; j >= v[i]; j--) {
dp[j] = Math.max(dp[j], dp[j - v[i]] + w[i]);
}
}
System.out.println(dp[V]);
// 解决第二个问题(是否能够正好装满背包)
// 清空 dp 数组
for (int i = 0; i <= V; i++) {
dp[i] = 0;
}
// 初始化背包容量为 0 的状态为 -1,表示不可达
for (int j = 1; j <= V; j++) {
dp[j] = -1;
}
for (int i = 1; i <= n; i++) {
for (int j = V; j >= v[i]; j--) {
if (dp[j - v[i]] != -1) {
dp[j] = Math.max(dp[j], dp[j - v[i]] + w[i]);
}
}
}
// 如果 dp[V] == -1,表示无法正好装满背包,输出 0;否则输出最大价值
System.out.println(dp[V] == -1 ? 0 : dp[V]);
sc.close();
}
}
1.2 分割等和子集
题目链接:分割等和子集
class Solution {
public boolean canPartition(int[] nums) {
// 采用动态规划的思想解决问题,首先计算一下这个数组的总和,并处理一下特殊的情况
int sum = 0;
for(int x : nums) sum += x;
if(sum % 2 == 1) return false;
// 接下来开始创建一个 dp 表,dp[i][j] 表示在前 i 个元素中是否能组成和为 j 的子集
int n = nums.length, aim = sum / 2;
boolean[][] dp = new boolean[n + 1][aim + 1];
// 接着初始化一下第一列
for(int i = 0; i <= n; i++)
dp[i][0] = true;
// 初始化完成后就可以从上往下开始填表了
for(int i = 1; i <= n; i++){
for(int j = 1; j <= aim; j++){
// 注意下标映射
dp[i][j] = dp[i - 1][j]; // 默认不选择当前元素,继承上一行的状态
if(j >= nums[i - 1]) dp[i][j] = dp[i][j] || dp[i - 1][j - nums[i - 1]];
}
}
return dp[n][aim];
}
}
class Solution {
public boolean canPartition(int[] nums) {
// 采用动态规划的思想解决问题,并采用滚动数组进行优化
// 首先统计一下 nums 数组的总和,并处理一下特殊情况
int sum = 0;
for(int x : nums) sum += x;
if(sum % 2 == 1) return false;
// 接着创建一个 dp 表,dp[j] 表示是否可以用某些元素组成和为 j 的子集
int aim = sum / 2, n = nums.length;
boolean[] dp = new boolean[aim + 1];
// 接着初始化 dp 表
dp[0] = true;
// 因为填表需要 nums 中的元素和 dp 表的元素,所以两层 for 循环固定两个数
// 逐个遍历 nums 数组中的每个元素
for(int i = 1; i <= n; i++){
// 从后往前填表,因为要防止 dp 表被重复使用和 dp 表的值被覆盖
// 如果 j 小于 nums[i - 1],那么 j - nums[i - 1] 会变成负数,这样的索引是无效的,,因此在内层循环中应该从 j = aim 开始,直到 j >= nums[i - 1] 结束,此时 dp[j] 天然满足 j >= nums[i - 1] 条件
for(int j = aim; j >= nums[i - 1]; j--){
dp[j] = dp[j] || dp[j - nums[i - 1]];
}
}
return dp[aim];
}
}
1.3 目标和
题目链接:目标和
class Solution {
public int findTargetSumWays(int[] nums, int target) {
// 采用动态规划的思想解决问题,首先求一下 a 的值并处理一下特殊情况
int sum = 0;
for(int x : nums) sum += x;
int a = (sum + target) / 2;
if(a < 0 || (sum + target) % 2 == 1) return 0;
// 接着创建并初始化 dp 表
int n = nums.length;
int[][] dp = new int[n + 1][a + 1];
dp[0][0] = 1;
// 接着就可以从上往下开始填表了,从 1 开始是因为我们需要逐步考虑每个元素
for(int i = 1; i <= n; i++){
// 从和为 0 开始,逐渐更新所有可能的和
for(int j = 0; j <= a; j++){
// 默认为不选,注意下标映射关系
dp[i][j] = dp[i - 1][j];
if(j >= nums[i - 1]) dp[i][j] += dp[i - 1][j - nums[i - 1]];
}
}
return dp[n][a];
}
}
class Solution {
public int findTargetSumWays(int[] nums, int target) {
// 采用动态规划的思想解决问题,并用滚动数组进行空间优化
int sum = 0, n = nums.length;
for(int x : nums) sum += x;
int aim = (sum + target) / 2;
if(aim < 0 || (sum + target) % 2 == 1) return 0;
// 接着创建一个 dp 表,dp[j] 表示和为 j 的组合数
int[] dp = new int[aim + 1];
dp[0] = 1;
// 接着开始填表,外层循环遍历 nums 中的每个元素
for(int i = 1; i <= n; i++){
// 内层循环从后往前填表
for(int j = aim; j >= nums[i - 1]; j--){
dp[j] += dp[j - nums[i - 1]];
}
}
return dp[aim];
}
}
1.4 最后一块石头的重量 II
题目链接:最后一块石头的重量 II
class Solution {
public int lastStoneWeightII(int[] stones) {
// 采用动态规划的思想解决问题,首先求一下 stones 数组的总和
int sum = 0;
for(int x : stones) sum += x;
// 接着创建一个 dp 表,dp[i][j] 表示前 i 个石头中,和为 j 的最大子集和,m 是我们想要找到的最接近 sum / 2 的子集和
int m = sum / 2, n = stones.length;
int[][] dp = new int[n + 1][m + 1];
// 因为要初始化的地方都为 0 ,所以直接跳过,进入填表
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
// 默认为不选
dp[i][j] = dp[i - 1][j];
if(j >= stones[i - 1]) dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - stones[i - 1]] + stones[i - 1]);
}
}
return sum - 2 * dp[n][m];
}
}
class Solution {
public int lastStoneWeightII(int[] stones) {
// 采用动态规划的思想解决问题,首先求一下 stones 数组的总和,并用滚动数组进行空间优化
int sum = 0;
for(int x : stones) sum += x;
// 接着创建一个 dp 表,dp[i][j] 表示前 i 个石头中,和为 j 的最大子集和,m 是我们想要找到的最接近 sum / 2 的子集和
int m = sum / 2, n = stones.length;
int[] dp = new int[m + 1];
// 因为要初始化的地方都为 0 ,所以直接跳过,进入填表
for(int i = 1; i <= n; i++){
for(int j = m; j >= stones[i - 1]; j--){
dp[j] = Math.max(dp[j], dp[j - stones[i - 1]] + stones[i - 1]);
}
}
return sum - 2 * dp[m];
}
}
1.5 完全背包
题目链接:完全背包
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
// 读入物品个数和背包容量
int n = in.nextInt(); // 物品数量
int V = in.nextInt(); // 背包容量
// 创建数组,分别保存物品的体积、价值
int[] v = new int[n + 1]; // 物品体积数组
int[] w = new int[n + 1]; // 物品价值数组
// dp[i][j] 表示前 i 个物品,在背包容量为 j 时能获得的最大价值
int[][] dp = new int[n + 1][V + 1];
// 读入每个物品的体积和价值
for (int i = 1; i <= n; i++) {
v[i] = in.nextInt(); // 物品的体积
w[i] = in.nextInt(); // 物品的价值
}
// 完全背包问题:求最大价值
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= V; j++) {
dp[i][j] = dp[i - 1][j]; // 不选择当前物品
if (j >= v[i]) { // 如果当前背包容量大于等于当前物品的体积
dp[i][j] = Math.max(dp[i][j], dp[i][j - v[i]] + w[i]); // 选择当前物品
}
}
}
// 输出第一个问题的结果:背包容量为 V 时的最大价值
System.out.println(dp[n][V]);
// 第二个问题:判断能否完全填满背包
// 初始化 dp 数组
for (int i = 0; i <= n; i++) {
for (int j = 0; j <= V; j++) {
dp[i][j] = 0; // 先将所有的 dp 值初始化为 0
}
}
// 特殊初始化:当背包容量为 0 时,最大价值为 0
dp[0][0] = 0;
// 初始化 dp 数组,将不可能填满的容量标记为 -1
for (int j = 1; j <= V; j++) {
dp[0][j] = -1; // dp[0][j] == -1 表示不可能填满背包
}
// 动态规划:填充 dp 数组,判断是否能够填满背包
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= V; j++) {
dp[i][j] = dp[i - 1][j]; // 不选当前物品
if (j >= v[i] && dp[i][j - v[i]] != -1) { // 如果能选择当前物品并且能填满剩余的背包
dp[i][j] = Math.max(dp[i][j], dp[i][j - v[i]] + w[i]); // 更新 dp[i][j] 为最大价值
}
}
}
// 输出第二个问题的结果:若无法填满背包,输出 0,否则输出最大价值
System.out.println(dp[n][V] == -1 ? 0 : dp[n][V]);
in.close(); // 关闭输入流
}
}
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
// 读入数据
int n = in.nextInt(); // 物品个数
int V = in.nextInt(); // 背包容量
int[] v = new int[n + 1]; // 物品体积
int[] w = new int[n + 1]; // 物品价值
int[] dp = new int[V + 1]; // dp[i]表示背包容量为i时的最大价值
// 读入每个物品的体积和价值
for (int i = 1; i <= n; i++) {
v[i] = in.nextInt(); // 物品体积
w[i] = in.nextInt(); // 物品价值
}
// 第一个问题:完全背包问题,求最大价值
for (int i = 1; i <= n; i++) {
for (int j = v[i]; j <= V; j++) {
dp[j] = Math.max(dp[j], dp[j - v[i]] + w[i]);
}
}
System.out.println(dp[V]); // 输出第一个问题的结果
// 第二个问题:判断能否填满背包,输出最大价值或0
// 重置 dp 数组
for (int i = 0; i <= V; i++) {
dp[i] = Integer.MIN_VALUE; // 初始化为极小值,表示无法填满背包
}
// 初始状态,第0个物品时的背包容量
dp[0] = 0; // 背包容量为0时的最大价值为0
// 完全背包动态规划过程
for (int i = 1; i <= n; i++) {
for (int j = v[i]; j <= V; j++) {
dp[j] = Math.max(dp[j], dp[j - v[i]] + w[i]);
}
}
// 如果 dp[V] < 0,表示无法填满背包,输出 0,否则输出最大价值
System.out.println(dp[V] < 0 ? 0 : dp[V]);
// 关闭输入流
in.close();
}
}
1.6 零钱兑换
题目链接:零钱兑换
class Solution {
public int coinChange(int[] coins, int amount) {
// 采用动态规划的思想解决问题,首先创建一个 dp 表, dp[i][j] 表示使用前 i 个硬币来凑成金额 j 的最小硬币数
int n = coins.length, INF = 0x3f3f3f3f; // INF 表示正无穷大
int[][] dp = new int[n + 1][amount + 1];
// 接着开始初始化
for(int j = 1; j <= amount; j++)
dp[0][j] = INF;
// 初始化完成后开始从上往下,从左往右填表
for(int i = 1; i <= n; i++){
for(int j = 0; j <= amount; j++){
// 默认为不选
dp[i][j] = dp[i - 1][j];
if(j >= coins[i - 1]) dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - coins[i - 1]] + 1);
}
}
return dp[n][amount] >= INF ? -1 : dp[n][amount];
}
}
class Solution {
public int coinChange(int[] coins, int amount) {
// 采用动态规划的思想解决问题,并用滚动数组进行空间优化,首先创建一个 dp 表, dp[i][j] 表示使用前 i 个硬币来凑成金额 j 的最小硬币数
int n = coins.length, INF = 0x3f3f3f3f; // INF 表示正无穷大
int[] dp = new int[amount + 1];
// 接着开始初始化
for(int j = 1; j <= amount; j++)
dp[j] = INF;
// 初始化完成后开始从上往下,从左往右填表
for(int i = 1; i <= n; i++){
// 在这个问题中,我们没有限制一个硬币只能用一次,因此可以按照 从左到右 的顺序填表。
// 每次更新 dp[j] 时,其实是基于之前的 dp[j - coins[i - 1]],而 dp[j - coins[i - 1]] 只依赖于上一轮的状态,所以可以安全地在顺序填表时更新。
for(int j = coins[i - 1]; j <= amount; j++){
dp[j] = Math.min(dp[j], dp[j - coins[i - 1]] + 1);
}
}
return dp[amount] >= INF ? -1 : dp[amount];
}
}
1.7 零钱兑换 II
题目链接:零钱兑换 II
class Solution {
public int change(int amount, int[] coins) {
// 采用动态规划的思想解决这个问题,首先创建一个 dp 表,dp[i] 表示金额 i 的组合数
int[] dp = new int[amount + 1];
// 接着初始化 dp
dp[0] = 1;
// 接着开始从上往下,从左往右填表
for(int i = 0; i < coins.length; i++){
for(int j = coins[i]; j <= amount; j++){
dp[j] += dp[j - coins[i]];
}
}
return dp[amount];
}
}
1.8 完全平方数
题目链接:完全平方数
class Solution {
public int numSquares(int n) {
// 首先创建并初始化 dp 表,dp 表用于存储每个数的最小完全平方数的个数
int[] dp = new int[n + 1];
dp[1] = 1;
for (int i = 2; i <= n; i++) {
dp[i] = 1 + dp[i - 1];
for (int j = 2; j * j <= i; j++)
dp[i] = Math.min(dp[i], dp[i - j * j] + 1);
}
return dp[n];
}
}
二: 二维费用的背包问题
2.1 一和零
题目链接:二维费用的背包问题 一和零
class Solution {
public int findMaxForm(String[] strs, int m, int n) {
// 采用动态规划的思想解决问题,首先创建一个 dp 表,dp[i][j][k],表示考虑前 i 个字符串,用 j 个 '0' 和 k 个 '1' 的情况下,能组成的最大字符串个数
int len = strs.length;
int[][][] dp = new int[len + 1][m + 1][n + 1];
// 因为无需初始化,所以直接开始填表,填表保证 i 从小到大
for (int i = 1; i <= len; i++) {
int a = 0, b = 0; // a 表示 '0' 的个数,b 表示 '1' 的个数
for (char ch : strs[i - 1].toCharArray()) {
if (ch == '0') a++;
else b++;
}
for (int j = 0; j <= m; j++) {
for (int k = 0; k <= n; k++) {
dp[i][j][k] = dp[i - 1][j][k]; // 默认为不选择当前字符串的情况
if (j >= a && k >= b) dp[i][j][k] = Math.max(dp[i][j][k], dp[i - 1][j - a][k - b] + 1);
}
}
}
return dp[len][m][n];
}
}
class Solution {
public int findMaxForm(String[] strs, int m, int n) {
// 采用动态规划的思想解决问题,并用滚动数组进行优化
// 首先创建一个 dp 表,dp[j][k],表示使用 j 个 '0' 和 k 个 '1' 能够组成的最大子集数量
int len = strs.length;
int[][] dp = new int[m + 1][n + 1];
// 接着统计字符串 '0' 和 '1' 的个数,并更新 dp 数组
for (int i = 1; i <= len; i++) {
int a = 0, b = 0;
for (char ch : strs[i - 1].toCharArray()) {
if (ch == '0') a++;
else b++;
}
for (int j = m; j >= a; j--) {
for (int k = n; k >= b; k--) {
dp[j][k] = Math.max(dp[j][k], dp[j - a][k - b] + 1);
}
}
}
return dp[m][n];
}
}
2.2 盈利计划
题目链接: 盈利计划
class Solution {
public int profitableSchemes(int n, int m, int[] g, int[] p) {
// 首先创建一个 dp 表,dp[i][j][k] 表示前 i 个工作,选择了 j 人,赚取了 k 利润的方案数
int len = g.length;
int MOD = (int) 1e9 + 7; // 因为结果需要对 10^9 + 7 取模,所以定义一个 MOD 变量
int[][][] dp = new int[len + 1][n + 1][m + 1];
// 初始化一下 dp 表
for (int j = 0; j <= n; j++)
dp[0][j][0] = 1;
// 接着开始填表
for (int i = 1; i <= len; i++) {
for (int j = 0; j <= n; j++) {
for (int k = 0; k <= m; k++) {
dp[i][j][k] = dp[i - 1][j][k]; // 默认不选择当前工作 i 的方案数
if (j >= g[i - 1]) dp[i][j][k] += dp[i - 1][j - g[i - 1]][Math.max(0, k - p[i - 1])];
// 由于可能存在溢出,方案数需要对 10^9 + 7 取模
dp[i][j][k] %= MOD;
}
}
}
return dp[len][n][m];
}
}
class Solution {
public int profitableSchemes(int n, int m, int[] g, int[] p) {
// 采用动态规划的思想解决问题,并用滚动数组进行优化,首先创建一个 dp 表,dp[j][k] 表示雇佣 j 个人并赚取 k 利润的方案数
int len = g.length;
int MOD = (int) 1e9 + 7; // 因为结果需要对 10^9 + 7 取模,所以定义一个 MOD 变量
int[][] dp = new int[n + 1][m + 1];
// 初始化一下 dp 表
for (int j = 0; j <= n; j++)
dp[j][0] = 1;
// 接着开始填表
for (int i = 1; i <= len; i++) {
for (int j = n; j >= g[i - 1]; j--) {
for (int k = m; k >= 0; k--) {
dp[j][k] += dp[j - g[i - 1]][Math.max(0, k - p[i - 1])];
// 由于可能存在溢出,方案数需要对 10^9 + 7 取模
dp[j][k] %= MOD;
}
}
}
return dp[n][m];
}
}
三:似包非包
3.1 组合总和 IV
题目链接:组合总和 IV
class Solution {
public int combinationSum4(int[] nums, int target) {
// 采用动态规划的思想解决问题,首先创建一个 dp 表,dp[i] 表示组成目标 i 的组合数
int[] dp = new int[target + 1];
// 接着初始化 dp 表
dp[0] = 1;
// 接着开始从左往右填表
for (int i = 1; i <= target; i++) {
for (int x : nums) {
if (i >= x) dp[i] += dp[i - x];
}
}
return dp[target];
}
}
3.2 不同的二叉搜索树
题目链接:不同的二叉搜索树
class Solution {
public int numTrees(int n) {
// 采用动态规划的思想解决问题,首先创建一个 dp 表,dp[i] 表示由 i 个节点组成的不同 BST 的数量
int[] dp = new int[n + 1];
// 初始化 dp 表
dp[0] = 1;
// 接着开始从左往右填表
for (int i = 1; i <= n; i++)
for (int j = 1; j <= i; j++)
dp[i] += dp[j - 1] * dp[i - j];
return dp[n];
}
}