思维导图总结
由于 CSDN 有图片大小限制,只能上传压缩过的图片。。。
动规题目分类:
- 背包问题A
- 打家劫舍
- 股票问题
- 子序列问题
- …
入门题
斐波那契数
题目:509. 斐波那契数
标准 DP:
public int fib(int n) {
if (n == 0) return 0;
// dp[i] 第 i 个斐波那契数
int[] dp = new int[n + 1];
// 初始化 dp 数组
dp[0] = 0;
dp[1] = 1;
// 递推
for (int i = 2; i <= n; i++)
dp[i] = dp[i - 1] + dp[i - 2];
return dp[n];
}
滚动数组思想:使用两个变量优化
public int fib(int n) {
if (n == 0) return 0;
int a = 0, b = 1;
for (int i = 2; i <= n; i++) {
b = a + b;
a = b - a;
}
return b;
}
爬楼梯
题目:70. 爬楼梯
public int climbStairs(int n) {
// dp[i] 爬到第 i 阶时的方法数量
int[] dp = new int[n + 1];
dp[0] = dp[1] = 1;
for (int i = 2; i <= n; i++)
dp[i] = dp[i - 1] + dp[i - 2];
return dp[n];
}
背包问题:
public int climbStairs(int n) {
// dp[i] 上第 i 层的方法数量
int[] dp = new int[n + 1];
dp[0] = 1;
for (int i = 1; i <= n; i++) // 背包
for (int j = 1; j <= 2; j++) // 物品
if (i >= j) dp[i] += dp[i - j];
return dp[n];
}
简单题
使用最小花费爬楼梯
给你一个整数数组 cost
,其中 cost[i]
是从楼梯第 i
个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。
你可以选择从下标为 0
或下标为 1
的台阶开始爬楼梯。
请你计算并返回达到楼梯顶部的最低花费。
输入:cost = [10,15,20]
输出:15
解释:你将从下标为 1 的台阶开始。
- 支付 15 ,向上爬两个台阶,到达楼梯顶部。
总花费为 15 。
public int minCostClimbingStairs(int[] cost) {
int n = cost.length;
// dp[i] 爬到第 i 层的最低花费
int[] dp = new int[n + 1];
dp[0] = dp[1] = 0;
for (int i = 2; i <= n; i++)
dp[i] = Math.min(dp[i - 2] + cost[i - 2], dp[i - 1] + cost[i - 1]);
return dp[n];
}
不同路径
题目:62. 不同路径
一个机器人位于一个 m x n
网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
输入:m = 3, n = 2
输出:3
解释:
---------
| s | o |
---------
| o | o |
---------
| o | e |
---------
从左上角开始,总共有 3 条路径可以到达右下角。
1. 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右
3. 向下 -> 向右 -> 向下
public int uniquePaths(int m, int n) {
// dp[i][j] 到达 a[i][j] 的不同路径数
int[][] dp = new int[m][n];
// 原点开始一直 往下 或 往右 走, 路径都是 1
for (int i = 0; i < m; i++) dp[i][0] = 1;
for (int i = 0; i < n; i++) dp[0][i] = 1;
// 递推
for (int i = 1; i < m; i++)
for (int j = 1; j < n; j++)
// 当前路径总数 = 从左过来的路径 + 从上过来的路径
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
return dp[m - 1][n - 1];
}
不同路径 II
题目:63. 不同路径 II
一个机器人位于一个 m x n
网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish”)。
现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
网格中的障碍物和空位置分别用 1
和 0
来表示。
输入:obstacleGrid =
[[0,0,0],
[0,1,0],
[0,0,0]]
输出:2
解释:3x3 网格的正中间有一个障碍物。
从左上角到右下角一共有 2 条不同的路径:
1. 向右 -> 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右 -> 向右
public int uniquePathsWithObstacles(int[][] g) {
int m = g.length, n = g[0].length;
// dp[i][j] 到达 [i][j] 的不同路径数
int[][] dp = new int[m][n];
dp[0][0] = 0;
// 初始化: 从起点 往右 或 往下, 能走到的初始化为 1
for (int i = 0; i < m; i++) {
if (g[i][0] == 1) break;
dp[i][0] = 1;
}
for (int i = 0; i < n; i++) {
if (g[0][i] == 1) break;
dp[0][i] = 1;
}
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
if (g[i][j] == 1) continue; // 遇到障碍, 跳过
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
return dp[m - 1][n - 1];
}
整数拆分
题目:343. 整数拆分
给定一个正整数 n
,将其拆分为 k
个 正整数 的和( k >= 2
),并使这些整数的乘积最大化。
返回 你可以获得的最大乘积 。
输入: n = 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。
public int integerBreak(int n) {
// dp[i] 整数 i 可以被拆分出的最大乘积
int[] dp = new int[n + 1];
dp[1] = dp[2] = 1;
for (int i = 3; i <= n; i++) {
for (int j = 1; j < i; j++) {
// 前 j 长度进行拆分的最大乘积: dp[j] * (i - j)
// 前 j 长度不进行拆分的最大乘积: j * (i - j)
dp[i] = Math.max(dp[i], Math.max(dp[j] * (i - j), j * (i - j)));
}
}
return dp[n];
}
不同的二叉搜索树*
题目:96. 不同的二叉搜索树
给你一个整数 n
,求恰由 n
个节点组成且节点值从 1
到 n
互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。
输入:n = 3
输出:5
DP:
public int numTrees(int n) {
int[] dp = new int[n + 1];
dp[0] = 1;
// 作为根的元素 1, 2, ..., n
for (int i = 1; i <= n; i++)
// 根的左右子树的元素数量
// 根1 [0, n-1], 根2 [1, n-2], ... 根n [n-1, 0]
for (int j = 1; j <= i; j++)
dp[i] += dp[j - 1] * dp[i - j];
return dp[n];
}
递归:
public int numTrees(int n) {
if (n <= 1) return 1;
int sum = 0;
for (int i = 1; i <= n; i++)
sum += numTrees(i - 1) * numTrees(n - i);
return sum;
}
背包问题总结
能否装满背包(其实就是在求装满背包的最大价值): d p [ j ] = m a x ( d p [ j ] , d p [ j − n u m s [ i ] ] + n u m s [ i ] ) dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]) dp[j]=max(dp[j],dp[j−nums[i]]+nums[i])
- 分隔等和子集
- 最后一块石头的重量 II
装满背包最大价值: d p [ j ] = m a x ( d p [ j ] , d p [ j − w [ i ] ] + v [ i ] ) dp[j] = max(dp[j], dp[j - w[i]] + v[i]) dp[j]=max(dp[j],dp[j−w[i]]+v[i])
- 一和零
装满背包有几种方法: d p [ j ] = d p [ j ] + d p [ j − n u m s [ i ] ] dp[j] = dp[j] + dp[j - nums[i]] dp[j]=dp[j]+dp[j−nums[i]]
- 目标和
- 零钱兑换 II
- 组合总和 IV
- 爬楼梯进阶版
装满背包所有物品的最小个数: d p [ j ] = m i n ( d p [ j ] , d p [ j − c o i n s [ i ] ] + 1 ) dp[j] = min(dp[j], dp[j - coins[i]] + 1) dp[j]=min(dp[j],dp[j−coins[i]]+1)
- 零钱兑换
- 完全平方数
完全背包问题中的循环顺序:
- 如果求组合数就是 外层 for 循环遍历物品,内层 for 遍历背包
- 如果求排列数就是 外层 for 遍历背包,内层 for 循环遍历物品
- 一般求最大值,最小值对遍历顺序没有要求
[1,2]
和[2,1]
是 排列
[1,2]
是 组合
01 背包*
有 N N N 件物品和一个容量是 V V V 的背包,每件物品只能使用一次
第 i i i 件物品的体积是 v i v_i vi,价值是 w i w_i wi
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大
class Solution {
// 二维数组实现
// v - 价值数组, w - 重量数组, c - 背包容量
static int maxValue(int[] v, int[] w, int c) {
// dp[i][j] 有前 i 个物品可选, 最大承重为 j 的背包的物品总价值
int[][] dp = new int[v.length + 1][c + 1];
for (int i = 1; i <= v.length; i++) // 物品
for (int j = 1; j <= c; j++) // 背包
// 容量不够放本次的物品
if (j < w[i - 1]) dp[i][j] = dp[i - 1][j];
// 容量够放本次物品: 比较 放 与 不放 获得的最大总价值来选择
else dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - w[i - 1]] + v[i - 1]);
return dp[v.length][c];
}
// 一维数组实现
static int maxValue1(int[] v, int[] w, int c) {
int[] dp = new int[c + 1];
for (int i = 1; i <= v.length; i++) // 物品
for (int j = c; j >= w[i - 1]; j--) // 背包
dp[j] = Math.max(dp[j], dp[j - w[i - 1]] + v[i - 1]);
return dp[c];
}
public static void main(String[] args) {
int[] values = {
6, 3, 5, 4, 6 }; // 价值
int[] weights = {
2, 2, 6, 5, 4 }; // 重量
int capacity = 10; // 背包容量
System.out.println(maxValue(values, weights, capacity));
System.out.println(maxValue1(values, weights, capacity));
}
}
分隔等和子集
题目:416. 分割等和子集
给你一个 只包含正整数 的 非空 数组 nums
。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。
题目解析:数组元素和为 sum
,如果数组中能找到部分元素和为 sum / 2
,则该数组可以分隔成两个子集
转化成 01 背包:物品数组为 nums
(价值、重量都是 nums[i]
),背包容量为 sum / 2
,求出背包能装的最大价值
public boolean canPartition(int[] nums) {
int sum = Arrays.stream(nums).sum();
// 奇数, 必定无法分隔
if ((sum & 1) == 1) return false;
// 目标子集的元素和
int target = sum / 2;
// 转化成 01背包问题
// dp[i][j]代表可装物品为0-i,背包容量为j的情况下,背包内容量的最大价值
return maxValue(nums, nums, target) == target;
}
二维 01 背包:
int maxValue(int[] v, int[] w, int c) {
// dp[i][j] 在前 i 个物品中选择, 容量为 j 的最大物品价值
int[][] dp = new int[v.length + 1][c + 1];
for (int i = 1; i <= v.length; i++) {
for (int j = 1; j <= c; j++)
if (j < w[i - 1]) dp[i][j] = dp[i - 1][j];
else dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - w[i - 1]] + v[i - 1]);
}
return dp[v.length][c];
}
一维 01 背包:
int maxValue(int[] v, int[] w, int c) {
int[] dp = new int[c + 1];
for (int i = 1; i <= v.length; i++)
for (int j = c; j >= w[i - 1] ; j--)
dp[j] = Math.max(dp[j], dp[j - w[i - 1]] + v[i - 1]);
return dp[c];
}
最后一块石头的重量 II
题目:最后一块石头的重量 II
有一堆石头,用整数数组 stones
表示。其中 stones[i]
表示第 i
块石头的重量。
每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x
和 y
,且 x <= y
。那么粉碎的可能结果如下:
- 如果
x == y
,那么两块石头都会被完全粉碎; - 如果
x != y
,那么重量为x
的石头将会完全粉碎,而重量为y
的石头新重量为y-x
。
最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回0
。
输入:stones = [2,7,4,1,8,1]
输出:1
解释:
组合 2 和 4,得到 2,所以数组转化为 [2,7,1,8,1],
组合 7 和 8,得到 1,所以数组转化为 [2,1,1,1],
组合 2 和 1,得到 1,所以数组转化为 [1,1,1],
组合 1 和 1,得到 0,所以数组转化为 [1],这就是最优值。
题目解析:
x == y
对应石头被完全粉碎(差为 0),x != y
对应大石头粉碎小石头且大石头剩余重量为 y-x(差为 y - x)- 可以发现其实粉碎的过程就是重量 相减 的过程
- 对于两堆石头,让他们粉碎最终的结果就是:两堆石头的和的差值
- 想要石头最终粉碎得到最小重量,就尽量找两堆重量接近的石头
public int lastStoneWeightII(int[] stones) {
// 核心思路:任意选 i 块石头,使它们的重量趋近于总重量的一半,因为这样和另一半抵消的差值就是最小的
// 背包容量为和的一半, 试图寻找尽量装满一半的背包的元素的和
// 最极端就是正好找到一半, 剩下的元素构成另一半, 得出结果为 0
int sum = Arrays.stream(stones).sum();
int max = maxValue(stones, stones, sum / 2);
return Math.abs(max - (sum - max));
}
二维 01 背包:
int maxValue(int[] w, int[] v, int c) {
// dp[i][j] 在前 i 个物品中选择, 容量为 j 的最大物品价值
int[][] dp = new int[w.length + 1][c + 1];
for (int i = 1; i <= w.length; i++) {
for (int j = 1; j <= c; j++) {
if (j < w[i - 1]) dp[i][j] = dp[i - 1][j];
else dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - w[i - 1]] + v[i - 1]);
}
}
return dp[w.length][c];
}
一维 01 背包:
int maxValue(int[] v, int[] w, int c) {
int[] dp = new int[c + 1];
for (int i = 1; i <= v.length; i++)
for (int j = c; j >= w[i - 1] ; j--)
dp[j] = Math.max(dp[j], dp[j - w[i - 1]] + v[i - 1]);
return dp[c];
}
目标和*
题目:目标和
给你一个整数数组 nums
和一个整数 target
。
向数组中的每个整数前添加 '+'
或 '-'
,然后串联起所有整数,可以构造一个 表达式 :
- 例如,
nums = [2, 1]
,可以在2
之前添加'+'
,在1
之前添加'-'
,然后串联起来得到表达式"+2-1"
。
返回可以通过上述方法构造的、运算结果等于target
的不同 表达式 的数目。
输入: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
回溯:
class Solution {
public int findTargetSumWays(int[] nums, int target) {
return dfs(nums, target, 0);
}
int dfs(int[] nums, int target, int idx) {
if (idx == nums.length) {
if (target == 0) return 1;
return 0;
}
return dfs(nums, target - nums[idx], idx + 1) + dfs(nums, target + nums[idx], idx + 1);
}
}
记忆化搜索:
class Solution {
Map<String, Integer> map = new HashMap<>();
public int findTargetSumWays(int[] nums, int target) {
return dfs(nums, target, 0, 0);
}
int dfs(int[] nums, int target, int sum, int idx) {
if (nums.length == idx) {
if (target == sum) return 1;
else return 0;
}
String key = sum + "&" + idx;
if (map.containsKey(key)) return map.get(key);
int val = dfs(nums, target, sum + nums[idx], idx + 1) + dfs(nums, target, sum - nums[idx], idx + 1);
map.put(key, val);
return val;
}
}
二维 DP:
// sumA - 正数和, sumB - 负数和, sum - nums 的和
// sumA + sumB = target
// sumA - sumB = sum
// sumA = (target + sum) / 2
// 同时可以发现 target + sum 必须是偶数, 否则无解
// 转化成 01背包, dp[i][j] 从前 i 个数字中选出若干个, 使得被选出的数字其和为 j 的方案数目
public int findTargetSumWays(int[] nums, int target) {
int sum = Arrays.stream(nums).sum();
if ((target + sum) % 2 == 1) return 0; // 奇数, 无解
// 若 target > sum, 无法凑够; target 太小导致 target < -sum,也凑不够
if (sum < Math.abs(target)) return 0;
// dp[i][j] 从前 i 个数字中选出若干个, 使得被选出的数字其和为 j 的方案数目
// dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i]]
int c = (sum + target) >> 1;
int[][] dp = new int[nums.length + 1][c + 1];
dp[0][0] = 1; // dp[i][0] 除了 dp[0][0] = 1, 有多个数字的情况下, 和为 0 的情况可能有多个
for (int i = 1; i <= nums.length; i++) {
for (int j = 0; j <= c; j++) {
// j 从 0开始, 因为数字和可以为 0 (这点和背包有区别)
// 容量有限, 无法更新
if (j < nums[i - 1]) dp[i][j] = dp[i - 1][j];
// 可以选择第 i 个数字 nums[i - 1], 也可以不选, s两者和
else dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i - 1]];
}
}
return dp[nums.length][c];
}
一维:
public int findTargetSumWays(int[] nums, int target) {
int sum = Arrays.stream(nums).sum();
if ((target + sum) % 2 == 1) return 0;
if (sum < Math.abs(target)) return 0;
// dp[j] 选出数字和为 j 的方案数目 dp[j] += dp[j - nums[i]]
int c = (sum + target) >> 1;
int[] dp = new int[c + 1];
dp[0] = 1;
for (int i = 1; i <= nums.length; i++) {
for (int j = c; j >= 0; j--) {
// j 从 0开始, 因为数字和可以为 0 (这点和背包有区别)
// 容量有限, 无法更新
if (j < nums[i - 1]) continue;
// 可以选择第 i 个数字 nums[i - 1], 也可以不选, s两者和
dp[j] += dp[j - nums[i - 1]];
}
}
return dp[c];
}
一和零
给你一个二进制字符串数组 strs
和两个整数 m
和 n
。
请你找出并返回 strs
的最大子集的长度,该子集中 最多 有 m
个 0
和 n
个 1
。
如果 x
的所有元素也是 y
的元素,集合 x
是集合 y
的 子集 。
输入: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 。
此题求的是最大子集的长度,可以理解为每次满足条件放入的物品价值为 1(即数量 + 1)
dp[i][j][k] = max(dp[i-1][j][k], dp[i-1][j-n_0][k-n_1] + 1)
- n_0 为当前字符串中 0 的个数
- n_1 为当前字符串中 1 的个数
三维 DP:
public int findMaxForm(String[] strs, int m, int n) {
int len = strs.length;
// dp[i][j][k] 在字符数组的前 i 个字符串中, 使用 j 个 0, k 个 1, 的最大字符串的个数
int[][][] dp = new int[len + 1][m + 1][n + 1];
for (int i = 1; i <= len; i++) {
// 物品
for (int j = 0; j <= m; j++) {
// 背包1
for (int k = 0; k <= n; k++) {
// 背包2
int n_1 = getOneNum(strs[i - 1]); // 1 的数量
int n_0 = strs[i - 1].length() - n_1; // 0 的数量
// 不放物品
if (n_0 > j || n_1 > k) dp[i][j][k] = dp[i - 1][j][k];
// 放物品
else dp[i][j][k] = Math.max(dp[i - 1][j][k],
dp[i - 1][j - n_0][k - n_1] + 1); // 此处 +1 其实是价值
}
}
}
return dp[len][m][n];
}
// 获取字符中 '1' 的个数
int getOneNum(String s) {
int cnt = 0;
for (char c : s.toCharArray())
if (c == '1') cnt++;
return cnt;
}
二维 DP:(很难直接写出来,从三维优化过来)
public int findMaxForm(String[] strs, int m, int n) {
int len = strs.length;
// dp[i][j] 在字符数组的字符串中,使用 i 个 0, j 个 1, 的最大字符串的个数
int[][] dp = new int[m + 1][n + 1];
for (int i = 1; i <= len; i++) {
int n_1 = getOneNum(strs[i - 1]); // 1 的数量
int n_0 = strs[i - 1].length() - n_1; // 0 的数量
for (int j = m; j >= n_0; j--)
for (int k = n; k >= n_1; k--)
dp[j][k] = Math.max(dp[j][k], dp[j - n_0][k - n_1] + 1);
}
return dp[m][n];
}
// 获取字符中 '1' 的个数
int getOneNum(String s) {
int cnt = 0;
for (char c : s.toCharArray())
if (c == '1') cnt++;
return cnt;
}
完全背包
有 N 种物品和一个容量是 V 的背包,每种物品都有无限件可用
第 i i i 种物品的体积是 v i v_i vi,价值是 w i w_i wi
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大
public class Solution {
// 朴素版
// v - 价值数组, w - 重量数组, c - 背包容量
static int maxValue(int[] v, int[] w, int c) {
int[][] dp = new int[v.length + 1][c + 1];
for (int i = 1; i <= v.length; i++) // 物品
for (int j = 1; j <= c; j++) // 背包容量
for (int k = 0; k * w[i - 1] <= j; k++) // 物品数量
dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - w[i - 1] * k] + v[i - 1] * k);
return dp[w.length][c];
}
// 二维数组实现
static int maxValue1(int[] v, int[] w, int c) {
// dp[i][j] 有前 i 个物品可选, 最大承重为 j 的背包的物品总价值
int[][] dp = new int[v.length + 1][c + 1];
for (int i = 1; i <= v.length; i++) // 物品
for (int j = 1; j <= c; j++) // 背包
// 容量不够放本次的物品
if (j < w[i - 1]) dp[i][j] = dp[i - 1][j];
// 容量够放本次物品
else dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - w[i - 1]] + v[i - 1]);
return dp[v.length][c];
}
// 一维数组实现
static int maxValue2(int[] v, int[] w, int c) {
int[] dp = new int[c + 1];
for (int i = 1; i <= v.length; i++)
for (int j = w[i - 1]; j <= c; j++)
dp[j] = Math.max(dp[j], dp[j - w[i - 1]] + v[i - 1]);
return dp[c];
}
public static void main(String[] args) {
int[] values = {
6, 3, 5, 4, 6 }; // 价值
int[] weights = {
2, 2, 6, 5, 4 }; // 重量
int capacity = 10; // 背包容量
System.out.println(maxValue(values, weights, capacity)); // 朴素版
System.out.println(maxValue1(values, weights, capacity)); // 二维数组
System.out.println(maxValue2(values, weights, capacity)); // 一维数组
}
}
零钱兑换 II
给你一个整数数组 coins
表示不同面额的硬币,另给一个整数 amount
表示总金额。
请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0
。
假设每一种面额的硬币有无限个。
输入:amount = 5, coins = [1, 2, 5]
输出:4
解释:有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1
完全背包问题中的循环顺序:
- 如果求组合数就是 外层 for 循环遍历物品,内层 for 遍历背包
- 如果求排列数就是 外层 for 遍历背包,内层 for 循环遍历物品
这题属于组合问题,应当先遍历物品再遍历背包。
[1, 2, 2]
和[2, 2, 1]
是相同的,所以是组合(和顺序无关)
0 1 2 3 4 5 (背包容量 j)
1 0 0 0 0 0 没有硬币的时候)
=======================
0 1 2 3 4 5 (背包容量 j)
1 1 1 1 1 1 1
=======================
0 1 2 3 4 5 (背包容量 j)
1 1 1 1 1 1 1
2 2 2 3 3
有了面值为 2 的硬币后:
- 如果不用,方案数还是 dp[j] 种
- 如果用了,看看在放入这枚硬币前,也就是背包容量为 [j-coins[i]] 的时候有几种方案
- 两种情况加起来,所以就是 dp[j] = dp[j] + dp[j-coins[i]]
========================
0 1 2 3 4 5 (背包容量 j)
1 1 1 1 1 1 1
2 2 2 3 3
5 4
一维数组解法:
public int change(int amount, int[] coins) {
// dp[i] 凑出总金额为 i 的硬币组合数
// 选择硬币 coins[i], 当前硬币组合数量有 dp[j - coins[i]] 种
// 不选择硬币 coins[i], 当前硬币组合数量为 dp[j] 种
// 总的组合数量为: dp[j] = dp[j] + dp[j - coins[i]]
int[] dp = new int[amount + 1];
dp[0] = 1; // 金额为 0 时, 什么都不装, 只有这一种方式
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];
}
二维数组解法:
public int change(int amount, int[] coins) {
// dp[i][j] 从前 i 个面额的硬币中选择, 凑出总金额为 j 的硬币的组合数
int[][] dp = new int[coins.length + 1][amount + 1];
// 初始化 dp 数组,金额为 0 时只有一种情况,也就是什么都不装
for (int i = 0; i <= coins.length; i++) dp[i][0] = 1;
for (int i = 1; i <= coins.length; i++) {
// 物品
for (int j = 1; j <= amount; j++) {
// 背包
if (j < coins[i - 1]) dp[i][j] = dp[i - 1][j];
else dp[i][j] = dp[i - 1][j] + dp[i][j - coins[i - 1]];
}
}
return dp[coins.length][amount];
}
另一种常见写法:
for (int i = 1; i <= coins.length; i++) {
// 物品
for (int j = 1; j <= amount; j++) {
// 背包
dp[i][j] = dp[i - 1][j];
if (j >= coins[i - 1]) dp[i][j] += dp[i][j - coins[i - 1]];
}
}
组合总和 IV
题目:377. 组合总和 Ⅳ
给你一个由 不同 整数组成的数组 nums
,和一个目标整数 target
。请你从 nums
中找出并返回总和为 target
的元素组合的个数。
题目数据保证答案符合 32 位整数范围。
输入:nums = [1,2,3], target = 4
输出:7
解释:
所有可能的组合为:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)
请注意,顺序不同的序列被视作不同的组合。
这题属于排列问题,应当先遍历背包容量,再遍历物品。
对于
[1, 2]
和[2, 1]
是不同的,所以是排列(和顺序有关)
一维 DP:
public int combinationSum4(int[] nums, int target) {
// dp[i] 总和为 i 的元素排列个数
// 选这个数: dp[i - nums[j]]
// 不选这个数: dp[i]
int[] dp = new int[target + 1];
dp[0] = 1;
for (int i = 1; i <= target; i++) // 背包
for (int j = 0; j < nums.length; j++) // 物品
if (i >= nums[j]) // 背包放的下
dp[i] += dp[i - nums[j]];
return dp[target];
}
二维 DP:(有点难理解)
public int combinationSum4(int[] nums, int target) {
// dp[i][j] 在前 i 个元素中总和为 j 的排列个数
int[][] dp = new int[nums.length + 1][target + 1];
// 总和为 0, 一个数也不选, 视为一种情况
for (int i = 0; i <= nums.length; i++) dp[i][0] = 1;
for (int j = 1; j <= target; j++) // 背包
for (int i = 1; i <= nums.length; i++) // 物品
for (int k = i; k > 0; k--)
if (j - nums[k - 1] >= 0)
dp[i][j] += dp[i][j - nums[k - 1]];
return dp[nums.length][target];
}
爬楼梯(进阶版)
进阶:一步一个台阶,两个台阶,三个台阶,…,直到 m 个台阶。问有多少种不同的方法可以爬到楼顶呢?
1阶,2阶,… m 阶就是物品,楼顶就是背包。
每一阶可以重复使用,例如跳了 1 阶,还可以继续跳 1 阶。
问跳到楼顶有几种方法其实就是问装满背包有几种方法。
public int climbStairs(int n) {
// dp[i] 爬到 i 楼的方法数
int[] dp = new int[n + 1];
dp[0] = 1;
for (int i = 1; i <= n; i++) // 背包
for (int j = i; j <= m; j++) // 物品
if (i >= j) dp[i] += dp[i - j]; // m 换成 2 可以通过 Leetcode 的爬楼梯
return dp[n];
}
零钱兑换
给你一个整数数组 coins
,表示不同面额的硬币;以及一个整数 amount
,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1
。
你可以认为每种硬币的数量是无限的。
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1
此题和遍历顺序没有关系,因为是求最小值
一般求最小值,DP 数组需要初始化为 MAX 值
public int coinChange(int[] coins, int amount) {
// dp[i] 凑到总金额为 i 需要的最少硬币个数
int[] dp = new int[amount + 1];
Arrays.fill(dp, 1, dp.length, Integer.MAX_VALUE); // dp[0] = 0
for (int i = 0; i < coins.length; i++) // 物品
for (int j = coins[i]; j <= amount; j++) // 背包
if (dp[j - coins[i]] != Integer.MAX_VALUE)
dp[j] = Math.min(dp[j], dp[j - coins[i]] + 1);
return dp[amount] == Integer.MAX_VALUE ? -1 : dp[amount];
}
完全平方数
给你一个整数 n
,返回 和为 n
的完全平方数的最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1
、4
、9
和 16
都是完全平方数,而 3
和 11
不是。
输入:n = 12
输出:3
解释:12 = 4 + 4 + 4
完全平方数就是物品(可以无限件使用),凑个正整数 n 就是背包,问凑满这个背包最少有多少物品?
先物品再背包:
public int numSquares(int n) {
// dp[i] 和为 i 的完全平方数的最少数量
int[] dp = new int[n + 1];
Arrays.fill(dp, 1, dp.length, Integer.MAX_VALUE); // dp[0] = 1
for (int i = 1; i * i <= n; i++) // 物品
for (int j = i * i; j <= n; j++) // 背包
dp[j] = Math.min(dp[j], dp[j - i * i] + 1);
return dp[n];
}
先背包再物品:
public int numSquares(int n) {
// dp[i] 和为 i 的完全平方数的最少数量
int[] dp = new int[n + 1];
Arrays.fill(dp, 1, dp.length, Integer.MAX_VALUE);
for (int i = 0; i <= n; i++) // 背包
for (int j = 1; j * j <= i; j++) // 物品
dp[i] = Math.min(dp[i], dp[i - j * j] + 1);
return dp[n];
}
单词拆分
给你一个字符串 s
和一个字符串列表 wordDict
作为字典。请你判断是否可以利用字典中出现的单词拼接出 s
。
注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
输入: s = "leetcode", wordDict = ["leet", "code"]
输出: true
解释: 返回 true 因为 "leetcode" 可以由 "leet" 和 "code" 拼接成。
public boolean wordBreak(String s, List<String> wordDict) {
// dp[i] s[0..i] 能否拆分成 wordDict
boolean[] dp = new boolean[s.length() + 1];
dp[0] = true;
for (int i = 1; i <= s.length(); i++) // 背包
for (int j = 0; j < i; j++) // 物品
if (wordDict.contains(s.substring(j, i)) && dp[j])
dp[i] = true;
return dp[s.length()];
}
多重背包
有 N N N 种物品和一个容量是 V V V 的背包。
第 i i i 种物品最多有 s i s_i si 件,每件体积是 v i v_i vi,价值是 w i w_i wi。
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
多重背包在 LeetCode 上没有对应的题目,了解一下。
class Solution {
static int maxValue(int[] nums, int[] w, int[] v, int c) {
int[] dp = new int[c + 1];
for (int i = 0; i < w.length; i++) // 物品
for (int j = c; j >= w[i]; j--) {
// 背包容量
for (int k = 1; k <= nums[i] && k * w[i] <= j; k++) // 遍历个数
dp[j] = Math.max(dp[j], dp[j - k * w[i]] + k * v[i]);
System.out.println(Arrays.toString(dp));
}
return dp[c];
}
public static void main(String[] args) {
int[] nums = new int[] {
2, 3, 2 };
int[] w = new int[] {
1, 3, 4 }; // 重量
int[] v = new int[] {
15, 20, 30 }; // 价值
int c = 10; // 背包容量
System.out.println(maxValue(nums, w, v, c));
}
}
打家劫舍系列
打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
public int rob(int[] nums) {
int n = nums.length;
// dp[i] 偷第 i 号房屋时的最高金额
int[] dp = new int[n + 1];
dp[1] = nums[0];
for (int i = 2; i <= n; i++)
// 今天偷 或 不偷 两种选择
dp[i] = Math.max(dp[i - 2] + nums[i - 1], dp[i - 1]);
return dp[n];
}
打家劫舍 II
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。
输入:nums = [2,3,2]
输出:3
解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。
既然头和尾是同一个,把头和尾分别去掉,在 [0, n - 1]
和 [1, n]
偷,返回两个结果的最大值即可
class Solution {
public int rob(int[] nums) {
if (nums.length == 1) return nums[0];
return Math.max(
help(Arrays.copyOfRange(nums, 0, nums.length - 1)),
help(Arrays.copyOfRange(nums, 1, nums.length)));
}
public int help(int[] nums) {
int n = nums.length;
int[] dp = new int[n + 1];
dp[1] = nums[0];
for (int i = 2; i <= n; i++)
dp[i] = Math.max(dp[i - 2] + nums[i - 1], dp[i - 1]);
return dp[n];
}
}
打家劫舍 III TODO
股票系列
股票问题:
- 121.买卖股票的最佳时机):只能买卖一次
- 122.买卖股票的最佳时机 II:可以买卖多次
- 123.买卖股票的最佳时机 III:最多买卖两次
- 188.买卖股票的最佳时机 IV:最多买卖 k 次
-
- 最佳买卖股票时机含冷冻期:买卖多次,卖出有一天冷冻期
- 714.买卖股票的最佳时机含手续费:买卖多次,每次有手续费
买卖股票的最佳时机 - 买卖一次
给定一个数组 prices
,它的第 i
个元素 prices[i]
表示一支给定股票第 i
天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0
。
输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
DP:
public int maxProfit(int[] prices) {
// dp[i] 第 i 天卖出股票(当天就卖)能获取的最大利润
int[] dp = new int[prices.length];
// min 记录 i 之前天数的最大值
int min = prices[0], res = 0;
for (int i = 0; i < prices.length; i++) {
if (prices[i] < min) min = prices[i];
dp[i] = prices[i] - min; // 今天的利润
res = Math.max(res, dp[i]);
}
return res;
}
贪心:从左往右找,左边的最小值与右边的最大值的差值
不需要分别求出左边的最小值与右边的最大值再做差
只需要从左往右遍历,维护左边最小值,再维护当前遍历值与最小值的差即可
public int maxProfit(int[] prices) {
int min = Integer.MAX_VALUE, res = 0;
for (int i = 0; i < prices.length; i++) {
min = Math.min(min, prices[i]);
res = Math.max(res, prices[i] - min);
}
return res;
}
买卖股票的最佳时机 II - 买卖多次
给你一个整数数组 prices
,其中 prices[i]
表示某支股票第 i
天的价格。
在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。
返回 你能获得的 最大 利润 。
输入:prices = [7,1,5,3,6,4]
输出:7
解释:在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4 。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6 - 3 = 3 。
总利润为 4 + 3 = 7 。
DP:
public int maxProfit(int[] prices) {
int n = prices.length;
// buy[i] 在第 i 天买入股票(不一定当天买)的最大利润
int[] buy = new int[n];
buy[0] = -prices[0];
// sell[i] 在第 i 天卖出股票(不一定当卖卖)的最大利润
int[] sell = new int[n];
for (int i = 1; i < n; i++) {
// 选择: [当前不买(之前买)] 或 [当前买]
buy[i] = Math.max(buy[i - 1], sell[i - 1] - prices[i]);
// 选择: [当前不卖(之前卖)] 或 [当前卖]
sell[i] = Math.max(sell[i - 1], buy[i - 1] + prices[i]);
}
return sell[n - 1];
}
贪心:
public int maxProfit(int[] prices) {
int res = 0;
for (int i = 1; i < prices.length; i++)
if (prices[i] > prices[i - 1])
res += prices[i] - prices[i - 1];
return res;
}
买卖股票的最佳时机 III - 最多买卖两次
给定一个数组,它的第 i
个元素是一支给定的股票在第 i
天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
输入:prices = [3,3,5,0,0,3,1,4]
输出:6
解释:在第 4 天(股票价格 = 0)的时候买入,在第 6 天(股票价格 = 3)的时候卖出,这笔交易所能获得利润 = 3-0 = 3 。
随后,在第 7 天(股票价格 = 1)的时候买入,在第 8 天 (股票价格 = 4)的时候卖出,这笔交易所能获得利润 = 4-1 = 3 。
public int maxProfit(int[] prices) {
int n = prices.length;
// sell[i][j] 在第 i 天不持股(当天或之前卖出)的最大利润, 当前是第 j 笔交易
int[][] sell = new int[n][3];
// buy[i][j] 在第 i 天持股(当天或之前买入)的最大利润, 当前是第 j 笔交易
int[][] buy = new int[n][3];
for (int i = 0; i < 3; i++) buy[0][i] = -prices[0];
for (int i = 1; i < n; i++) {
for (int j = 1; j < 3; j++) {
// 卖的必须是当前轮买的
sell[i][j] = Math.max(sell[i - 1][j], buy[i - 1][j] + prices[i]);
// 买之前必须把上一轮卖掉
buy[i][j] = Math.max(buy[i - 1][j], sell[i- 1][j - 1] - prices[i]);
}
}
int max = 0;
for (int i = 0; i < 3; i++) max = Math.max(max, sell[n - 1][i]);
return max;
}
买卖股票的最佳时机 IV - 最多买卖 k 次
给定一个整数数组 prices
,它的第 i
个元素 prices[i]
是一支给定的股票在第 i
天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
输入:k = 2, prices = [2,4,1]
输出:2
解释:在第 1 天 (股票价格 = 2) 的时候买入,在第 2 天 (股票价格 = 4) 的时候卖出,这笔交易所能获得利润 = 4-2 = 2 。
public int maxProfit(int k, int[] prices) {
int n = prices.length;
// sell[i][j] 在第 i 天不持股(当天或之前卖出)的最大利润, 当前是第 j 笔交易
int[][] sell = new int[n][k + 1];
// buy[i][j] 在第 i 天持股(当天或之前买入)的最大利润, 当前是第 j 笔交易
int[][] buy = new int[n][k + 1];
for (int i = 0; i < k + 1; i++) buy[0][i] = -prices[0];
for (int i = 1; i < n; i++) {
for (int j = 1; j < k + 1; j++) {
// 卖的必须是当前轮买的
sell[i][j] = Math.max(sell[i - 1][j], buy[i - 1][j] + prices[i]);
// 买之前必须把上一轮卖掉
buy[i][j] = Math.max(buy[i - 1][j], sell[i - 1][j - 1] - prices[i]);
}
}
int max = 0;
for (int i = 0; i < k + 1; i++) max = Math.max(max, sell[n - 1][i]);
return max;
}
最佳买卖股票时机含冷冻期 - 买卖多次,卖出有一天冷冻期
给定一个整数数组prices
,其中第 prices[i]
表示第 i
天的股票价格 。
设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
- 卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
输入: prices = [1,2,3,0,2]
输出: 3
解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]
public int maxProfit(int[] prices) {
int n = prices.length;
// sell[i] 第 i 天卖出的最大收益
int[] sell = new int[n];
// buy[i] 第 i 天买入的最大收益
int[] buy = new int[n];
// cool[i] 第 i 天冷冻的最大收益
int[] cool = new int[n];
buy[0] = -prices[0];
for (int i = 1; i < n; i++) {
// 不买 或 买(只能在冷冻期过后买)
buy[i] = Math.max(buy[i - 1], cool[i - 1] - prices[i]);
// 不卖 或 卖
sell[i] = Math.max(sell[i - 1], buy[i - 1] + prices[i]);
// 冷冻期只会由昨天卖出得到
cool[i] = sell[i - 1];
}
return Math.max(sell[n - 1], cool[n - 1]);
}
买卖股票的最佳时机含手续费 - 买卖多次,每次有手续费
给定一个整数数组 prices
,其中 prices[i]
表示第 i
天的股票价格 ;整数 fee
代表了交易股票的手续费用。
你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。
返回获得利润的最大值。
注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。
输入:prices = [1, 3, 2, 8, 4, 9], fee = 2
输出:8
解释:能够达到的最大利润:
在此处买入 prices[0] = 1
在此处卖出 prices[3] = 8
在此处买入 prices[4] = 4
在此处卖出 prices[5] = 9
总利润: ((8 - 1) - 2) + ((9 - 4) - 2) = 8
public int maxProfit(int[] prices, int fee) {
int n = prices.length;
// buy[i] 第 i 天时买入股票的最大收益
int[] buy = new int[n];
// sell[i] 第 i 天时卖出股票的最大收益
int[] sell = new int[n];
buy[0] = -prices[0];
for (int i = 1; i < n; i++) {
// 当前不买(之前买) 或 当前买
buy[i] = Math.max(buy[i - 1], sell[i - 1] - prices[i]);
// 当前不卖(之前卖) 或 当前卖
sell[i] = Math.max(sell[i - 1], buy[i - 1] + prices[i] - fee);
}
return sell[n - 1];
}
子序列、子数组
子序列 是不连续的,保持元素相对顺序即可
子数组、子串 是连续的,元素之间不能多或少某些元素
最长递增子序列
给你一个整数数组 nums
,找到其中最长严格递增子序列的长度。
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
public int lengthOfLIS(int[] nums) {
// dp[i] 以 i 位置结尾的最长递增子序列的长度
int[] dp = new int[nums.length];
Arrays.fill(dp, 1); // 每个位置的上升子序列至少是 1
int res = -1;
for (int i = 0; i < nums.length; i++) {
for (int j = 0; j < i; j++)
if (nums[j] < nums[i])
dp[i] = Math.max(dp[i], dp[j] + 1);
res = Math.max(res, dp[i]);
}
return res;
}
最长连续递增序列
给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。
输入:nums = [1,3,5,4,7]
输出:3
解释:最长连续递增序列是 [1,3,5], 长度为3。
尽管 [1,3,5,7] 也是升序的子序列, 但它不是连续的,因为 5 和 7 在原数组里被 4 隔开。
DP:
public int findLengthOfLCIS(int[] nums) {
// dp[i] 以 i 位置结尾的最长连续递增序列的长度
int[] dp = new int[nums.length];
Arrays.fill(dp, 1); // 长度至少为 1
int res = 1;
for (int i = 0; i < nums.length - 1; i++) {
if (nums[i] < nums[i + 1])
dp[i + 1] = dp[i] + 1;
res = Math.max(res, dp[i + 1]);
}
return res;
贪心:
public int findLengthOfLCIS(int[] nums) {
int cnt = 1, res = 1;
for (int i = 0; i < nums.length - 1; i++) {
if (nums[i] < nums[i + 1]) cnt++;
else cnt = 1;
res = Math.max(res, cnt);
}
return res;
}
最长重复子数组
给两个整数数组 nums1
和 nums2
,返回 两个数组中 公共的 、长度最长的子数组的长度 。
输入:nums1 = [1,2,3,2,1], nums2 = [3,2,1,4,7]
输出:3
解释:长度最长的公共子数组是 [3,2,1] 。
public int findLength(int[] nums1, int[] nums2) {
int n = nums1.length, m = nums2.length;
// dp[i][j] - nums1[i] 和 nums2[j] 结尾的最长重复子数组的长度
int[][] dp = new int[n + 1][m + 1];
int res = 0;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++)
if (nums1[i - 1] == nums2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
res = Math.max(res, dp[i][j]);
}
}
return res;
}
最长公共子序列
给定两个字符串 text1
和 text2
,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0
。
public int longestCommonSubsequence(String s1, String s2) {
int n = s1.length(), m = s2.length();
char[] cs1 = s1.toCharArray(), cs2 = s2.toCharArray();
// dp[i][j] s1 中 [0..i] 和 s2 中 [0..j] 的公共子序列长度
int[][] dp = new int[n + 1][m + 1];
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (cs1[i - 1] == cs2[j - 1])
dp[i][j] = dp[i - 1][j - 1] + 1;
else
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
return dp[n][m];
}
不相交的线
在两条独立的水平线上按给定的顺序写下 nums1
和 nums2
中的整数。
现在,可以绘制一些连接两个数字 nums1[i]
和 nums2[j]
的直线,这些直线需要同时满足满足:
nums1[i] == nums2[j]
- 且绘制的直线不与任何其他连线(非水平线)相交。
请注意,连线即使在端点也不能相交:每个数字只能属于一条连线。
以这种方法绘制线条,并返回可以绘制的最大连线数。
输入:nums1 = [1,4,2], nums2 = [1,2,4]
输出:2
解释:可以画出两条不交叉的线,如上图所示。
但无法画出第三条不相交的直线,因为从 nums1[1]=4 到 nums2[2]=4 的直线将与从 nums1[2]=2 到 nums2[1]=2 的直线相交。
分析题意,发现这题就是在求两个数组的最长公共子序列:
var maxUncrossedLines = function (nums1, nums2) {
const [n, m] = [nums1.length, nums2.length];
let dp = new Array(n + 1).fill(0).map((x) => new Array(m + 1).fill(0));
for (let i = 1; i <= n; i++)
for (let j = 1; j <= m; j++)
if (nums1[i - 1] === nums2[j - 1])
dp[i][j] = dp[i - 1][j - 1] + 1;
else
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
return dp[n][m];
};
最大子数组和
给你一个整数数组 nums
,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
public int maxSubArray(int[] nums) {
// dp[i] 以 i 位置结尾的元素的连续子数组的最大和
int[] dp = new int[nums.length];
int res = dp[0] = nums[0];
for (int i = 1; i < nums.length; i++) {
dp[i] = Math.max(dp[i - 1] + nums[i], nums[i])
res = Math.max(res, dp[i]);
}
return res;
}
编辑距离
判断子序列
给定字符串 s 和 t ,判断 s 是否为 t 的子序列。
输入:s = "abc", t = "ahbgdc"
输出:true
双指针:
public boolean isSubsequence(String s, String t) {
if (s.length() == 0) return true;
char [] cs = s.toCharArray(), ct = t.toCharArray();
int j = 0;
for (int i = 0; i < ct.length; i++) {
if (cs[j] == ct[i]) j++;
if (j == cs.length) return true;
}
return false;
}
DP:
public boolean isSubsequence(String s, String t) {
int l1 = s.length(), l2 = t.length();
// dp[i][j] s 中 [0..i] 的串和 t 中 [0..j] 的串的公共子序列的长度
int[][] dp = new int[l1 + 1][l2 + 1];
for (int i = 1; i <= l1; i++)
for (int j = 1; j <= l2; j++)
if (s.charAt(i - 1) == t.charAt(j - 1))
dp[i][j] = dp[i - 1][j - 1] + 1;
else
dp[i][j] = dp[i][j - 1];
return dp[l1][l2] == s.length();
}
不同的子序列
给定一个字符串 s
和一个字符串 t
,计算在 s
的子序列中 t
出现的个数。
dp[i][j]: s[0..i] 和 t[0..j] 的子序列的个数
s[i] == t[j] 的情况:
- 对于 s = "rara", t = "ra", i = 3, j = 1
1. s 用最后一位 'a', 此时相当于求 "rar" 和 "r" 的子序列个数, 即 dp[i - 1][j - 1]
2. s 不用最后一位 'a', 此时相当于求 "rar" 和 "ra" 的子序列个数, 即 dp[i - 1][j]
s[i] != t[j] 的情况:
- 对于 s = "rarb", t = "ra", i = 3, j = 1
1. s 只能不用最后一位 'b', 此时相当于求 "rar" 和 "ra" 的子序列个数, 即 dp[i - 1][j]
public int numDistinct(String s, String t) {
int n = s.length(), m = t.length();
char[] cs = s.toCharArray(), ct = t.toCharArray();
// dp[i][j] s[0..i] 和 t[0..j] 的子序列的个数
int[][] dp = new int[n + 1][m + 1];
for (int i = 0; i <= n; i++) dp[i][0] = 1;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (j > i) continue;
if (cs[i - 1] == ct[j - 1])
dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
else
dp[i][j] = dp[i - 1][j];
}
}
return dp[n][m];
}
两个字符串的删除操作
给定两个单词 word1
和 word2
,返回使得 word1
和 word2
相同所需的最小步数。
每步 可以删除任意一个字符串中的一个字符。
输入: word1 = "sea", word2 = "eat"
输出: 2
解释: 第一步将 "sea" 变为 "ea" ,第二步将 "eat "变为 "ea"
DP 1:(编辑距离思路)
public int minDistance(String s1, String s2) {
char[] cs1 = s1.toCharArray(), cs2 = s2.toCharArray();
int n = s1.length(), m = s2.length();
// dp[i][j] 删除字符使得 s1 [0..i] 和 s2 [0..j] 相同的最小步数
int[][] dp = new int[n + 1][m + 1];
for (int i = 0; i <= n; i++) dp[i][0] = i;
for (int i = 0; i <= m; i++) dp[0][i] = i;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
if (cs1[i - 1] == cs2[j - 1])
dp[i][j] = dp[i - 1][j - 1];
else
dp[i][j] = Math.min(dp[i - 1][j - 1] + 2,
Math.min(dp[i - 1][j], dp[i][j - 1]) + 1);
return dp[n][m];
}
DP2:求出两个字符串的最长公共子序列长度,除此之外的字符都需要删除
public int minDistance(String s1, String s2) {
int n = s1.length(), m = s2.length();
char[] cs1 = s1.toCharArray(), cs2 = s2.toCharArray();
// dp[i][j] s1 [0..i] 和 s2 [0..j] 的最长公共子序列的长度
int[][] dp = new int[n + 1][m + 1];
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
if (cs1[i - 1] == cs2[j - 1])
dp[i][j] = dp[i - 1][j - 1] + 1;
else
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
return m + n - dp[n][m] * 2;
}
编辑距离
给你两个单词 word1
和 word2
, 请返回将 word1
转换成 word2
所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
- 插入一个字符
- 删除一个字符
- 替换一个字符
输入:word1 = "horse", word2 = "ros"
输出:3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')
public int minDistance(String s1, String s2) {
int n = s1.length(), m = s2.length();
char[] cs1 = s1.toCharArray(), cs2 = s2.toCharArray();
// dp[i][j] 将 s1[0..n] 变成 s2[0..m] 需要的最少操作数
int[][] dp = new int[n + 1][m + 1];
for (int i = 0; i <= n; i++) dp[i][0] = i;
for (int i = 0; i <= m; i++) dp[0][i] = i;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
// 不需要进行操作
if (cs1[i - 1] == cs2[j - 1])
dp[i][j] = dp[i - 1][j - 1];
// 需要进行操作: 取 添加, 删除, 修改 的最小值
else
dp[i][j] = Math.min(
Math.min(dp[i - 1][j], dp[i][j - 1]), dp[i - 1][j - 1]
) + 1;
return dp[n][m];
}
回文子串
回文子串
给你一个字符串 s
,请你统计并返回这个字符串中 回文子串 的数目。
输入:s = "aaa"
输出:6
解释:6个回文子串: "a", "a", "a", "aa", "aa", "aaa"
二维 DP:
public int countSubstrings(String s) {
char[] cs = s.toCharArray();
// dp[i][j] [i..j] 的子串是否是回文串
boolean[][] dp = new boolean[s.length()][s.length()];
int res = 0;
for (int i = cs.length - 1; i >= 0; i--)
for (int j = i; j < cs.length; j++)
// 由状态转移方程可知, 每一个位置的更新取决于左下角位置的状态值
// 所以外层循环从下往上, 内层从左往右
if (cs[i] == cs[j] && (j - i < 2 || dp[i + 1][j - 1])) {
dp[i][j] = true;
res++;
}
return res;
}
中心扩散:
class Solution {
int res = 0;
public int countSubstrings(String s) {
char[] cs = s.toCharArray();
for (int i = 0; i < cs.length; i++) {
count(cs, i, i); // 回文串长度为奇数
count(cs, i, i + 1); // 回文串长度为偶数
}
return res;
}
void count(char[] cs, int l, int r) {
while (l >= 0 && r < cs.length && cs[l] == cs[r]) {
res++;
l--;
r++;
}
}
}
一维 DP:(暴力双指针)
class Solution {
public int countSubstrings(String s) {
char[] cs = s.toCharArray();
// dp[i] 以 s[i] 结尾的回文子串的数目
int[] dp = new int[cs.length];
for (int i = 0; i < cs.length; i++)
for (int j = 0; j <= i; j++)
if (isPalindrome(cs, j, i))
dp[i]++;
return Arrays.stream(dp).sum();
}
// 是否是回文串
boolean isPalindrome(char[] cs, int l, int r) {
while (l < r)
if (cs[l++] != cs[r--])
return false;
return true;
}
}
最长回文子串
给你一个字符串 s
,找到 s
中最长的回文子串。
输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。
中心扩散:
class Solution {
String res = "";
public String longestPalindrome(String s) {
for (int i = 0; i < s.length(); i++) {
helper(s, i, i); // 处理奇数长度的回文串
helper(s, i, i + 1); // 处理偶数长度的回文串
}
return res;
}
void helper(String s, int l, int r) {
while (l >= 0 && r < s.length() && s.charAt(l) == s.charAt(r)) {
String t = s.substring(l, r + 1);
if (t.length() > res.length()) res = t;
l--;
r++;
}
}
}
动态规划:
public String longestPalindrome(String s) {
// dp[i][j] s[i..j] 的子串是否是回文子串
boolean[][] dp = new boolean[s.length()][s.length()];
int l = 0, r = 0; // 记录结果的截取位置
for (int i = s.length() - 1; i >= 0; i--)
for (int j = i; j < s.length(); j++)
// 由状态转移方程可知, 每一个位置的更新取决于左下角位置的状态值
// 所以外层循环从下往上, 内层从左往右
if (s.charAt(i) == s.charAt(j) && (j - i < 2 || dp[i + 1][j - 1])) {
dp[i][j] = true;
if (r - l < j - i) {
l = i;
r = j;
}
}
return s.substring(l, r + 1);
}
最长回文子序列
给你一个字符串 s
,找出其中最长的回文子序列,并返回该序列的长度。
输入:s = "bbbab"
输出:4
解释:一个可能的最长回文子序列为 "bbbb" 。
public int longestPalindromeSubseq(String s) {
int n = s.length();
char[] cs = s.toCharArray();
// dp[i][j] s[i..j] 中的最长回文子序列的长度
int[][] dp = new int[n][n];
for (int i = 0; i < n; i++) dp[i][i] = 1;
for (int i = n - 1; i >= 0; i--) {
for (int j = i + 1; j < n; j++) {
if (cs[i] == cs[j])
dp[i][j] = dp[i + 1][j - 1] + 2;
else
dp[i][j] = Math.max(dp[i][j - 1], dp[i + 1][j]);
}
}
return dp[0][n - 1];
}