笔记摘自代码随想录
一、概念
动态规划,英文:Dynamic Programming
,简称DP,如果某一问题有很多重叠子问题
,使用动态规划是最有效的。
区别:
- DP:每一个状态一定是由
上一个状态
推导出来的 - 贪心:局部最优解,没有推导
二、模板
解题步骤:
- 确定dp数组、下标含义
- 递推公式
- 初始化
- 遍历顺序
- 举例推导
遇到无法AC,请灵魂三问:
- 这道题目我==举例==推导状态转移公式了么?
- 我 ==打印== dp数组的日志了么?
- 打印出来了dp数组和我想的一样么?
背包总结:
三、例题
爬楼梯
题:509. 斐波那契数
斐波那契数
(通常用 F(n)
表示)形成的序列称为 斐波那契数列
。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:
F(0) = 0,F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1
给定 n
,请计算 F(n)
。
示例 1:
输入:n = 2
输出:1
解释:F(2) = F(1) + F(0) = 1 + 0 = 1
示例 2:
输入:n = 3
输出:2
解释:F(3) = F(2) + F(1) = 1 + 1 = 2
示例 3:
输入:n = 4
输出:3
解释:F(4) = F(3) + F(2) = 2 + 1 = 3
提示:
0 <= n <= 30
解:
解题思路:
- dp[i]代表第i个斐波那契数的值
- dp[i] = dp[i-1] + dp[i-2]
- dp[0] = 0, dp[1] = 1;
- 从前往后
- 举例:0,1,1,2,3, 测试正确
AC代码:
class Solution {
public int fib(int n) {
int[] dp = new int[31];
dp[0] = 0; dp[1] = 1;
for(int i = 2; i <= n; ++ i) {
dp[i] = dp[i-1] + dp[i-2];
}
return dp[n];
}
}
解题思路:
当前状态只有前两个状态有关,所以我们可以滚动数组
AC代码:
class Solution {
public int fib(int n) {
if(n < 1) return 0;
int[] dp = new int[2];
dp[0] = 0; dp[1] = 1;
for(int i = 2; i <= n; ++ i) {
int sum = dp[0] + dp[1];
dp[0] = dp[1];
dp[1] = sum;
}
return dp[1];
}
}
解题思路:递归
AC代码:
class Solution {
// 1.int返回值,参数n
// 2.终止条件 N < 2
public int fib(int n) {
if(n < 2) return n;
return fib(n-1) + fib(n-2);
}
}
题:70. 爬楼梯
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
示例 1:
输入:n = 2
输出:2
解释:有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶
示例 2:
输入:n = 3
输出:3
解释:有三种方法可以爬到楼顶。
1. 1 阶 + 1 阶 + 1 阶
2. 1 阶 + 2 阶
3. 2 阶 + 1 阶
提示:
1 <= n <= 45
解:
解题思路:
- dp[i]代表爬到i阶的方法数
- dp[i] = dp[i-1] + dp[i-2];
- dp[1]=1,dp[2] = 2;
- 正向遍历
- 举例:1,2,3,5
AC代码:
class Solution {
public int climbStairs(int n) {
int[] dp = new int[46];
dp[1] = 1; dp[2] = 2;
for(int i = 3; i <= n; ++ i) {
dp[i] = dp[i-1] + dp[i-2];
}
return dp[n];
}
}
解题思路:滚动数组
AC代码:
class Solution {
public int climbStairs(int n) {
if(n < 3) return n;
int[] dp = new int[3];
dp[1] = 1; dp[2] = 2;
for(int i = 3; i <= n; ++ i) {
int sum = dp[1] + dp[2];
dp[1] = dp[2];
dp[2] = sum;
}
return dp[2];
}
}
题:746. 使用最小花费爬楼梯
给你一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。
你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。
请你计算并返回达到楼梯顶部的最低花费。
示例 1:
输入:cost = [10,15,20]
输出:15
解释:你将从下标为 1 的台阶开始。
- 支付 15 ,向上爬两个台阶,到达楼梯顶部。
总花费为 15 。
示例 2:
输入:cost = [1,100,1,1,1,100,1,1,100,1]
输出:6
解释:你将从下标为 0 的台阶开始。
- 支付 1 ,向上爬两个台阶,到达下标为 2 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 4 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 6 的台阶。
- 支付 1 ,向上爬一个台阶,到达下标为 7 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 9 的台阶。
- 支付 1 ,向上爬一个台阶,到达楼梯顶部。
总花费为 6 。
提示:
2 <= cost.length <= 1000
0 <= cost[i] <= 999
解:
解题思路:
- dp[i] = 爬第i个台阶最低费用
- dp[i] = min(dp[i-1],dp[i-2]) + cost[i];
- dp[0] = cost[0]; dp[1] = cost[1];
- 正遍历
- 10 15
AC代码:
class Solution {
public int minCostClimbingStairs(int[] cost) {
int N = cost.length;
int[] dp = new int[N];
dp[0] = cost[0]; dp[1] = cost[1];
for(int i = 2; i < N; ++ i) {
dp[i] = Math.min(dp[i-1], dp[i-2]) + cost[i];
}
// 最后一步的费用可能来自上一步,上两步
return Math.min(dp[N-1], dp[N-2]);
}
}
路径
题:62. 不同路径
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
示例 1:
输入:m = 3, n = 7
输出:28
示例 2:
输入:m = 3, n = 2
输出:3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。
1. 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右
3. 向下 -> 向右 -> 向下
示例 3:
输入:m = 7, n = 3
输出:28
示例 4:
输入:m = 3, n = 3
输出:6
提示:
1 <= m, n <= 100
题目数据保证答案小于等于 2 * 109
解:
解题思路:DFS
AC代码:
class Solution {
// 图论DFS(超时)
// 1.参数:x, y
// 2.终止:越界、终点
// 3.需要返回值 int
int M; int N;
public int uniquePaths(int m, int n) {
this.M = m; this.N = n;
return dfs(1, 1);
}
int dfs(int x, int y) {
if(x > M || y > N) return 0; // 越界
if(x == M && y == N) return 1; // 终点
return dfs(x + 1, y) + dfs(x, y + 1);
}
}
解题思路:DP
- dpi 代表(i,j)有多少条路径
- dpi = dpi-1 + dpi;
- 第一行、第一列均为1,要么一直向下,要么一直往右,别无它法
- 正向遍历
- 演算示例,检查
AC代码:
class Solution {
public int uniquePaths(int m, int n) {
int[][] dp = new int[m][n];
for(int i = 0; i < m; ++ i) dp[i][0] = 1;
for(int j = 0; j < n; ++ j) dp[0][j] = 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];
}
}
解题思路:滚动数组
AC代码:
class Solution {
public int uniquePaths(int m, int n) {
int[] dp = new int[n];
Arrays.fill(dp, 1);
for(int i = 1; i < m; ++ i) {
for(int j = 1; j < n; ++ j) {
dp[j] += dp[j-1];
}
}
return dp[n-1];
}
}
题:63. 不同路径 II
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish”)。
现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
网格中的障碍物和空位置分别用 1 和 0 来表示。
示例 1:
输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]
输出:2
解释:3x3 网格的正中间有一个障碍物。
从左上角到右下角一共有 2 条不同的路径:
1. 向右 -> 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右 -> 向右
示例 2:
输入:obstacleGrid = [[0,1],[0,0]]
输出:1
提示:
m == obstacleGrid.length
n == obstacleGrid[i].length
1 <= m, n <= 100
obstacleGrid[i][j] 为 0 或 1
解:
解题思路:
- dpi 代表(i,j)有多少条路径
- dpi = dpi-1 + dpi;
- 第一行、第一列均为1,要么一直向下,要么一直往右,别无它法
- 正向遍历
- 演算示例,检查
AC代码:
class Solution {
// 1.dp[i][j] 代表(i,j)有多少条路径
// 2.dp[i][j] = dp[i-1][j] + dp[i][j-1];
// 3.第一行、第一列均为1,如果遇到障碍物后变为0[只能左下、右下走]
// 4.正向遍历
public int uniquePathsWithObstacles(int[][] obstacleGrid) {
int m = obstacleGrid.length;
int n = obstacleGrid[0].length;
int[][] dp = new int[m][n];
// 初始化
for(int i = 0; i < m && obstacleGrid[i][0] == 0; ++ i) dp[i][0] = 1;
for(int j = 0; j < n && obstacleGrid[0][j] == 0; ++ j) dp[0][j] = 1;
for(int i = 1; i < m; ++ i) {
for(int j = 1; j < n; ++ j) {
if(obstacleGrid[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
),并使这些整数的乘积最大化。
返回 你可以获得的最大乘积 。
示例 1:
输入: n = 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1。
示例 2:
输入: n = 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。
提示:
2 <= n <= 58
解:
解题思路:DP
- dp[i] 第i个最大乘积整数
- 初始化dp[2] = 1;
- j为切点
- dp[i] = j dp[i-j]; dp[i] = j (i-j); dp[i] = dp[i];
- 正向遍历
AC代码:
class Solution {
public int integerBreak(int n) {
int[] dp = new int[n+1];
dp[2] = 1;
for(int i = 3; i <= n; ++ i) {
for(int j = 1; j < i - 1; ++ j) {
dp[i] = Math.max(dp[i], Math.max(j * (i-j), j * dp[i-j]));
}
}
return dp[n];
}
}
背包问题
01背包
题:416. 分割等和子集
给你一个 只包含正整数
的 非空
数组 nums
。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
示例 1:
输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。
示例 2:
输入:nums = [1,2,3,5]
输出:false
解释:数组不能分割成两个元素和相等的子集。
提示:
1 <= nums.length <= 200
1 <= nums[i] <= 100
解:
解题思路:
使用DP
- 元素和 = sum/2(背包的体积)
- 每个元素只能用一次 => 01背包
- 背包的重量:元素值,背包的价值:元素值
dp分析
- dp[j]:容量为j的背包最大价值为dp[j]
- dp[j] = max(dp[j], dp[j-nums[i]] + nums[i])
- dp[0] = 0
- 先物品正向 后背包倒序(防止重复物品使用)
- 推导 sum/2 = 11
dp[5] = {0, 1, 1, 1, 1, 5, 6, 6, 6, 6, 10, 11}
AC代码:
class Solution {
// 使用DP
// 0.元素和 = sum/2(背包的体积)
// 1.每个元素只能用一次 => 01背包
// 2.背包的重量:元素值,背包的价值:元素值
// dp分析
// 1.dp[j]:容量为j的背包最大价值为dp[j]
// 2.dp[j] = max(dp[j], dp[j-nums[i]] + nums[i])
// 3.dp[0] = 0
// 4.先物品正向 后背包倒序(防止重复物品使用)
// 5.推导 sum/2 = 11
// dp[5] = {0, 1, 1, 1, 1, 5, 6, 6, 6, 6, 10, 11}
public boolean canPartition(int[] nums) {
int sum = 0; int len = nums.length;
for(int i = 0; i < len; ++ i) {
sum += nums[i];
}
if((sum & 1) != 0) return false; // 总和为奇数,不能平分
int tar = sum / 2;
int[] dp = new int[tar + 1];
for(int i = 0; i < len; ++ i) { // 物品
for(int j = tar; j >= nums[i]; --j) { // 背包
dp[j] = Math.max(dp[j], dp[j-nums[i]] + nums[i]);
}
}
return dp[tar] == tar;
}
}
题:1049. 最后一块石头的重量 II
有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。
每一回合,从中选出任意两块石头
,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:
- 如果 x == y,那么两块石头都会被完全粉碎;
- 如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。
最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0
。
示例 1:
输入: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],这就是最优值。
示例 2:
输入:stones = [31,26,33,21,40]
输出:5
提示:
1 <= stones.length <= 30
1 <= stones[i] <= 100
解:
解题思路:
- 背包容量:不超过 sum/2
- 重量 = 价值 = stones[i]
- dp[j] = 容量为j的背包最多可以背dp[j]重的石头
- dp[j] = max(dp[j], dp[j-stones[i]]+stones[i])
- dp[0] = 0
- 外层物品 正向 内层背包 倒序
[2, 4, 1, 1] tar = 4
模拟dp[5] = {0, 0, 2, 2, 4}
AC代码:
class Solution {
// 0.背包容量:不超过 sum/2
// 1.重量 = 价值 = stones[i]
// 2.dp[j] = 容量为j的背包最多可以背dp[j]重的石头
// 3.dp[j] = max(dp[j], dp[j-stones[i]]+stones[i])
// 4.dp[0] = 0
// 5.外层物品 正向 内层背包 倒序
// [2, 4, 1, 1] tar = 4
// 模拟dp[5] = {0, 0, 2, 2, 4}
public int lastStoneWeightII(int[] stones) {
int sum = 0; int len = stones.length;
for(int i = 0; i < len; ++ i) sum += stones[i];
int tar = sum >> 1;
int[] dp = new int[tar + 1];
for(int i = 0; i < len; ++ i) { // 物品
for(int j = tar; j >= stones[i]; -- j) {
dp[j] = Math.max(dp[j], dp[j-stones[i]] + stones[i]);
}
}
return (sum - dp[tar]) - dp[tar]; // 石头相撞
}
}
题: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
解:
解题思路:
推导:
转化01背包=>分两堆 :加法的总和x
减法的总和 sum-x
最后求得结果 tar = x - (sum - x)
;x = (tar + sum) / 2
问题转化 :
装满容量x背包,几种方法
特殊情况考虑:
- 无解问题(abs(S) > sum)
- 下取整问题((S + sum) 为奇数)
dp五部曲
- dp[j]:填满j容量的包有dp[j]种方法
- dp[j] += dp[j - nums[i]]
- dp[0] = 1 填满背包0只有一种
- 外物品正序,内背包倒序
- 推导dp
AC代码:
class Solution {
public int findTargetSumWays(int[] nums, int tar) {
int sum = 0;
for(int i = 0; i < nums.length; ++ i) sum += nums[i];
if(Math.abs(tar) > sum) return 0;
if((tar + sum & 1) == 1) return 0;
int bagSize = tar + sum >> 1;
int[] dp = new int[bagSize + 1];
dp[0] = 1;
for(int i = 0; i < nums.length; ++ i) {
for(int j = bagSize; j >= nums[i]; -- j) {
dp[j] += dp[j - nums[i]];
}
}
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
解:
解题思路:
- 01背包,两个维度,背包容量m, n
- dpi:最多有i个0,j个1的最大子集的大小为dpi
- dpi = max(dpi, dpi-zeroNum + 1)
- 0
- 外物品正 内背包倒
AC代码:
class Solution {
// 01背包,两个维度,背包容量m, n
// dp[i][j]:最多有i个0,j个1的最大子集的大小为dp[i][j]
// dp[i][j] = max(dp[i][j], dp[i-zeroNum][j-oneNum] + 1)
// 0
// 外物品正 内背包倒
public int findMaxForm(String[] strs, int m, int n) {
int[][] dp = new int[m + 1][n + 1];
for(String str : strs) { // 遍历物品
// 统计01个数
int zeroNum = 0; int oneNum = 0;
for(char c : str.toCharArray()) {
if(c == '0') ++ zeroNum;
else ++ oneNum;
}
for(int i = m; i >= zeroNum; -- i) { // 遍历背包
for(int j = n; j >= oneNum; -- j) {
dp[i][j] = Math.max(dp[i][j], dp[i-zeroNum][j-oneNum] + 1);
}
}
}
return dp[m][n];
}
}
完全背包
题: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
解:
解题思路:
- 无限个 => 完全背包 组合数 => 与顺序无关
- 背包容量:amount
- dp[j]:容量为j的背包有dp[j]种方法
- dp[j] += dp[j-coins[i]];
- dp[0] = 1;
- 外物品正 内背包正
AC代码:
class Solution {
// 无限个 => 完全背包 组合数 => 与顺序无关
// 背包容量:amount
// dp[j]:容量为j的背包有dp[j]种方法
// dp[j] += dp[j-coins[i]];
// dp[0] = 1;
// 外物品正 内背包正
public int change(int amount, int[] coins) {
int[] dp = new int[amount + 1];
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];
}
}
题:377. 组合总和 Ⅳ
给你一个由 不同
整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数。
题目数据保证答案符合 32 位整数范围。
示例 1:
输入: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)
请注意,顺序不同的序列被视作不同的组合。
示例 2:
输入:nums = [9], target = 3
输出:0
提示:
1 <= nums.length <= 200
1 <= nums[i] <= 1000
nums 中的所有元素 互不相同
1 <= target <= 1000
- 进阶:如果给定的数组中含有负数会发生什么?问题会产生何种变化?如果允许负数出现,需要向题目中添加哪些限制条件?
解:
解题思路:
- 完全背包、背包容量 tar
- dp[j]: 容量为i背包有dp[j]种组合方法
- dp[j] += dp[j - nums[i]];
- dp[0] = 1;
- {1,3} != {3,1} 每个物品要在每个背包都试一次
- 外背包正 内物品正
- dp模拟:
以nums = [1, 2, 3]; tar = 4;为例:
1 0 0 0 0 // 背包容量为0
1 1 0 0 0 // 背包容量为1
1 1 2 0 0 // 背包容量为2
1 1 2 4 0 // 背包容量为3
1 1 2 4 7 // 背包容量为4
AC代码:
class Solution {
// 完全背包、背包容量 tar
// dp[j]: 容量为i背包有dp[j]种组合方法
// dp[j] += dp[j - nums[i]];
// dp[0] = 1;
// {1,3} != {3,1} 每个物品要在每个背包都试一次
// 外背包正 内物品正
public int combinationSum4(int[] nums, int tar) {
int[] dp = new int[tar + 1];
dp[0] = 1;
for(int j = 0; j <= tar; ++ j) { // 遍历背包
for(int i = 0; i < nums.length; ++ i) {
if(j - nums[i] >= 0 && dp[j] < Integer.MAX_VALUE - dp[j - nums[i]]) { // 防止累加溢出
dp[j] += dp[j - nums[i]];
}
}
}
return dp[tar];
}
}
题:70. 爬楼梯 [ 进阶版 ]
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
思考:一步一个台阶,两个台阶,三个台阶,.......,直到 m个台阶。问有多少种不同的方法可以爬到楼顶呢?
解:
解题思路:
- 完全背包:容量:n
- dp[i]:爬到i阶有dp[i]种方法
- dp[i] += dp[i-j];
- dp[0] = 1;
- {1,2} != {2,1} 外背包正 内物品正
AC代码:
class Solution {
public int climbStairs(int n) {
int[] dp = new int[n + 1];
int[] weight = {1, 2}; // 假设1,2步
dp[0] = 1;
for(int i = 0; i <= n; ++ i) {
for(int j = 0; j < weight.length; ++ j){
if(i >= weight[j]) dp[i] += dp[i - weight[j]];
}
}
return dp[n];
}
}
题:322. 零钱兑换
给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。
你可以认为每种硬币的数量是无限的。
示例 1:
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1
示例 2:
输入:coins = [2], amount = 3
输出:-1
示例 3:
输入:coins = [1], amount = 0
输出:0
提示:
1 <= coins.length <= 12
1 <= coins[i] <= 231 - 1
0 <= amount <= 104
解:
解题思路:
- 硬币数量无限 => 完全背包:容量=amount;
- dp[j]:金额i所需最少硬币个数为dp[j];
- dp[j] = mins(dp[j - coins[i]] + 1, dp[j])(思考来源)
- dp[0] = 0; 其余dp赋值为Int_Max
- {1,2} = {2,1} 外物品正,内背包正
AC代码:
class Solution {
// 硬币数量无限 => 完全背包:容量=amount;
// dp[j]:金额i所需最少硬币个数为dp[j];
// dp[j] = mins(dp[j - coins[i]] + 1, dp[j])(思考来源)
// dp[0] = 0; 其余dp赋值为Int_Max
// {1,2} = {2,1} 外物品正,内背包正
public int coinChange(int[] coins, int amount) {
int[] dp = new int[amount + 1];
Arrays.fill(dp, 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 - coins[i]] + 1, dp[j]);
}
}
}
// 如果dp[amount] == Integer.MAX_VALUE 说明未找到最少硬币数
return dp[amount] == Integer.MAX_VALUE ? -1 : dp[amount];
}
}
题:279. 完全平方数
给你一个整数 n ,返回 和为 n 的完全平方数的最少数量 。
完全平方数
是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
示例 1:
输入:n = 12
输出:3
解释:12 = 4 + 4 + 4
示例 2:
输入:n = 13
输出:2
解释:13 = 4 + 9
提示:
1 <= n <= 104
解:
解题思路:先背包再物品
AC代码:
class Solution {
// 完全平方数可多次使用 => 完全背包:背包容量 n
// dp[j]:和为j完全平方数最少数量:dp[j]
// 有比较问题【放与不放】
// dp[j] = min(dp[j-i*i] + 1, dp[j]);
// dp[0] = 0; 其余dp[i] = INT_MAX
// 多次使用 都正向遍历
// 先物品先背包? 都可以
public int numSquares(int n) {
int[] dp = new int[n + 1];
Arrays.fill(dp, Integer.MAX_VALUE);
dp[0] = 0;
// 先背包再物品
for(int i = 0; i <= n; ++ i) {
for(int j = 1; j * j <= i; ++ j) { // 物品取值范围[1, sqrt(i)]
dp[i] = Math.min(dp[i - j * j] + 1, dp[i]);
}
}
return dp[n];
}
}
解题思路:先物品再背包
AC代码:
class Solution {
// 完全平方数可多次使用 => 完全背包:背包容量 n
// dp[j]:和为j完全平方数最少数量:dp[j]
// 有比较问题【放与不放】
// dp[j] = min(dp[j-i*i] + 1, dp[j]);
// dp[0] = 0; 其余dp[i] = INT_MAX
// 多次使用 都正向遍历
// 先物品先背包? 都可以
public int numSquares(int n) {
int[] dp = new int[n + 1];
Arrays.fill(dp, Integer.MAX_VALUE);
dp[0] = 0;
// 先物品再背包
for(int i = 1; i * i <= n; ++ i) {
for(int j = 0; j <= n; ++ j) {
if(i * i <= j) {
dp[j] = Math.min(dp[j - i * i] + 1, dp[j]);
}
}
}
return dp[n];
}
}
题:139. 单词拆分
给你一个字符串 s 和一个字符串列表 wordDict 作为字典。请你判断是否可以利用字典中出现的单词拼接出 s 。
注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
示例 1:
输入: s = "leetcode", wordDict = ["leet", "code"]
输出: true
解释: 返回 true 因为 "leetcode" 可以由 "leet" 和 "code" 拼接成。
示例 2:
输入: s = "applepenapple", wordDict = ["apple", "pen"]
输出: true
解释: 返回 true 因为 "applepenapple" 可以由 "apple" "pen" "apple" 拼接成。
注意,你可以重复使用字典中的单词。
示例 3:
输入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
输出: false
提示:
1 <= s.length <= 300
1 <= wordDict.length <= 1000
1 <= wordDict[i].length <= 20
s 和 wordDict[i] 仅有小写英文字母组成
wordDict 中的所有字符串 互不相同
解:
解题思路:
单词重复使用 => 多重背包:容量-s.length;
- dp[i]:长度为i的字符串可以利用字典拼接出这一事件是dp[i];
- 来源:[0,i]字符串 = [0,j] + [j,i]
- if([j,i]子串在单词里面出现 && dp[j] == true) dp[i] = true;
- dp[0] = true;
- 外背包正 内物品正
AC代码:
class Solution {
// 单词重复使用 => 多重背包:容量-s.length;
// 1.dp[i]:长度为i的字符串可以利用字典拼接出这一事件是dp[i];
// 来源:[0,i]字符串 = [0,j] + [j,i]
// 2.if([j,i]子串在单词里面出现 && dp[j] == true) dp[i] = true;
// 3.dp[0] = true;
// 4.外背包正 内物品正
public boolean wordBreak(String s, List<String> wordDict) {
boolean[] dp = new boolean[s.length() + 1];
dp[0] = true;
for(int i = 1; i < dp.length; ++ i) { // 背包
for(int j = 0; j < i; ++ j) { // 物品
String word = s.substring(j, i);
if(wordDict.contains(word) && dp[j]) {
dp[i] = true;
}
}
}
return dp[s.length()];
}
}
解题思路:记忆化递归
AC代码:
打家劫舍
题:198. 打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例 1:
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
示例 2:
输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
偷窃到的最高金额 = 2 + 9 + 1 = 12 。
提示:
1 <= nums.length <= 100
0 <= nums[i] <= 400
解:
解题思路:
- dp[i]:下标i内的房屋,最后可以偷窃的金额dp[i]
- dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
- dp[0] = nums[0], dp[1] = max(dp[0], dp[1])
- 正向遍历
AC代码:
class Solution {
// dp[i]:下标i内的房屋,最后可以偷窃的金额dp[i]
// dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
// dp[0] = nums[0], dp[1] = max(dp[0], dp[1])
// 正向遍历
public int rob(int[] nums) {
int len = nums.length;
if(len == 1) return nums[0];
int[] dp = new int[len];
dp[0] = nums[0]; dp[1] = Math.max(nums[0], nums[1]);
for(int i = 2; i < len; ++ i) {
dp[i] = Math.max(dp[i - 2] + nums[i], dp[i - 1]);
}
return dp[len - 1];
}
}
题:213. 打家劫舍 II
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。
示例 1:
输入:nums = [2,3,2]
输出:3
解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。
示例 2:
输入:nums = [1,2,3,1]
输出:4
解释:你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
示例 3:
输入:nums = [1,2,3]
输出:3
提示:
1 <= nums.length <= 100
0 <= nums[i] <= 1000
解:
解题思路:
在上一题的基础上,数组可以成环(首尾相连):
我们可能会有如下几种考虑:
- 长度为1:
- 长度为2:
- 剔除尾:
- 剔除首:
AC代码:
class Solution {
public int rob(int[] nums) {
int len = nums.length;
if(len == 1) return nums[0];
if(len == 2) return Math.max(nums[0], nums[1]);
int res1 = robrange(nums, 0, len - 2);
int res2 = robrange(nums, 1, len - 1);
return Math.max(res1, res2);
}
int robrange(int[] nums, int l, int r) {
int[] dp = new int[nums.length];
dp[l] = nums[l]; dp[l + 1] = Math.max(nums[l], nums[l + 1]);
for(int i = l + 2; i <= r; ++ i) {
dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i]);
}
return dp[r];
}
}
题:337. 打家劫舍 III
小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root 。
除了 root 之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫
,房屋将自动报警。
给定二叉树的 root 。返回 在不触动警报的情况下
,小偷能够盗取的最高金额 。
示例 1:
输入: root = [3,2,3,null,3,null,1]
输出: 7
解释: 小偷一晚能够盗取的最高金额 3 + 3 + 1 = 7
示例 2:
输入: root = [3,4,5,1,3,null,1]
输出: 9
解释: 小偷一晚能够盗取的最高金额 4 + 5 = 9
提示:
树的节点数在 [1, 104] 范围内
0 <= Node.val <= 104
解:
解题思路:暴力递归 -- 超时
AC代码:
class Solution {
// 暴力递归
// 后序遍历 -> 需要通过递归的返回值做计算
// 如果抢了父节点,孩子不能动,如果没有抢父节点,孩子可以考虑动
// 终止条件:走到头
public int rob(TreeNode root) {
// 终止条件
if(root == null) return 0;
// 偷父节点
int val1 = root.val;
if(root.left != null) {
val1 += rob(root.left.left) + rob(root.left.right);
}
if(root.right != null) {
val1 += rob(root.right.left) + rob(root.right.right);
}
// 不偷父节点
int val2 = rob(root.left) + rob(root.right);
return Math.max(val1, val2);
}
}
解题思路:记忆化递归
AC代码:
class Solution {
// 记忆化递归,暴力递归计算完以root的四个孙子为头节点的子树,又计算了一次以root的左右孩子为头节点的子树。
// 记录计算过的结果
Map<TreeNode, Integer> memo = new HashMap<TreeNode, Integer>();
public int rob(TreeNode root) {
if(root == null) return 0;
if(memo.containsKey(root)) {
return memo.get(root);
}
// 抢父节点
int val1 = root.val;
if(root.left != null) {
val1 += rob(root.left.left) + rob(root.left.right);
}
if(root.right != null) {
val1 += rob(root.right.left) + rob(root.right.right);
}
// 不抢父节点
int val2 = rob(root.left) + rob(root.right);
int res = Math.max(val1, val2);
memo.put(root, res);
return res;
}
}
解题思路:树形DP
AC代码:
class Solution {
// 树形DP
// 1.返回值:长度为2的数组,下标0:不偷该节点所得到的最大金钱,下标1:偷该节点所得到的最大金钱
public int rob(TreeNode root) {
int[] res = robTree(root);
return Math.max(res[0], res[1]);
}
int[] robTree(TreeNode root) {
if(root == null) return new int[2];
int[] left = robTree(root.left);
int[] right = robTree(root.right);
// 不抢父节点
int val1 = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
// 抢父节点
int val2 = root.val + left[0] + right[0];
return new int[]{val1, val2};
}
}
股票问题
题:121. 买卖股票的最佳时机
给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。
示例 1:
输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
示例 2:
输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 没有交易完成, 所以最大利润为 0。
提示:
1 <= prices.length <= 105
0 <= prices[i] <= 104
解:
解题思路:暴力
AC代码:
略
解题思路:贪心
直接找最大区间利润
AC代码:
class Solution {
public int maxProfit(int[] prices) {
int min_price = Integer.MAX_VALUE; // 最小买入
int res = 0; // 利润
for(int i = 0; i < prices.length; ++ i) {
min_price = min_price > prices[i] ? prices[i] : min_price; // 记录之前最小的价格
res = res > prices[i] - min_price ? res : prices[i] - min_price; // 记录之前最大的利润区间
}
return res;
}
}
解题思路:
- 状态分析
dp 两个状态:持有股票、不持有股票 => dpi, dpi => 第i天持有股票所得最多现金,第i天未持有股票所得最多现金
- 状态转移
dpi = max(dpi - 1, -prices[i]) 第i天持有股票=>(1)i-1天持有(2)i天买入
dpi = max(dpi - 1, dpi - 1 + prices[i]) 第i天未持有股票=> (1)i-1天未持有 (2)i天卖掉
- 初始化
dp0 = -prices0; dp0 = 0
- 正向遍历
AC代码:
class Solution {
// 状态分析
// dp 两个状态:持有股票、不持有股票 => dp[i][0], dp[i][1] => 第i天持有股票所得最多现金,第i天未持有股票所得最多现金
// 状态转移
// dp[i][0] = max(dp[i - 1][0], -prices[i]) 第i天持有股票=>(1)i-1天持有(2)i天买入
// dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i]) 第i天未持有股票=> (1)i-1天未持有 (2)i天卖掉
// 初始化
// dp[0][0] = -prices[0](第0天持有股票); dp[0][1] = 0
// 正向遍历
public int maxProfit(int[] prices) {
int len = prices.length;
int[][] dp = new int[len][2];
dp[0][0] = -prices[0]; dp[0][1] = 0;
for(int i = 1; i < len; ++ i) {
dp[i][0] = Math.max(dp[i - 1][0], -prices[i]);
dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] + prices[i]);
}
return dp[len - 1][1];
}
}
解题思路:滚动数组优化
从递推公式来看,dp[i]只依赖于dp[i-1]
AC代码:
class Solution {
public int maxProfit(int[] prices) {
int len = prices.length;
int[][] dp = new int[2][2];
dp[0][0] = -prices[0]; dp[0][1] = 0;
for(int i = 1; i < len; ++ i) {
dp[i % 2][0] = Math.max(dp[(i - 1) % 2][0], -prices[i]);
dp[i % 2][1] = Math.max(dp[(i - 1) % 2][1], dp[(i - 1) % 2][0] + prices[i]);
}
return dp[(len - 1) % 2][1];
}
}
题:122. 买卖股票的最佳时机 II
给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。
在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。
返回 你能获得的 最大 利润 。
示例 1:
输入: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 。
示例 2:
输入:prices = [1,2,3,4,5]
输出:4
解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4 。
总利润为 4 。
示例 3:
输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 交易无法获得正利润,所以不参与交易可以获得最大利润,最大利润为 0 。
提示:
1 <= prices.length <= 3 * 104
0 <= prices[i] <= 104
解:
解题思路:
- 两种状态:持有股票,未持股票 => dpi,dpi
- 状态转移
第i天持有股票 <= 前一天持有股票 或者 今天买了股票
dpi = max(dpi - 1, dpi - 1 - prices[i]);
第i天不持有股票 <= 前一天不持有股票 或者 今天卖了股票
dpi = max(dpi - 1, dpi - 1 + prices[i]);
- 初始化
dp0 = -prices[0]; dp0 = 0;
AC代码:
class Solution {
// 两种状态:持有股票,未持股票 => dp[i][0],dp[i][1]
// 状态转移
// 第i天持有股票 <= 前一天持有股票 或者 今天买了股票
// dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]);
// 第i天不持有股票 <= 前一天不持有股票 或者 今天卖了股票
// dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i]);
// 初始化
// dp[0][0] = -prices[0]; dp[0][1] = 0;
public int maxProfit(int[] prices) {
int len = prices.length;
int[][] dp = new int[len][2];
dp[0][0] = -prices[0]; dp[0][1] = 0;
for(int i = 1; i < len; ++ i) {
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] - prices[i]);
dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] + prices[i]);
}
return dp[len - 1][1];
}
}
解题思路:滚动数组优化
AC代码:
class Solution {
public int maxProfit(int[] prices) {
int len = prices.length;
// 由于dp[i][0] 只有 dp[i - 1][0] 有关
int[][] dp = new int[2][2];
dp[0][0] = -prices[0]; dp[0][1] = 0;
for(int i = 1; i < len; ++ i) {
dp[i % 2][0] = Math.max(dp[(i - 1) % 2][0], dp[(i - 1) % 2][1] - prices[i]);
dp[i % 2][1] = Math.max(dp[(i - 1) % 2][1], dp[(i - 1) % 2][0] + prices[i]);
}
return dp[(len - 1) % 2][1];
}
}
题:123. 买卖股票的最佳时机 III
给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
输入:prices = [3,3,5,0,0,3,1,4]
输出:6
解释:在第 4 天(股票价格 = 0)的时候买入,在第 6 天(股票价格 = 3)的时候卖出,这笔交易所能获得利润 = 3-0 = 3 。
随后,在第 7 天(股票价格 = 1)的时候买入,在第 8 天 (股票价格 = 4)的时候卖出,这笔交易所能获得利润 = 4-1 = 3 。
示例 2:
输入:prices = [1,2,3,4,5]
输出:4
解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。
因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。
示例 3:
输入:prices = [7,6,4,3,1]
输出:0
解释:在这个情况下, 没有交易完成, 所以最大利润为 0。
示例 4:
输入:prices = [1]
输出:0
提示:
1 <= prices.length <= 105
0 <= prices[i] <= 105
解:
解题思路:
AC代码:
class Solution {
// 最多完成两笔交易 => 不交易, 1笔交易, 2笔交易
// 五种状态:不交易,第一次买入,第一次卖出,第二次买入,第二次卖出
// dp[i][5] 0 1 2 3 4
// 状态转移
// dp[i][0] = dp[i - 1][0];
// dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
// dp[i][2] = max(dp[i - 1][2], dp[i - 1][1] + prices[i]);
// ...
// 初始化
// dp[0][0] = 0; dp[0][1] = -prices[0]; dp[0][2] = 0; dp[0][3] = -prices[0]; dp[0][4] = 0;
// 每个状态依赖于前一个状态
public int maxProfit(int[] prices) {
int len = prices.length;
int[][] dp = new int[len][5];
dp[0][1] = -prices[0]; dp[0][3] = -prices[0];
for(int i = 1; i < len; ++ i) {
dp[i][0] = dp[i - 1][0];
dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
dp[i][2] = Math.max(dp[i - 1][2], dp[i - 1][1] + prices[i]);
dp[i][3] = Math.max(dp[i - 1][3], dp[i - 1][2] - prices[i]);
dp[i][4] = Math.max(dp[i - 1][4], dp[i - 1][3] + prices[i]);
}
return dp[len - 1][4]; // 肯定两次购买最后一次利润多
}
}
解题思路:空间优化
AC代码:
class Solution {
public int maxProfit(int[] prices) {
int len = prices.length;
int[] dp = new int[5];
dp[1] = -prices[0]; dp[3] = -prices[0];
for(int i = 1; i < len; ++ i) {
dp[1] = Math.max(dp[1], dp[0] - prices[i]);
dp[2] = Math.max(dp[2], dp[1] + prices[i]);
dp[3] = Math.max(dp[3], dp[2] - prices[i]);
dp[4] = Math.max(dp[4], dp[3] + prices[i]);
}
return dp[4]; // 肯定两次购买最后一次利润多
}
}
题:188. 买卖股票的最佳时机 IV
给定一个整数数组 prices ,它的第 i 个元素 prices[i] 是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。
- 注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
输入:k = 2, prices = [2,4,1]
输出:2
解释:在第 1 天 (股票价格 = 2) 的时候买入,在第 2 天 (股票价格 = 4) 的时候卖出,这笔交易所能获得利润 = 4-2 = 2 。
示例 2:
输入:k = 2, prices = [3,2,6,5,0,3]
输出:7
解释:在第 2 天 (股票价格 = 2) 的时候买入,在第 3 天 (股票价格 = 6) 的时候卖出, 这笔交易所能获得利润 = 6-2 = 4 。
随后,在第 5 天 (股票价格 = 0) 的时候买入,在第 6 天 (股票价格 = 3) 的时候卖出, 这笔交易所能获得利润 = 3-0 = 3 。
提示:
0 <= k <= 100
0 <= prices.length <= 1000
0 <= prices[i] <= 1000
解:
解题思路:
AC代码:
class Solution {
// 状态分析
// 0,1(买入),2,3(买入),4...
// 我们可以得出奇数买入,偶数卖出
public int maxProfit(int k, int[] prices) {
int len = prices.length;
if(len == 0 || k == 0) return 0;
int[][] dp = new int[len][2 * k + 1];
// 初始化
for(int j = 1; j < 2 * k; j += 2) {
dp[0][j] = -prices[0];
}
for(int i = 1; i < len; ++ i) {
for(int j = 1; j < 2 * k + 1; j += 2) {
// 持有股票
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - 1] - prices[i]);
// 未持有股票
dp[i][j + 1] = Math.max(dp[i - 1][j + 1], dp[i - 1][j] + prices[i]);
}
}
return dp[len - 1][2 * k];
}
}
题:309. 最佳买卖股票时机含冷冻期
给定一个整数数组prices,其中第 prices[i] 表示第 i 天的股票价格 。
设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
输入: prices = [1,2,3,0,2]
输出: 3
解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]
示例 2:
输入: prices = [1]
输出: 0
提示:
1 <= prices.length <= 5000
0 <= prices[i] <= 1000
解:
解题思路:状态分析
- 持有股票状态
1.1 昨天持有股票
1.2 今天买了股票=>昨天要么冰冻期要么不持有股票=>前天肯定不持有股票=>昨天前天肯定都不持有股票
- 未持有股票状态
2.1 昨天未持有股票
2.2 今天卖了股票=>昨天持有股票
AC代码:
class Solution {
// 状态分析
// 1.持有股票状态
// 1.1 昨天持有股票 1.2 今天买了股票=>昨天要么冰冻期要么不持有股票=>前天肯定不持有股票=>昨天前天肯定都不持有股票
// 2.未持有股票状态
// 2.1 昨天未持有股票 2.2 今天卖了股票=>昨天持有股票
public int maxProfit(int[] prices) {
int len = prices.length;
int[][] dp = new int[len][2];
// 0=>未持有 1=>持有
dp[0][0] = 0; dp[0][1] = -prices[0];
for(int i = 1; i < len; ++ i) {
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
dp[i][1] = Math.max(
dp[i - 1][1],
// 可能没有前天
i > 1 ? dp[i - 2][0] - prices[i] : dp[i - 1][0] - prices[i]
);
}
return dp[len - 1][0];
}
}
题:714. 买卖股票的最佳时机含手续费
给定一个整数数组 prices,其中 prices[i]表示第 i 天的股票价格 ;整数 fee 代表了交易股票的手续费用。
你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。
返回获得利润的最大值。
- 注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。
示例 1:
输入: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
示例 2:
输入:prices = [1,3,7,5,10,3], fee = 3
输出:6
提示:
1 <= prices.length <= 5 * 104
1 <= prices[i] < 5 * 104
0 <= fee < 5 * 104
解:
解题思路:
AC代码:
class Solution {
// 状态分析
// 持有股票 => 1.昨天持有 2.今天买入
// 未持有股票 => 1.昨天未持有 2.今天卖出(扣手续费)
public int maxProfit(int[] prices, int fee) {
int len = prices.length;
int[][] dp = new int[len][2];
dp[0][0] = 0; dp[0][1] = -prices[0];
for(int i = 1; i < len; ++ i) {
// 未持有股票
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i] - fee);
// 持有股票
dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
}
return dp[len - 1][0];
}
}
序列
题:300. 最长递增子序列
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
示例 1:
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
示例 2:
输入:nums = [0,1,0,3,2,3]
输出:4
示例 3:
输入:nums = [7,7,7,7,7,7,7]
输出:1
提示:
1 <= nums.length <= 2500
-104 <= nums[i] <= 104
进阶:
你能将算法的时间复杂度降低到 O(n log(n)) 吗?
解:
解题思路:
- dp[i]表示nums[0]到nums[i]以nums[i]结尾的最长递增子序列长度
- 状态转移:(与j∈[0, i-1]比较)
1.如果nums[i] > nums[j] 考虑 dp[i] = max(dp[i], dp[j] + 1);
否则 dp[i] = dp[i];
- 初始化 dp[i] = 0;
- 遍历顺序:正向
- 返回值:dp[i]的最大值
AC代码:
class Solution {
// dp[i]表示nums[0]到nums[i]以nums[i]结尾的最长递增子序列长度
// 状态转移:(与j∈[0, i-1]比较)
// 1.如果nums[i] > nums[j] 考虑 dp[i] = max(dp[i], dp[j] + 1);
// 否则 dp[i] = dp[i];
// 初始化 dp[i] = 0;
// 遍历顺序:正向
// 返回值:dp[i]的最大值
public int lengthOfLIS(int[] nums) {
int len = nums.length;
int[] dp = new int[len];
Arrays.fill(dp, 1); // 初始化
int res = 1; // 长度最小不低于1
for(int i = 0; i < len; ++ i) {
for(int j = 0; j < i; ++ j) {
if(nums[i] > nums[j]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
if(dp[i] > res) res = dp[i];
}
}
return res;
}
}
题:674. 最长连续递增序列
给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。
连续递增的子序列 可以由两个下标 l 和 r(l < r)确定,如果对于每个 l <= i < r,都有 nums[i] < nums[i + 1] ,那么子序列 [nums[l], nums[l + 1], ..., nums[r - 1], nums[r]] 就是连续递增子序列。
示例 1:
输入:nums = [1,3,5,4,7]
输出:3
解释:最长连续递增序列是 [1,3,5], 长度为3。
尽管 [1,3,5,7] 也是升序的子序列, 但它不是连续的,因为 5 和 7 在原数组里被 4 隔开。
示例 2:
输入:nums = [2,2,2,2,2]
输出:1
解释:最长连续递增序列是 [2], 长度为1。
提示:
1 <= nums.length <= 104
-109 <= nums[i] <= 109
解:
解题思路:
- dp[i]:以dp[i]结尾的最长连续递增序列长度
- dp[i] = dp[i - 1] + 1; // 只能由上一个状态推出
- dp[i] = 1;
- 结果:取dp[]最大值
AC代码:
class Solution {
// dp[i]:以dp[i]结尾的最长连续递增序列长度
// dp[i] = dp[i - 1] + 1; // 只能由上一个状态推出
// dp[i] = 1;
// 结果:取dp[]最大值
public int findLengthOfLCIS(int[] nums) {
int len = nums.length;
int[] dp = new int[len];
Arrays.fill(dp, 1); // 初始化
int res = 1; //
for(int i = 1; i < len; ++ i) {
if(nums[i] > nums[i - 1]) {
dp[i] = dp[i - 1] + 1;
}
if(dp[i] > res) res = dp[i];
}
return res;
}
}
解题思路:贪心
AC代码:
class Solution {
// 贪心
public int findLengthOfLCIS(int[] nums) {
int len = nums.length;
int cnt = 1;
int res = 1;
for(int i = 1; i < len; ++ i) {
if(nums[i] > nums[i - 1]) {
cnt ++;
}else {
cnt = 1;
}
if(cnt > res) res = cnt;
}
return res;
}
}
题:718. 最长重复子数组
给两个整数数组 nums1 和 nums2 ,返回 两个数组中 公共的 、长度最长的子数组的长度 。
示例 1:
输入:nums1 = [1,2,3,2,1], nums2 = [3,2,1,4,7]
输出:3
解释:长度最长的公共子数组是 [3,2,1] 。
示例 2:
输入:nums1 = [0,0,0,0,0], nums2 = [0,0,0,0,0]
输出:5
提示:
1 <= nums1.length, nums2.length <= 1000
0 <= nums1[i], nums2[i] <= 100
解:
解题思路:
- dpi:以下标i-1结尾的A,j-1结尾的B,最长公共子数组长度
- if(A[i - 1] == B[j - 1]) dpi = dpi - 1 + 1;
- 这里填充dp[][]需要从左上角推导,为了方便非空判断,加了一行一列0
- 初始化:dp0 = 0;
- 结果:取dp的Max
AC代码:
class Solution {
// dp[i][j]:以下标i-1结尾的A,j-1结尾的B,最长公共子数组长度
// if(A[i - 1] == B[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1;
// 这里填充dp[][]需要从左上角推导,为了方便非空判断,加了一行一列0
// 初始化:dp[0][0] = 0;
// 结果:取dp的Max
public int findLength(int[] nums1, int[] nums2) {
int len1 = nums1.length; int len2 = nums2.length;
int[][] dp = new int[len1 + 1][len2 + 1];
int res = 0; // dp[i][j] 最小不低于0
for(int i = 1; i <= len1; ++ i) {
for(int j = 1; j <= len2; ++ j) {
if(nums1[i - 1] == nums2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
}
res = dp[i][j] > res ? dp[i][j] : res;
}
}
return res;
}
}
解题思路:滚动数组优化
AC代码:
class Solution {
public int findLength(int[] nums1, int[] nums2) {
int[] dp = new int[nums2.length + 1];
int res = 0;
for(int i = 1; i <= nums1.length; ++ i) {
for(int j = nums2.length; j > 0; -- j) { // 注意这里是从后往前遍历
if(nums2[j - 1] == nums1[i - 1]) dp[j] = dp[j - 1] + 1;
else dp[j] = 0; // 如果不相等一定要初始化为0
if(dp[j] > res) res = dp[j];
}
}
return res;
}
}
题:1143. 最长公共子序列
给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
- 例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。
示例 1:
输入:text1 = "abcde", text2 = "ace"
输出:3
解释:最长公共子序列是 "ace" ,它的长度为 3 。
示例 2:
输入:text1 = "abc", text2 = "abc"
输出:3
解释:最长公共子序列是 "abc" ,它的长度为 3 。
示例 3:
输入:text1 = "abc", text2 = "def"
输出:0
解释:两个字符串没有公共子序列,返回 0 。
提示:
1 <= text1.length, text2.length <= 1000
text1 和 text2 仅由小写英文字符组成。
解:
解题思路:
- 状态分析
dpi:长度为[0,i-1]的text1,[0,j-1]的text2的最长公共子序列
- 状态转移
if(text1[i - 1] == text2[j - 1]) dpi = dpi - 1 + 1
if(text1[i - 1] != text2[j - 1]) dpi = max(dpi - 1, dpi)
- 初始化
空串和任何一个串最长公共子序列为0 dpi = 0, dp0 = 0; - 遍历顺序 dpi的来源可能有(dpi - 1, dpi - 1, dpi)
AC代码:
class Solution {
// 状态分析
// dp[i][j]:长度为[0,i-1]的text1,[0,j-1]的text2的最长公共子序列
// 状态转移
// if(text1[i - 1] == text2[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1
// if(text1[i - 1] != text2[j - 1]) dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
// 空串和任何一个串最长公共子序列为0 dp[i][0] = 0, dp[0][j] = 0;
// 遍历顺序 dp[i][j]的来源可能有(dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1])
public int longestCommonSubsequence(String text1, String text2) {
int len1 = text1.length(); int len2 = text2.length();
int[][] dp = new int[len1 + 1][len2 + 1];
for(int i = 1; i <= len1; ++ i) {
for(int j = 1; j <= len2; ++ j) {
if(text1.charAt(i - 1) == text2.charAt(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[len1][len2];
}
}
题:1035. 不相交的线
在两条独立的水平线上按给定的顺序写下 nums1 和 nums2 中的整数。
现在,可以绘制一些连接两个数字 nums1[i] 和 nums2[j] 的直线,这些直线需要同时满足满足:
- nums1[i] == nums2[j]
- 且绘制的直线不与任何其他连线(非水平线)相交。
请注意,连线即使在端点也不能相交:每个数字只能属于一条连线。
以这种方法绘制线条,并返回可以绘制的最大连线数。
示例 1:
输入:nums1 = [1,4,2], nums2 = [1,2,4]
输出:2
解释:可以画出两条不交叉的线,如上图所示。
但无法画出第三条不相交的直线,因为从 nums1[1]=4 到 nums2[2]=4 的直线将与从 nums1[2]=2 到 nums2[1]=2 的直线相交。
示例 2:
输入:nums1 = [2,5,1,2,5], nums2 = [10,5,2,1,5,2]
输出:3
示例 3:
输入:nums1 = [1,3,7,1,7,5], nums2 = [1,9,2,5,1]
输出:2
提示:
1 <= nums1.length, nums2.length <= 500
1 <= nums1[i], nums2[j] <= 2000
解:
解题思路:
AC代码:
class Solution {
// 求绘制的最大连接数 == 求两个字符串的最长公共子序列的长度
public int maxUncrossedLines(int[] nums1, int[] nums2) {
int len1 = nums1.length;
int len2 = nums2.length;
int[][] dp = new int[len1 + 1][len2 + 1];
for(int i = 1; i <= len1; ++ i) {
for(int j = 1; j <= len2; ++ 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[len1][len2];
}
}
题:53. 最大子数组和
给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
子数组 是数组中的一个连续部分。
示例 1:
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
示例 2:
输入:nums = [1]
输出:1
示例 3:
输入:nums = [5,4,-1,7,8]
输出:23
提示:
1 <= nums.length <= 105
-104 <= nums[i] <= 104
- 进阶:如果你已经实现复杂度为 O(n) 的解法,尝试使用更为精妙的 分治法 求解。
解:
AC代码:
class Solution {
// 状态分析
// dp[i]: nums下标在[0,i]最大子序列和
// 状态转移
// dp[i] = dp[i - 1] + nums[i]; dp[i] = nums[i];
// 初始化
// dp[0] = nums[0];
// 结果取dp最大值
public int maxSubArray(int[] nums) {
int[] dp = new int[nums.length];
int res = nums[0];
dp[0] = nums[0];
for(int i = 1; i < nums.length; ++ i) {
dp[i] = Math.max(dp[i - 1] + nums[i], nums[i]);
if(dp[i] > res) res = dp[i];
}
return res;
}
}
编辑距离
题:392. 判断子序列
给定字符串 s 和 t ,判断 s 是否为 t 的子序列。
字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。
进阶:
如果有大量输入的 S,称作 S1, S2, ... , Sk 其中 k >= 10亿,你需要依次检查它们是否为 T 的子序列。在这种情况下,你会怎样改变代码?
示例 1:
输入:s = "abc", t = "ahbgdc"
输出:true
示例 2:
输入:s = "axc", t = "ahbgdc"
输出:false
提示:
0 <= s.length <= 100
0 <= t.length <= 10^4
两个字符串都只由小写字符组成。
解:
解题思路:
AC代码:
class Solution {
// dp[i][j]:[0,i)s [0,j)t 相同子序列个数
// 状态转移
// s[i-1] == t[j-1] dp[i][j] = dp[i-1][j-1] + 1
// s[i-1] != t[j-1] dp[i][j] = dp[i][j-1]
public boolean isSubsequence(String s, String t) {
int len1 = s.length(); int len2 = t.length();
int[][] dp = new int[len1 + 1][len2 + 1];
for(int i = 1; i <= len1; ++ i) {
for(int j = 1; j <= len2; ++ 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[len1][len2] == len1;
}
}
题:115. 不同的子序列
给定一个字符串 s 和一个字符串 t ,计算在 s 的子序列中 t 出现的个数。
字符串的一个 子序列 是指,通过删除一些(也可以不删除)字符且不干扰剩余字符相对位置所组成的新字符串。(例如,"ACE" 是 "ABCDE" 的一个子序列,而 "AEC" 不是)
题目数据保证答案符合 32 位带符号整数范围。
示例 1:
输入:s = "rabbbit", t = "rabbit"
输出:3
解释:
如下图所示, 有 3 种可以从 s 中得到 "rabbit" 的方案。
rabbbit
rabbbit
rabbbit
示例 2:
输入:s = "babgbag", t = "bag"
输出:5
解释:
如下图所示, 有 5 种可以从 s 中得到 "bag" 的方案。
babgbag
babgbag
babgbag
babgbag
babgbag
提示:
0 <= s.length, t.length <= 1000
s 和 t 由英文字母组成
解:
解题思路:
AC代码:
class Solution {
// 状态分析
// dp[i][j]:[0,i) s 中 [0,j) t 出现的个数
// 状态转移
// s[i - 1] == t[j - 1]
// b a g g b a g
// j-1 j
// dp[i][j] = dp[i - 1][j] + dp[i - 1][j - 1];
// s[i - 1] != t[j - 1]
// dp[i][j] = dp[i - 1][j]
// 初始化
// dp[i][0] = 1; // 任何一个串都能匹配到空串一次
// dp[0][j] = 0; // 任何一个空串都不能匹配到非空串(j > 0)
public int numDistinct(String s, String t) {
int len1 = s.length();
int len2 = t.length();
int[][] dp = new int[len1 + 1][len2 + 1];
for(int i = 0; i <= len1; ++ i) dp[i][0] = 1;
for(int i = 1; i <= len1; ++ i) {
for(int j = 1; j <= len2; ++ j) {
if(s.charAt(i - 1) == t.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j] + dp[i - 1][j - 1];
} else {
dp[i][j] = dp[i - 1][j];
}
}
}
return dp[len1][len2];
}
}
题:583. 两个字符串的删除操作
给定两个单词 word1 和 word2 ,返回使得 word1 和 word2 相同所需的最小步数。
每步 可以删除任意一个字符串中的一个字符。
示例 1:
输入: word1 = "sea", word2 = "eat"
输出: 2
解释: 第一步将 "sea" 变为 "ea" ,第二步将 "eat "变为 "ea"
示例 2:
输入:word1 = "leetcode", word2 = "etco"
输出:4
提示:
1 <= word1.length, word2.length <= 500
word1 和 word2 只包含小写英文字母
解:
解题思路:
AC代码:
class Solution {
// 状态分析
// dp[i][j]:以i-1结尾word1,j-1结尾word2,达到相等所需删除元素最少次数
// 状态转移
// word1[i - 1] == word2[j - 1]
// dp[i][j] = dp[i - 1][j - 1]; // 删除次数不变
// word1[i - 1] != word2[j - 1]
// 1.删word1[i - 1] 2.删word2[j - 1] 3.删word1[i - 1], word2[j - 1]
// dp[i][j] = min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + 2)
// 初始化
// dp[i][0] = i dp[0][j] = j;
public int minDistance(String word1, String word2) {
int len1 = word1.length(); int len2 = word2.length();
int[][] dp = new int[len1 + 1][len2 + 1];
for(int i = 0; i <= len1; ++ i) dp[i][0] = i;
for(int j = 0; j <= len2; ++ j) dp[0][j] = j;
for(int i = 1; i <= len1; ++ i) {
for(int j = 1; j <= len2; ++ j) {
if(word1.charAt(i - 1) == word2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = Math.min(dp[i - 1][j] + 1, Math.min(dp[i][j - 1] + 1, dp[i - 1][j - 1] + 2));
}
}
}
return dp[len1][len2];
}
}
解题思路:
AC代码:
class Solution {
// 思路2:两个字符串长度 - 字符串最长公共子序列 * 2
public int minDistance(String word1, String word2) {
int len1 = word1.length(); int len2 = word2.length();
int[][] dp = new int[len1 + 1][len2 + 1];
for(int i = 1; i <= len1; ++ i) {
for(int j = 1; j <= len2; ++ j) {
if(word1.charAt(i - 1) == word2.charAt(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 len1 + len2 - dp[len1][len2] * 2;
}
}
题:72. 编辑距离
给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
- 插入一个字符
- 删除一个字符
- 替换一个字符
示例 1:
输入:word1 = "horse", word2 = "ros"
输出:3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')
示例 2:
输入:word1 = "intention", word2 = "execution"
输出:5
解释:
intention -> inention (删除 't')
inention -> enention (将 'i' 替换为 'e')
enention -> exention (将 'n' 替换为 'x')
exention -> exection (将 'n' 替换为 'c')
exection -> execution (插入 'u')
提示:
0 <= word1.length, word2.length <= 500
word1 和 word2 由小写英文字母组成
解:
解题思路:
AC代码:
class Solution {
// 状态分析
// dp[i][j]表示以[0,i-1]word1,[0,j-1]word2最近编辑距离dp[i][j]
// 状态转移
// word1[i - 1] == word2[j - 1] => 不操作
// dp[i][j] = dp[i - 1][j - 1];
// word1[i - 1] != word2[j - 1] => 增、删、换
// word1删除相当于word2增加,word1增加相当于word2删除
// dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + 1;
// 换
// dp[i][j] = dp[i - 1][j - 1] + 1;
// => dp[i][j] = min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]) + 1;
// 初始化
// dp[i][0] = i dp[0][j] = j;
public int minDistance(String word1, String word2) {
int len1 = word1.length(); int len2 = word2.length();
int[][] dp = new int[len1 + 1][len2 + 1];
// 初始化
for(int i = 1; i <= len1; ++ i) dp[i][0] = i;
for(int j = 1; j <= len2; ++ j) dp[0][j] = j;
// 正向遍历
for(int i = 1; i <= len1; ++ i) {
for(int j = 1; j <= len2; ++ j) {
if(word1.charAt(i - 1) == word2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = Math.min(dp[i - 1][j], Math.min(dp[i][j - 1], dp[i - 1][j - 1])) + 1;
}
}
}
return dp[len1][len2];
}
}
杂题
题:647. 回文子串
给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。
回文字符串 是正着读和倒过来读一样的字符串。
子字符串 是字符串中的由连续字符组成的一个序列。
具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。
示例 1:
输入:s = "abc"
输出:3
解释:三个回文子串: "a", "b", "c"
示例 2:
输入:s = "aaa"
输出:6
解释:6个回文子串: "a", "a", "a", "aa", "aa", "aaa"
提示:
1 <= s.length <= 1000
s 由小写英文字母组成
解:
解题思路:
AC代码:
class Solution {
// 状态分析
// dp[i][j] [i, j]字符串是否为回文串
// 状态转移
// s[i] != s[j] dp[i][j] = false;
// s[i] == s[j]
// 1. i==j a是回文串
// 2. j - i = 1、2 aa、aba是回文串
// 3. cabac 是回文串 判断 [i + 1, j - 1]是否为回文串 => dp[i + 1][j - 1]是否为true
// 初始化
// dp[i][j] = false;
// 遍历顺序
// dp[i + 1][j - 1]在dp[i][j]的左下角 => 从下到上,从左往右
// 返回结果 res 统计true
public int countSubstrings(String s) {
int len = s.length();
boolean[][] dp = new boolean[len][len];
int res = 0;
for(int i = len - 1; i >= 0; -- i) {
for(int j = i; j < len; ++ j) {
if(s.charAt(i) == s.charAt(j)) {
// 1.2合并
if(j - i <= 2) {
dp[i][j] = true;
res ++;
} else if(dp[i + 1][j - 1]){
dp[i][j] = true;
res ++;
}
}
}
}
return res;
}
}
解题思路:中心扩散法
AC代码:
class Solution {
// 中心扩散法
public int countSubstrings(String s) {
int res = 0;
int len = s.length();
for(int i = 0; i < len; ++ i) {
// 以一个元素为中心点
res += extend(s, i, i, len);
// 以两个元素为中心点
res += extend(s, i, i + 1, len);
}
return res;
}
// [l, r)
int extend(String s, int l, int r, int len) {
int res = 0;
while(l >= 0 && r < len && s.charAt(l) == s.charAt(r)) {
-- l; ++ r; ++ res;
}
return res;
}
}
题:516. 最长回文子序列
给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。
子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。
示例 1:
输入:s = "bbbab"
输出:4
解释:一个可能的最长回文子序列为 "bbbb" 。
示例 2:
输入:s = "cbbd"
输出:2
解释:一个可能的最长回文子序列为 "bb" 。
提示:
1 <= s.length <= 1000
s 仅由小写英文字母组成
解:
解题思路:
AC代码:
class Solution {
// 状态分析
// dp[i][j]代表[i,j]字符串最长回文子序列
// 状态转移
// s[i] == s[j] dp[i][j] = dp[i - 1][j + 1] + 2; 中心扩展
// s[i] != s[j] 康康加入一个可以组成回文序列不
// dp[i][j] = max(dp[i - 1][j], dp[i][j + 1])
// 初始化
// dp[i - 1][j + 1]算不到i==j dp[i][j] = 1
// 遍历顺序 dp[i][j]来源于左、下、左下
// 从下到上 从左往右
// 返回结果:右上
public int longestPalindromeSubseq(String s) {
int len = s.length();
int[][] dp = new int[len][len];
// 初始化
for(int i = 0; i < len; ++ i) dp[i][i] = 1;
for(int i = len - 1; i >= 0; -- i) {
for(int j = i + 1; j < len; ++ j) {
if(s.charAt(i) == s.charAt(j)) {
dp[i][j] = dp[i + 1][j - 1] + 2;
} else {
dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]);
}
}
}
return dp[0][len - 1];
}
}