文章目录
- 动态规划
- ……………………基础题目………………
- 70. 爬楼梯
- 343. 整数拆分
- 63. 不同路径 II
- 1414. 和为 K 的最少斐波那契数字数目
- 相似题目:509. 斐波那契数
- 91. 解码方法
- 62. 不同路径
- 相似题目:剑指 Offer 47. 礼物的最大价值
- 403. 青蛙过河
- 978. 最长湍流子数组
- 相似题目:376. 摆动序列
- 53. 最大子数组和
- 32. 最长有效括号
- 221. 最大正方形
- ……………………背包问题………………
- ……………………背包问题:01背包………………
- 1049. 最后一块石头的重量 II
- 494. 目标和
- 474. 一和零
- ……………………背包问题:完全背包………………
- 求最小数
- 322. 零钱兑换
- 求组合数
- 518. 零钱兑换 II
- 377. 组合总和 Ⅳ
- 279. 完全平方数
- 139. 单词拆分
- 相似题目:140. 单词拆分 II
- ……………………打家劫舍………………
- 198. 打家劫舍
- 相似题目:740. 删除并获得点数
- 213. 打家劫舍 II
- 337. 打家劫舍 III
- ……………………股票问题………………
- 121. 买卖股票的最佳时机
- 122. 买卖股票的最佳时机 II
- 123. 买卖股票的最佳时机 III
- 188. 买卖股票的最佳时机 IV
- 309. 最佳买卖股票时机含冷冻期
- 714. 买卖股票的最佳时机含手续费
- ……………………子序列问题………………
- ……………………子序列问题:子序列(不连续)………………
- 300. 最长递增子序列
- 673. 最长递增子序列的个数
- 674. 最长连续递增序列
- 相似题目:128. 最长连续序列
- 相似题目:354. 俄罗斯套娃信封问题
- 1143. 最长公共子序列
- 相似题目:392. 判断子序列
- 相似题目:718. 最长重复子数组
- 相似题目:1035. 不相交的线
- 53. 最大子数组和
- ……………………子序列问题:子序列(连续)………………
- ……………………子序列问题:编辑距离………………
- 72. 编辑距离
- 相似题目:583. 两个字符串的删除操作
- ……………………子序列问题:回文………………
- 647. 回文子串
- 相似题目:5. 最长回文子串
- 相似题目:516. 最长回文子序列
- 689. 三个无重叠子数组的最大和
- 1220. 统计元音字母序列的数目
- 64. 最小路径和
- 120. 三角形最小路径和
- 375. 猜数字大小 II
- 相似题目:374. 猜数字大小
- 264. 丑数 II
- 相似题目:263. 丑数
- 55. 跳跃游戏
- 相似题目:45. 跳跃游戏 II
动态规划
……………………基础题目………………
70. 爬楼梯
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
示例 1:
输入:n = 2
输出:2
解释:有两种方法可以爬到楼顶。
- 1 阶 + 1 阶
- 2 阶
示例 2:
输入:n = 3
输出:3
解释:有三种方法可以爬到楼顶。
- 1 阶 + 1 阶 + 1 阶
- 1 阶 + 2 阶
- 2 阶 + 1 阶
提示:
1 <= n <= 45
解法1:动态规划
dp[i]: 爬到第i层楼梯,有dp[i]种方法
// 版本一
class Solution {
public:
int climbStairs(int n) {
if (n <= 1) return n; // 因为下面直接对dp[2]操作了,防止空指针
vector<int> dp(n + 1);
dp[1] = 1;
dp[2] = 2;
for (int i = 3; i <= n; i++) { // 注意i是从3开始的
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
};
时间复杂度:
O
(
n
)
O(n)
O(n)
空间复杂度:
O
(
n
)
O(n)
O(n)
// 版本二
class Solution {
public:
int climbStairs(int n) {
if (n <= 1) return n;
int dp[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];
}
};
时间复杂度:
O
(
n
)
O(n)
O(n)
空间复杂度:
O
(
1
)
O(1)
O(1)
解法2:完全背包
加粗样式
class Solution {
public:
int climbStairs(int n) {
vector<int> dp(n + 1, 0);
dp[0] = 1;
for (int i = 1; i <= n; i++) { // 遍历背包
for (int j = 1; j <= 2; j++) { // 遍历物品
if (i - j >= 0) dp[i] += dp[i - j];
}
}
return dp[n];
}
};
解法1:动态规划
- 确定dp数组以及下标的含义
使用动态规划,就要有一个数组来记录状态,本题只需要一个一维数组dp[i]就可以了。
dp[i]的定义:到达第i个台阶所花费的最少体力为dp[i]。(注意这里认为是第一步一定是要花费)
对于dp数组的定义,大家一定要清晰!
- 确定递推公式
可以有两个途径得到dp[i],一个是dp[i-1] 一个是dp[i-2]。
那么究竟是选dp[i-1]还是dp[i-2]呢?
一定是选最小的,所以dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i];
注意这里为什么是加cost[i],而不是cost[i-1],cost[i-2]之类的,因为题目中说了:每当你爬上一个阶梯你都要花费对应的体力值
- dp数组如何初始化
根据dp数组的定义,dp数组初始化其实是比较难的,因为不可能初始化为第i台阶所花费的最少体力。
那么看一下递归公式,dp[i]由dp[i-1],dp[i-2]推出,既然初始化所有的dp[i]是不可能的,那么只初始化dp[0]和dp[1]就够了,其他的最终都是dp[0]dp[1]推出。
所以初始化代码为:
vector<int> dp(cost.size());
dp[0] = cost[0];
dp[1] = cost[1];
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost) {
vector<int> dp(cost.size());
dp[0] = cost[0];
dp[1] = cost[1];
for (int i = 2; i < cost.size(); i++) {
dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i];
}
// 注意最后一步可以理解为不用花费,所以取倒数第一步,第二步的最少值
return min(dp[cost.size() - 1], dp[cost.size() - 2]);
}
};
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
解法1:动态规划
思路:
代码:
class Solution {
public:
int integerBreak(int n) {
vector<int> dp(n+1);//dp[i]表示正整数i可以拆分得到的最大乘积
dp[2] = 1;
for(int i = 3;i<=n;i++){
for(int j = 1;j<i;j++){
dp[i] = max(dp[i],max(dp[i-j]*j,(i-j)*j));
}
}
return dp[n];
}
};
63. 不同路径 II
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish”)。
现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
网格中的障碍物和空位置分别用 1 和 0 来表示。
示例 1:
输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]
输出:2
解释:3x3 网格的正中间有一个障碍物。
从左上角到右下角一共有 2 条不同的路径:
- 向右 -> 向右 -> 向下 -> 向下
- 向下 -> 向下 -> 向右 -> 向右
示例 2:
输入:obstacleGrid = [[0,1],[0,0]]
输出:1
提示:
m == obstacleGrid.length
n == obstacleGrid[i].length
1 <= m, n <= 100
obstacleGrid[i][j] 为 0 或 1
解法1:动态规划
class Solution {
public:
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
int m = obstacleGrid.size(), n = obstacleGrid[0].size();
vector<vector<int>> dp(m,vector<int>(n,0));
for(int i = 0;i<n && obstacleGrid[0][i] == 0;i++) dp[0][i] = 1;
for(int i = 0;i<m && obstacleGrid[i][0] == 0;i++) dp[i][0] = 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];
}
};
1414. 和为 K 的最少斐波那契数字数目
力扣链接
给你数字 k ,请你返回和为 k 的斐波那契数字的最少数目,其中,每个斐波那契数字都可以被使用多次。
斐波那契数字定义为:
F1 = 1
F2 = 1
Fn = Fn-1 + Fn-2 , 其中 n > 2 。
数据保证对于给定的 k ,一定能找到可行解。
示例 1:
输入:k = 7
输出:2
解释:斐波那契数字为:1,1,2,3,5,8,13,……
对于 k = 7 ,我们可以得到 2 + 5 = 7 。
示例 2:
输入:k = 10
输出:2
解释:对于 k = 10 ,我们可以得到 2 + 8 = 10 。
示例 3:
输入:k = 19
输出:3
解释:对于 k = 19 ,我们可以得到 1 + 5 + 13 = 19 。
提示:
1 <= k <= 10^9
贪心+动态规划
思路:
首先找到所有不超过 k 的斐波那契数字,然后每次贪心地选取不超过 k 的最大斐波那契数字,将 k 减去该斐波那契数字,重复该操作直到 k 变为 0,此时选取的斐波那契数字满足和为 k 且数目最少。
证明见题解,挺难的
代码:
class Solution {
public:
int findMinFibonacciNumbers(int k) {
vector<int> vec;
int a = 1, b = 1;
vec.push_back(a);
vec.push_back(b);
while(a + b <= k){
int c = a+b;
vec.push_back(c);
a = b;
b = c;
}
int result = 0;
for(int i = vec.size() - 1;i>=0 && k>0;i--){
if(k>=vec[i]){
k-=vec[i];
result++;
}
}
return result;
}
};
复杂度分析:
时间复杂度:O(logk),其中 k 为给定的整数。需要找到所有不超过 k 的斐波那契数字,然后计算和为 k 的最少斐波那契数字数目,不超过 k 的斐波那契数字的个数是 O(logk) 个。
空间复杂度:O(logk),其中 k 为给定的整数。需要 O(logk) 的空间存储所有不超过 k 的斐波那契数字。
相似题目: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
解法1:动态规划
思路:
(1)对于动规,如果没有方法论的话,可能简单题目可以顺手一写就过,难一点就不知道如何下手了。
所以总结的动规五部曲,是要用来贯穿整个动态规划系列的,就像之前讲过二叉树系列的递归三部曲 (opens new window),回溯法系列的回溯三部曲 (opens new window)一样。后面慢慢大家就会体会到,动规五部曲方法的重要性。
动态规划:
动规五部曲:
这里我们要用一个一维dp数组来保存递归的结果
1.确定dp数组以及下标的含义
dp[i]的定义为:第i个数的斐波那契数值是dp[i]
2.确定递推公式
为什么这是一道非常简单的入门题目呢?
因为题目已经把递推公式直接给我们了:状态转移方程 dp[i] = dp[i - 1] + dp[i - 2];
3.dp数组如何初始化
题目中把如何初始化也直接给我们了,如下:
dp[0] = 0;
dp[1] = 1;
4.确定遍历顺序
从递归公式dp[i] = dp[i - 1] + dp[i - 2];中可以看出,dp[i]是依赖 dp[i - 1] 和 dp[i - 2],那么遍历的顺序一定是从前到后遍历的
5.举例推导dp数组
按照这个递推公式dp[i] = dp[i - 1] + dp[i - 2],我们来推导一下,当N为10的时候,dp数组应该是如下的数列:
0 1 1 2 3 5 8 13 21 34 55
如果代码写出来,发现结果不对,就把dp数组打印出来看看和我们推导的数列是不是一致的。
以上我们用动规的方法分析完了,C++代码如下:
class Solution {
public:
int fib(int N) {
if (N <= 1) return N;
vector<int> dp(N + 1);
dp[0] = 0;
dp[1] = 1;
for (int i = 2; i <= N; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[N];
}
};
时间复杂度:
O
(
n
)
O(n)
O(n)
空间复杂度:
O
(
n
)
O(n)
O(n)
当然可以发现,我们只需要维护两个数值就可以了,不需要记录整个序列。
代码如下:
class Solution {
public:
int fib(int N) {
if (N <= 1) return N;
int dp[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];
}
};
91. 解码方法
力扣链接
一条包含字母 A-Z 的消息通过以下映射进行了 编码 :
‘A’ -> “1”
‘B’ -> “2”
…
‘Z’ -> “26”
要 解码 已编码的消息,所有数字必须基于上述映射的方法,反向映射回字母(可能有多种方法)。例如,“11106” 可以映射为:
“AAJF” ,将消息分组为 (1 1 10 6)
“KJF” ,将消息分组为 (11 10 6)
注意,消息不能分组为 (1 11 06) ,因为 “06” 不能映射为 “F” ,这是由于 “6” 和 “06” 在映射中并不等价。
给你一个只含数字的 非空 字符串 s ,请计算并返回 解码 方法的 总数 。
题目数据保证答案肯定是一个 32 位 的整数。
示例 1:
输入:s = “12”
输出:2
解释:它可以解码为 “AB”(1 2)或者 “L”(12)。
示例 2:
输入:s = “226”
输出:3
解释:它可以解码为 “BZ” (2 26), “VF” (22 6), 或者 “BBF” (2 2 6) 。
示例 3:
输入:s = “0”
输出:0
解释:没有字符映射到以 0 开头的数字。
含有 0 的有效映射是 ‘J’ -> “10” 和 ‘T’-> “20” 。
由于没有字符,因此没有有效的方法对此进行解码,因为所有数字都需要映射。
提示:
1 <= s.length <= 100
s 只包含数字,并且可能包含前导零。
解法1:动态规划
思路:
定义dp[i]表示前i个字符的解码数。
如果要求前i个字符的解码数
1.我们可以先求前i-1个字符的解码数,但前提条件是当前字符也可以解码(一个字符的话,只要不是0,都可以)
2.还可以求前i-2个字符的解码数,但前提条件是当前字符和前一个字符构成的两个数字是有效的。
看到这里大家应该已经明白了,如果没有条件限制的话,这题解法和爬楼梯完全一样,递归公式其实就是个斐波那契数列
dp[i]=dp[i-1]+dp[i-2]
只不过这里都有条件限制,但原理都差不多,我们只需要根据条件来判断哪一项该加,哪一项不该加
dp[0] = 1 表示空字符串可以有 1 种解码方法,解码出一个空字符串。
代码:
class Solution {
public:
int numDecodings(string s) {
int size = s.size();
vector<int> dp(size+1);
dp[0] = 1;
for(int i = 1;i<=size;i++){
if(s[i-1] != '0'){
dp[i] = dp[i-1];
}
if(i >= 2 && (s[i-2] == '1' || (s[i-2] == '2' && s[i-1] <= '6'))){
dp[i] += dp[i-2];
}
}
return dp[size];
}
};
优化:
class Solution {
public:
int numDecodings(string s) {
int size = s.size();
// a = f[i-2], b = f[i-1], c = f[i]
int a = 0, b = 1, c;
for(int i = 1;i<=size;i++){
c = 0;
if(s[i-1] != '0'){
c += b;
}
if(i >= 2 && (s[i-2] == '1' || (s[i-2] == '2' && s[i-1] <= '6'))){
c += a;
}
a = b;
b = c;
}
return c;
}
};
62. 不同路径
力扣链接
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
示例 1:
输入:m = 3, n = 7
输出:28
示例 2:
输入:m = 3, n = 2
输出:3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。
- 向右 -> 向下 -> 向下
- 向下 -> 向下 -> 向右
- 向下 -> 向右 -> 向下
示例 3:
输入:m = 7, n = 3
输出:28
示例 4:
输入:m = 3, n = 3
输出:6
提示:
1 <= m, n <= 100
题目数据保证答案小于等于 2 * 109
解法1:动态规划
思路:
-
确定dp数组(dp table)以及下标的含义
dp[i][j] :表示从(0 ,0)出发,到(i, j) 有dp[i][j]条不同的路径。 -
确定递推公式
想要求dp[i][j],只能有两个方向来推导出来,即dp[i - 1][j] 和 dp[i][j - 1]。
代码:
class Solution {
public:
int uniquePaths(int m, int n) {
vector<vector<int>> dp(m, vector<int>(n, 0));
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];
}
};
时间复杂度:
O
(
m
×
n
)
O(m × n)
O(m×n)
空间复杂度:
O
(
m
×
n
)
O(m × n)
O(m×n)
优化:
class Solution {
public:
int uniquePaths(int m, int n) {
vector<int> dp(n+1);
for(int i = 0;i<m;i++){
for(int j = 0;j<n;j++){
if(i == 0) dp[j+1] = 1;
else if(j == 0) dp[j+1] = 1;
else dp[j+1] += dp[j];
}
}
return dp[n];
}
};
相似题目:剑指 Offer 47. 礼物的最大价值
力扣链接
在一个 m*n 的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于 0)。你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格、直到到达棋盘的右下角。给定一个棋盘及其上面的礼物的价值,请计算你最多能拿到多少价值的礼物?
示例 1:
输入:
[
[1,3,1],
[1,5,1],
[4,2,1]
]
输出: 12
解释: 路径 1→3→5→2→1 可以拿到最多价值的礼物
提示:
0 < grid.length <= 200
0 < grid[0].length <= 200
解法1:动态规划
代码:
class Solution {
public:
int maxValue(vector<vector<int>>& grid) {
int m = grid.size(), n = grid[0].size();
vector<vector<int>> dp(m,vector<int>(n,0));
dp[0][0] = grid[0][0];
for(int i = 1;i<m;i++){
dp[i][0] = dp[i-1][0] + grid[i][0];
}
for(int j = 1;j<n;j++){
dp[0][j] = dp[0][j-1] + grid[0][j];
}
for(int i = 1;i<m;i++){
for(int j = 1;j<n;j++){
dp[i][j] = max(dp[i-1][j] + grid[i][j], dp[i][j-1] + grid[i][j]);
}
}
return dp[m-1][n-1];
}
};
优化:
class Solution {
public:
int maxValue(vector<vector<int>>& grid) {
int m = grid.size(), n = grid[0].size();
vector<int> dp(n+1);
for(int i = 0;i<m;i++){
for(int j = 0;j<n;j++){
dp[j+1] = max(dp[j],dp[j+1]) + grid[i][j];
}
}
return dp[n];
}
};
403. 青蛙过河
力扣链接
一只青蛙想要过河。 假定河流被等分为若干个单元格,并且在每一个单元格内都有可能放有一块石子(也有可能没有)。 青蛙可以跳上石子,但是不可以跳入水中。
给你石子的位置列表 stones(用单元格序号 升序 表示), 请判定青蛙能否成功过河(即能否在最后一步跳至最后一块石子上)。开始时, 青蛙默认已站在第一块石子上,并可以假定它第一步只能跳跃 1 个单位(即只能从单元格 1 跳至单元格 2 )。
如果青蛙上一步跳跃了 k 个单位,那么它接下来的跳跃距离只能选择为 k - 1、k 或 k + 1 个单位。 另请注意,青蛙只能向前方(终点的方向)跳跃。
示例 1:
输入:stones = [0,1,3,5,6,8,12,17]
输出:true
解释:青蛙可以成功过河,按照如下方案跳跃:跳 1 个单位到第 2 块石子, 然后跳 2 个单位到第 3 块石子, 接着 跳 2 个单位到第 4 块石子, 然后跳 3 个单位到第 6 块石子, 跳 4 个单位到第 7 块石子, 最后,跳 5 个单位到第 8 个石子(即最后一块石子)。
示例 2:
输入:stones = [0,1,2,3,4,8,9,11]
输出:false
解释:这是因为第 5 和第 6 个石子之间的间距太大,没有可选的方案供青蛙跳跃过去。
提示:
2 <= stones.length <= 2000
0 <= stones[i] <= 231 - 1
stones[0] == 0
stones 按严格升序排列
解法1:动态规划
思路:
代码:
class Solution {
public:
bool canCross(vector<int>& stones) {
//dp
int n = stones.size();
if(stones[1] != 1) return false;
vector<vector<bool>> dp(n,vector<bool>(n)); //动规数组,其中行表示对应石子的编号,列表示上一跳的距离
dp[0][0] = true;
for(int i = 1;i<n;i++){
for(int j = 0;j<i;j++){
int k = stones[i] - stones[j];
if(k <= j+1){ // 相邻石子的距离大于其编号的时候,必然跳不过去(存在一个距离递增的关系)
dp[i][k] = dp[j][k-1] || dp[j][k] || dp[j][k+1];
if(i == n - 1 && dp[i][k]) return true;
}
}
}
return false;
}
};
978. 最长湍流子数组
力扣链接
给定一个整数数组 arr ,返回 arr 的 最大湍流子数组的长度 。
如果比较符号在子数组中的每个相邻元素对之间翻转,则该子数组是 湍流子数组 。
更正式地来说,当 arr 的子数组 A[i], A[i+1], …, A[j] 满足仅满足下列条件时,我们称其为湍流子数组:
若 i <= k < j :
当 k 为奇数时, A[k] > A[k+1],且
当 k 为偶数时,A[k] < A[k+1];
或 若 i <= k < j :
当 k 为偶数时,A[k] > A[k+1] ,且
当 k 为奇数时, A[k] < A[k+1]。
示例 1:
输入:arr = [9,4,2,10,7,8,8,1,9]
输出:5
解释:arr[1] > arr[2] < arr[3] > arr[4] < arr[5]
示例 2:
输入:arr = [4,8,12,16]
输出:2
示例 3:
输入:arr = [100]
输出:1
提示:
1 <= arr.length <= 4 * 104
0 <= arr[i] <= 109
解法1:动态规划
思路:
这题应该算是376. 摆动序列的变形,只不过此题要求连续,所以当出现up或者down的时候,对应的down或者up要恢复到初始状态。
代码:
class Solution {
public:
int maxTurbulenceSize(vector<int>& arr) {
int n = arr.size();
int res = 1;
int up = 1, down = 1;
for(int i = 1;i<n;i++){
if(arr[i] > arr[i-1]) {
up = down + 1;
down = 1;
}else if(arr[i] < arr[i-1]){
down = up + 1;
up = 1;
}else{
up = 1;
down = 1;
}
res = max(res,max(up,down));
}
return res;
}
};
相似题目:376. 摆动序列
力扣链接
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为 摆动序列 。第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。
例如, [1, 7, 4, 9, 2, 5] 是一个 摆动序列 ,因为差值 (6, -3, 5, -7, 3) 是正负交替出现的。
相反,[1, 4, 7, 2, 5] 和 [1, 7, 4, 5, 5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。
子序列 可以通过从原始序列中删除一些(也可以不删除)元素来获得,剩下的元素保持其原始顺序。
给你一个整数数组 nums ,返回 nums 中作为 摆动序列 的 最长子序列的长度 。
示例 1:
输入:nums = [1,7,4,9,2,5]
输出:6
解释:整个序列均为摆动序列,各元素之间的差值为 (6, -3, 5, -7, 3) 。
示例 2:
输入:nums = [1,17,5,10,13,15,10,5,16,8]
输出:7
解释:这个序列包含几个长度为 7 摆动序列。
其中一个是 [1, 17, 10, 13, 10, 16, 8] ,各元素之间的差值为 (16, -7, 3, -3, 6, -8) 。
示例 3:
输入:nums = [1,2,3,4,5,6,7,8,9]
输出:2
提示:
1 <= nums.length <= 1000
0 <= nums[i] <= 1000
进阶:你能否用 O(n) 时间复杂度完成此题?
解法1:动态规划
class Solution {
public:
int wiggleMaxLength(vector<int>& nums) {
int n = nums.size();
int res = 1;
int up = 1, down = 1;
for(int i = 1;i<n;i++){
if(nums[i] > nums[i-1]) up = down + 1;
else if(nums[i] < nums[i-1]) down = up + 1;
res = max(res,max(up,down));
}
return res;
}
};
解法2:动态规划
class Solution {
public:
int wiggleMaxLength(vector<int>& nums) {
if (nums.size() <= 1) return nums.size();
int curDiff = 0;//当前峰值
int preDiff = 0;//之前峰值
int result = 1;
for (int i = 0;i<nums.size()-1;i++){
curDiff = nums[i+1] - nums[i];
if ((curDiff >0 && preDiff<=0)||(curDiff <0 && preDiff>=0)){
result++;
preDiff = curDiff;
}
}
return result;
}
};
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) 的解法,尝试使用更为精妙的 分治法 求解。
解法1:动态规划
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int n = nums.size();
vector<int> dp(2,0);
dp[0] = nums[0];
int res = dp[0];
for(int i = 1;i<n;i++){
dp[i%2] = max(dp[(i-1)%2]+nums[i],nums[i]);
res = max(res,dp[i%2]);
}
return res;
}
};
32. 最长有效括号
力扣链接
给你一个只包含 ‘(’ 和 ‘)’ 的字符串,找出最长有效(格式正确且连续)括号子串的长度。
示例 1:
输入:s = “(()”
输出:2
解释:最长有效括号子串是 “()”
示例 2:
输入:s = “)()())”
输出:4
解释:最长有效括号子串是 “()()”
示例 3:
输入:s = “”
输出:0
提示:
0 <= s.length <= 3 * 104
s[i] 为 ‘(’ 或 ‘)’
解法1:动态规划
思路:
class Solution {
public:
int longestValidParentheses(string s) {
int n = s.size();
vector<int> dp(n,0);
int result = 0;
for(int i = 1;i<n;i++){
if(s[i] == ')'){
if(s[i-1] == '('){
dp[i] = 2;
if(i-2>=0){
dp[i] = dp[i-2] + 2;
}
}else if(dp[i-1] > 0){//即s[i-1] == ')'
if(i-dp[i-1]-1>=0 && s[i-dp[i-1]-1] == '('){
dp[i] = dp[i-1] + 2;
if(i-dp[i-1]-2>=0){
dp[i] = dp[i-1] + dp[i-dp[i-1]-2] + 2;
}
}
}
}
result = max(result,dp[i]);
}
return result;
}
};
解法2:不需要额外的空间
class Solution {
public:
int longestValidParentheses(string s) {
int left = 0, right = 0, maxLen = 0;
int n = s.size();
for(int i = 0;i<n;i++){
if(s[i] == '(') left++;
if(s[i] == ')') right++;
if(left == right) maxLen = max(maxLen, right*2);
else if(left < right) left = right = 0;
}
left = 0, right = 0;
for(int i = n-1;i>=0;i--){
if(s[i] == '(') left++;
if(s[i] == ')') right++;
if(left == right) maxLen = max(maxLen, right*2);
else if(left > right) left = right = 0;
}
return maxLen;
}
};
221. 最大正方形
力扣链接
在一个由 ‘0’ 和 ‘1’ 组成的二维矩阵内,找到只包含 ‘1’ 的最大正方形,并返回其面积。
示例 1:
输入:matrix = [[“1”,“0”,“1”,“0”,“0”],[“1”,“0”,“1”,“1”,“1”],[“1”,“1”,“1”,“1”,“1”],[“1”,“0”,“0”,“1”,“0”]]
输出:4
示例 2:
输入:matrix = [[“0”,“1”],[“1”,“0”]]
输出:1
示例 3:
输入:matrix = [[“0”]]
输出:0
提示:
m == matrix.length
n == matrix[i].length
1 <= m, n <= 300
matrix[i][j] 为 ‘0’ 或 ‘1’
解法1:动态规划
我们用dp(i,j) 表示以(i,j) 为右下角,且只包含 1 的正方形的边长最大值。
class Solution {
public:
int maximalSquare(vector<vector<char>>& matrix) {
int m = matrix.size(), n = matrix[0].size();
vector<vector<int>> dp(m+1,vector<int>(n+1,0));
int res = 0;
for(int i = 1;i<=m;i++){
for(int j = 1;j<=n;j++){
if(matrix[i-1][j-1] == '1'){
dp[i][j] = 1 + min(dp[i-1][j-1],min(dp[i-1][j],dp[i][j-1]));
res = max(res,dp[i][j]);
}
}
}
return res*res;
}
};
……………………背包问题………………
……………………背包问题:01背包………………
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
解法1:动态规划:01背包
思路
本题物品的重量为store[i],物品的价值也为store[i]。
对应着01背包里的物品重量weight[i]和 物品价值value[i]。
确定dp数组以及下标的含义
dp[j]表示容量(这里说容量更形象,其实就是重量)为j的背包,最多可以背dp[j]这么重的石头。
确定递推公式
01背包的递推公式为:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
本题则是:dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
代码:
class Solution {
public:
int lastStoneWeightII(vector<int>& stones) {
int sum = 0;
for(int stone:stones){
sum += stone;
}
int target = sum / 2;
vector<int> dp(target+1,0);//dp[i]表示容量为i的背包,最多可以被b[i]重量的石头
for(int i = 0;i<stones.size();i++){
for(int j = target;j>=stones[i];j--){
dp[j] = max(dp[j],dp[j-stones[i]]+stones[i]);
}
}
return sum - dp[target]*2;
}
};
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
解法1:动态规划:01背包
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
int sum = 0;
for(int num:nums) sum+=num;
if((target + sum) % 2 == 1) return 0;
int bagSize = (target + sum) / 2;
if(bagSize < 0) return 0;
vector<int> dp(bagSize+1,0);
dp[0] = 1;
for(int i = 0;i<nums.size();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
解法1:
class Solution {
public:
int findMaxForm(vector<string>& strs, int m, int n) {
vector<vector<int>> dp(m+1,vector<int>(n+1,0));//最多有i个0 j个1的最大子集的大小为dp[i][j]
for(int k = 0;k<strs.size();k++){
int zeroNum = 0, oneNum = 0;
for(char c:strs[k]){
if(c == '1') oneNum++;
else zeroNum++;
}
for(int i = m;i>=zeroNum;i--){
for(int j = n;j>=oneNum;j--){
dp[i][j] = max(dp[i][j],dp[i-zeroNum][j-oneNum]+1);
}
}
}
return dp[m][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
解法1:完全背包
思路
确定遍历顺序
本题求钱币最小个数,那么钱币有顺序和没有顺序都可以,都不影响钱币的最小个数。
所以本题并不强调集合是组合还是排列。
如果求组合数就是外层for循环遍历物品,内层for遍历背包。
如果求排列数就是外层for遍历背包,内层for循环遍历物品。
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
//完全背包
vector<int> dp(amount+1,INT_MAX);
dp[0] = 0;
for(int i = 0;i<coins.size();i++){
for(int j = coins[i];j<=amount;j++){
if(dp[j-coins[i]] != INT_MAX) dp[j] = min(dp[j],dp[j-coins[i]]+1);
}
}
if(dp[amount] == INT_MAX) return -1;
return dp[amount];
}
};
求组合数
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
解法1:完全背包
class Solution {
public:
int change(int amount, vector<int>& coins) {
//完全背包 求组合数
vector<int> dp(amount+1,0);//容量为i的背包可以凑成的最大金额
dp[0] = 1;
for(int i = 0;i<coins.size();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
进阶:如果给定的数组中含有负数会发生什么?问题会产生何种变化?如果允许负数出现,需要向题目中添加哪些限制条件?
解法1:动态规划
思路:
本题题目描述说是求组合,但又说是可以元素相同顺序不同的组合算两个组合,其实就是求排列!
弄清什么是组合,什么是排列很重要。组合不强调顺序,(1,5)和(5,1)是同一个组合。排列强调顺序,(1,5)和(5,1)是两个不同的排列。
但其本质是本题求的是排列总和,而且仅仅是求排列总和的个数,并不是把所有的排列都列出来。
如果本题要把排列都列出来的话,只能使用回溯算法爆搜。
动规五部曲分析如下:
-
确定dp数组以及下标的含义
dp[i]: 凑成目标正整数为i的排列个数为dp[i] -
确定递推公式
dp[i](考虑nums[j])可以由 dp[i - nums[j]](不考虑nums[j]) 推导出来。
因为只要得到nums[j],排列个数dp[i - nums[j]],就是dp[i]的一部分。
在动态规划:494.目标和 (opens new window)和 动态规划:518.零钱兑换II (opens new window)中我们已经讲过了,求装满背包有几种方法,递推公式一般都是dp[i] += dp[i - nums[j]];
本题也一样。
- dp数组如何初始化
因为递推公式dp[i] += dp[i - nums[j]]的缘故,dp[0]要初始化为1,这样递归其他dp[i]的时候才会有数值基础。
至于dp[0] = 1 有没有意义呢?
其实没有意义,所以我也不去强行解释它的意义了,因为题目中也说了:给定目标值是正整数! 所以dp[0] = 1是没有意义的,仅仅是为了推导递推公式。
至于非0下标的dp[i]应该初始为多少呢?
初始化为0,这样才不会影响dp[i]累加所有的dp[i - nums[j]]。
- 确定遍历顺序
个数可以不限使用,说明这是一个完全背包。
得到的集合是排列,说明需要考虑元素之间的顺序。
本题要求的是排列,那么这个for循环嵌套的顺序可以有说法了。
接下来讲一下背包问题的判定
背包问题具备的特征:给定一个target,target可以是数字也可以是字符串,再给定一个数组nums,nums中装的可能是数字,也可能是字符串,问:能否使用nums中的元素做各种排列组合得到target。
背包问题技巧:
1.如果是0-1背包,即数组中的元素不可重复使用,nums(物品)放在外循环,target(背包)在内循环,且内循环倒序;
for num in nums:
for i in range(target, nums-1, -1):
2.如果是完全背包,即数组中的元素可重复使用,nums放在外循环,target在内循环。且内循环正序。
for num in nums:
for i in range(nums, target+1):
3.如果组合问题需考虑元素之间的顺序,需将target放在外循环,将nums放在内循环。
for i in range(1, target+1):
for num in nums:
代码:
class Solution {
public:
int combinationSum4(vector<int>& nums, int target) {
vector<int> dp(target+1,0);
dp[0] = 1;
for(int i = 0;i<=target;i++){
for(int j = 0;j<nums.size();j++){
if(i >= nums[j] && dp[i] < INT_MAX - dp[i-nums[j]]){
dp[i] += dp[i-nums[j]];
}
}
}
return dp[target];
}
};
C++测试用例有两个数相加超过int的数据,所以需要在if里加上dp[i] < INT_MAX - dp[i - num]。
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
解法1:完全背包
dp[j]:和为j的完全平方数的最少数量为dp[j]
class Solution {
public:
int numSquares(int n) {
vector<int> dp(n+1,INT_MAX);
dp[0] = 0;
for(int i = 1;i*i<=n;i++){//物品
for(int j = i*i;j<=n;j++){
dp[j] = min(dp[j],dp[j-i*i]+1);
}
}
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 中的所有字符串 互不相同
解法1:动态规划-完全背包
思路:
单词就是物品,字符串s就是背包,单词能否组成字符串s,就是问物品能不能把背包装满。
拆分时可以重复使用字典中的单词,说明就是一个完全背包!
动规五部曲分析如下:
-
确定dp数组以及下标的含义
dp[i] : 字符串长度为i的话,dp[i]为true,表示可以拆分为一个或多个在字典中出现的单词。 -
确定递推公式
如果确定dp[j] 是true,且 [j, i] 这个区间的子串出现在字典里,那么dp[i]一定是true。(j < i )。
所以递推公式是 if([j, i] 这个区间的子串出现在字典里 && dp[j]是true) 那么 dp[i] = true。
- dp数组如何初始化
从递归公式中可以看出,dp[i] 的状态依靠 dp[j]是否为true,那么dp[0]就是递归的根基,dp[0]一定要为true,否则递归下去后面都都是false了。
那么dp[0]有没有意义呢?
dp[0]表示如果字符串为空的话,说明出现在字典里。
但题目中说了“给定一个非空字符串 s” 所以测试数据中不会出现i为0的情况,那么dp[0]初始为true完全就是为了推导公式。
下标非0的dp[i]初始化为false,只要没有被覆盖说明都是不可拆分为一个或多个在字典中出现的单词。
- 确定遍历顺序
题目中说是拆分为一个或多个在字典中出现的单词,所以这是完全背包。
还要讨论两层for循环的前后循序。
如果求组合数就是外层for循环遍历物品,内层for遍历背包。
如果求排列数就是外层for遍历背包,内层for循环遍历物品。
代码:
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
unordered_set<string> wordSet(wordDict.begin(), wordDict.end());
vector<bool> dp(s.size() + 1, false);
dp[0] = true;
for (int i = 1; i <= s.size(); i++) { // 遍历背包
for (int j = 0; j < i; j++) { // 遍历物品
string word = s.substr(j, i - j); //substr(起始位置,截取的个数)
if (wordSet.find(word) != wordSet.end() && dp[j]) {
dp[i] = true;
}
}
}
return dp[s.size()];
}
};
解法2:记忆化回溯
思路:
递归的过程中有很多重复计算,可以使用数组保存一下递归过程中计算的结果。
这个叫做记忆化递归,这种方法我们之前已经提过很多次了。
使用memory数组保存每次计算的以startIndex起始的计算结果,如果memory[startIndex]里已经被赋值了,直接用memory[startIndex]的结果。
代码:
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
//记忆化 回溯
unordered_set<string> wordSet(wordDict.begin(),wordDict.end());
vector<bool> mem(s.size(),true);
return dfs(s,wordSet,0,mem);
}
bool dfs(const string& s, const unordered_set<string> wordSet, int startIndex, vector<bool>& mem){
if(startIndex >= s.size()) return true;
if(!mem[startIndex]) return mem[startIndex];
for(int i = startIndex;i<s.size();i++){
string word = s.substr(startIndex,i-startIndex+1);
if(wordSet.find(word)!=wordSet.end() && dfs(s,wordSet,i+1,mem)) return true;
}
mem[startIndex] = false;
return false;
}
};
时间复杂度:O(2^n),因为每一个单词都有两个状态,切割和不切割
空间复杂度:O(n),算法递归系统调用栈的空间
相似题目:140. 单词拆分 II
力扣链接
给定一个字符串 s 和一个字符串字典 wordDict ,在字符串 s 中增加空格来构建一个句子,使得句子中所有的单词都在词典中。以任意顺序 返回所有这些可能的句子。
注意:词典中的同一个单词可能在分段中被重复使用多次。
示例 1:
输入:s = “catsanddog”, wordDict = [“cat”,“cats”,“and”,“sand”,“dog”]
输出:[“cats and dog”,“cat sand dog”]
示例 2:
输入:s = “pineapplepenapple”, wordDict = [“apple”,“pen”,“applepen”,“pine”,“pineapple”]
输出:[“pine apple pen apple”,“pineapple pen apple”,“pine applepen apple”]
解释: 注意你可以重复使用字典中的单词。
示例 3:
输入:s = “catsandog”, wordDict = [“cats”,“dog”,“sand”,“and”,“cat”]
输出:[]
提示:
1 <= s.length <= 20
1 <= wordDict.length <= 1000
1 <= wordDict[i].length <= 10
s 和 wordDict[i] 仅有小写英文字母组成
wordDict 中所有字符串都 不同
解法1:记忆化回溯
思路:
回溯+剪枝,利用一个map保留键值映射,想相当于加入剪枝操作,可以对之前计算过的避免重复计算,进而加速计算过程。
代码:
class Solution {
public:
vector<string> wordBreak(string s, vector<string>& wordDict) {
unordered_map<int,vector<string>> mem;
unordered_set<string> uset(wordDict.begin(),wordDict.end());
dfs(s,mem,uset,0);
return mem[0];
}
void dfs(string& s, unordered_map<int,vector<string>>& mem, unordered_set<string>& uset, int startIndex){
if(startIndex >= s.size()){
mem[startIndex] = {""};
return;
}
if(mem.count(startIndex)) return;
mem[startIndex] = {};
for(int i = startIndex;i<s.size();i++){
string word = s.substr(startIndex, i-startIndex+1);
if(uset.find(word) != uset.end()){
dfs(s,mem,uset,i+1);
for(string& ss:mem[i+1]){
mem[startIndex].push_back(ss.empty()?word:word + " " + ss);
}
}
}
}
};
……………………打家劫舍………………
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
解法1:动态规划
思路:
打家劫舍是dp解决的经典问题,动规五部曲分析如下:
-
确定dp数组(dp table)以及下标的含义
dp[i]:考虑下标i(包括i)以内的房屋,最多可以偷窃的金额为dp[i]。 -
确定递推公式
决定dp[i]的因素就是第i房间偷还是不偷。
如果偷第i房间,那么dp[i] = dp[i - 2] + nums[i] ,即:第i-1房一定是不考虑的,找出 下标i-2(包括i-2)以内的房屋,最多可以偷窃的金额为dp[i-2] 加上第i房间偷到的钱。
如果不偷第i房间,那么dp[i] = dp[i - 1],即考虑i-1房,(注意这里是考虑,并不是一定要偷i-1房,这是很多同学容易混淆的点)
然后dp[i]取最大值,即dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
- dp数组如何初始化
从递推公式dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);可以看出,递推公式的基础就是dp[0] 和 dp[1]
从dp[i]的定义上来讲,dp[0] 一定是 nums[0],dp[1]就是nums[0]和nums[1]的最大值即:dp[1] = max(nums[0], nums[1]);
代码如下:
vector<int> dp(nums.size());
dp[0] = nums[0];
dp[1] = max(nums[0], nums[1]);
- 确定遍历顺序
dp[i] 是根据dp[i - 2] 和 dp[i - 1] 推导出来的,那么一定是从前到后遍历!
代码如下:
for (int i = 2; i < nums.size(); i++) {
dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
}
- 举例推导dp数组
以示例二,输入[2,7,9,3,1]为例。
代码:
class Solution {
public:
int rob(vector<int>& nums) {
int n = nums.size();
if(n == 1) return nums[0];
vector<int> dp(n);
dp[0] = nums[0], dp[1] = max(nums[0],nums[1]);
for(int i = 2 ;i<n;i++){
dp[i] = max(dp[i-1],dp[i-2] + nums[i]);
}
return dp[n-1];
}
};
相似题目:740. 删除并获得点数
力扣链接
给你一个整数数组 nums ,你可以对它进行一些操作。
每次操作中,选择任意一个 nums[i] ,删除它并获得 nums[i] 的点数。之后,你必须删除 所有 等于 nums[i] - 1 和 nums[i] + 1 的元素。
开始你拥有 0 个点数。返回你能通过这些操作获得的最大点数。
示例 1:
输入:nums = [3,4,2]
输出:6
解释:
删除 4 获得 4 个点数,因此 3 也被删除。
之后,删除 2 获得 2 个点数。总共获得 6 个点数。
示例 2:
输入:nums = [2,2,3,3,3,4]
输出:9
解释:
删除 3 获得 3 个点数,接着要删除两个 2 和 4 。
之后,再次删除 3 获得 3 个点数,再次删除 3 获得 3 个点数。
总共获得 9 个点数。
提示:
1 <= nums.length <= 2 * 104
1 <= nums[i] <= 104
解法1:动态规划
思路:
遍历原数组,dp保存下标值出现的频数*数值。 dp[i]动态规划,新的dp值在两种情况中选择当前可能的最大值
代码:
class Solution {
public:
int deleteAndEarn(vector<int>& nums) {
int n = nums.size();
vector<int> dp(10001,0);
for(int num:nums){
dp[num]+=num;
}
for(int i = 2;i<10001;i++){
dp[i] = max(dp[i] + dp[i-2], dp[i-1]);
}
return dp[10000];
}
};
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:动态规划
思路:
注意我这里用的是"考虑",例如情况三,虽然是考虑包含尾元素,但不一定要选尾部元素! 对于情况三,取nums[1] 和 nums[3]就是最大的。
而情况二 和 情况三 都包含了情况一了,所以只考虑情况二和情况三就可以了。
分析到这里,本题其实比较简单了。 剩下的和198.打家劫舍 (opens new window)就是一样的了。
代码:
class Solution {
public:
int rob(vector<int>& nums) {
if(nums.size() == 1) return nums[0];
int res1 = robHelper(nums, 0, nums.size()-2);
int res2 = robHelper(nums, 1, nums.size()-1);
return max(res1,res2);
}
int robHelper(vector<int>& nums, int start, int end){
if(start == end) return nums[start];
vector<int> dp(nums.size(),0);
dp[start] = nums[start];
dp[start+1] = max(nums[start], nums[start+1]);
for(int i = start+2;i<=end;i++){
dp[i] = max(dp[i-1],nums[i] + dp[i-2]);
}
return dp[end];
}
};
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
解法1:记忆化递归
思路:
所以可以使用一个map把计算过的结果保存一下,这样如果计算过孙子了,那么计算孩子的时候可以复用孙子节点的结果。
代码:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
unordered_map<TreeNode*, int> umap;// 记录计算过的结果
int rob(TreeNode* root) {
return robHelper(root);
}
int robHelper(TreeNode* cur){
if(!cur) return 0;
if(umap.count(cur)) return umap[cur];// 如果umap里已经有记录则直接返回
//偷该节点
int res1 = cur->val;
if(cur->left) res1 += robHelper(cur->left->left) + robHelper(cur->left->right);
if(cur->right) res1 += robHelper(cur->right->left) + robHelper(cur->right->right);
//
int res2 = robHelper(cur->left) + robHelper(cur->right);
umap[cur] = max(res1,res2);
return umap[cur];
}
};
时间复杂度:
O
(
n
)
O(n)
O(n)
空间复杂度:
O
(
log
n
)
O(\log n)
O(logn),算上递推系统栈的空间
解法2:动态规划
思路:
在上面两种方法,其实对一个节点 偷与不偷得到的最大金钱都没有做记录,而是需要实时计算。
而动态规划其实就是使用状态转移容器来记录状态的变化,这里可以使用一个长度为2的数组,记录当前节点偷与不偷所得到的的最大金钱。
这道题目算是树形dp的入门题目,因为是在树上进行状态转移,我们在讲解二叉树的时候说过递归三部曲,那么下面我以递归三部曲为框架,其中融合动规五部曲的内容来进行讲解。
- 确定递归函数的参数和返回值
这里我们要求一个节点 偷与不偷的两个状态所得到的金钱,那么返回值就是一个长度为2的数组。
参数为当前节点,代码如下:
vector<int> robTree(TreeNode* cur) {
其实这里的返回数组就是dp数组。
所以dp数组(dp table)以及下标的含义:下标为0记录不偷该节点所得到的的最大金钱,下标为1记录偷该节点所得到的的最大金钱。
所以本题dp数组就是一个长度为2的数组!
那么有同学可能疑惑,长度为2的数组怎么标记树中每个节点的状态呢?
别忘了在递归的过程中,系统栈会保存每一层递归的参数。
如果还不理解的话,就接着往下看,看到代码就理解了哈。
-
确定终止条件
在遍历的过程中,如果遇到空节点的话,很明显,无论偷还是不偷都是0,所以就返回if (cur == NULL) return vector{0, 0};
这也相当于dp数组的初始化 -
确定遍历顺序
首先明确的是使用后序遍历。 因为通过递归函数的返回值来做下一步计算。
通过递归左节点,得到左节点偷与不偷的金钱。
通过递归右节点,得到右节点偷与不偷的金钱。
代码如下:
// 下标0:不偷,下标1:偷
vector<int> left = robTree(cur->left); // 左
vector<int> right = robTree(cur->right); // 右
// 中
- 确定单层递归的逻辑
如果是偷当前节点,那么左右孩子就不能偷,val1 = cur->val + left[0] + right[0]; (如果对下标含义不理解就在回顾一下dp数组的含义)
如果不偷当前节点,那么左右孩子就可以偷,至于到底偷不偷一定是选一个最大的,所以:val2 = max(left[0], left[1]) + max(right[0], right[1]);
最后当前节点的状态就是{val2, val1}; 即:{不偷当前节点得到的最大金钱,偷当前节点得到的最大金钱}
代码如下:
vector<int> left = robTree(cur->left); // 左
vector<int> right = robTree(cur->right); // 右
// 偷cur
int val1 = cur->val + left[0] + right[0];
// 不偷cur
int val2 = max(left[0], left[1]) + max(right[0], right[1]);
return {val2, val1};
- 举例推导dp数组
以示例1为例,dp数组状态如下:(注意用后序遍历的方式推导)
代码:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
int rob(TreeNode* root) {
vector<int> res = robHelper(root);
return max(res[0], res[1]);
}
vector<int> robHelper(TreeNode* cur){
//0不偷 1偷
if(cur == nullptr) return {0,0};
vector<int> left = robHelper(cur->left);
vector<int> right = robHelper(cur->right);
//不偷
int res1 = max(left[0],left[1]) + max(right[0],right[1]);
//
int res2 = cur->val + left[0] + right[0];
return {res1,res2};
}
};
……………………股票问题………………
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
解法1:动态规划
思路:
动规五部曲分析如下:
- 确定dp数组(dp table)以及下标的含义
dp[i][0] 表示第i天持有股票所得最多现金 ,这里可能有同学疑惑,本题中只能买卖一次,持有股票之后哪还有现金呢?
其实一开始现金是0,那么加入第i天买入股票现金就是 -prices[i], 这是一个负数。
dp[i][1] 表示第i天不持有股票所得最多现金
注意这里说的是“持有”,“持有”不代表就是当天“买入”!也有可能是昨天就买入了,今天保持持有的状态
很多同学把“持有”和“买入”没分区分清楚。
在下面递推公式分析中,我会进一步讲解。
- 确定递推公式
如果第i天持有股票即dp[i][0], 那么可以由两个状态推出来
第i-1天就持有股票,那么就保持现状,所得现金就是昨天持有股票的所得现金 即:dp[i - 1][0]
第i天买入股票,所得现金就是买入今天的股票后所得现金即:-prices[i]
那么dp[i][0]应该选所得现金最大的,所以dp[i][0] = max(dp[i - 1][0], -prices[i]);
如果第i天不持有股票即dp[i][1], 也可以由两个状态推出来
第i-1天就不持有股票,那么就保持现状,所得现金就是昨天不持有股票的所得现金 即:dp[i - 1][1]
第i天卖出股票,所得现金就是按照今天股票佳价格卖出后所得现金即:prices[i] + dp[i - 1][0]
同样dp[i][1]取最大的,dp[i][1] = max(dp[i - 1][1], prices[i] + dp[i - 1][0]);
这样递归公式我们就分析完了
- dp数组如何初始化
由递推公式 dp[i][0] = max(dp[i - 1][0], -prices[i]); 和 dp[i][1] = max(dp[i - 1][1], prices[i] + dp[i - 1][0]);可以看出
其基础都是要从dp[0][0]和dp[0][1]推导出来。
那么dp[0][0]表示第0天持有股票,此时的持有股票就一定是买入股票了,因为不可能有前一天推出来,所以dp[0][0] -= prices[0];
dp[0][1]表示第0天不持有股票,不持有股票那么现金就是0,所以dp[0][1] = 0;
- 确定遍历顺序
从递推公式可以看出dp[i]都是有dp[i - 1]推导出来的,那么一定是从前向后遍历。
dp[5][1]就是最终结果。
为什么不是dp[5][0]呢?
因为本题中不持有股票状态所得金钱一定比持有股票状态得到的多!
代码:
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n = prices.size();
vector<vector<int>> dp(2,vector<int>(2,0));//dp[i][0]持有 dp[i][1]不持有
dp[0][0] = -prices[0];
dp[0][1] = 0;
for(int i = 1;i<n;i++){
dp[i%2][0] = max(dp[(i-1)%2][0],- prices[i]);
dp[i%2][1] = max(dp[(i-1)%2][1],dp[(i-1)%2][0] + prices[i]);
}
return dp[(n-1)%2][1];
}
};
解法2:贪心
因为股票就买卖一次,那么贪心的想法很自然就是取最左最小值,取最右最大值,那么得到的差值就是最大利润。
class Solution {
public:
int maxProfit(vector<int>& prices) {
//贪心
int low = INT_MAX;
int res = 0;
for(int i = 0;i<prices.size();i++){
low = min(low,prices[i]);
res = max(res,prices[i]-low);
}
return res;
}
};
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
解法1:动态规划
思路:
本题和121. 买卖股票的最佳时机 (opens new window)的唯一区别本题股票可以买卖多次了(注意只有一只股票,所以再次购买前要出售掉之前的股票)
在动规五部曲中,这个区别主要是体现在递推公式上,其他都和121. 买卖股票的最佳时机 (opens new window)一样一样的。
代码:
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n = prices.size();
vector<vector<int>> dp(n,vector<int>(2,0));//0持有 1不
dp[0][0] = -prices[0];
dp[0][1] = 0;
for(int i = 1;i<n;i++){
dp[i][0] = max(dp[i-1][0],dp[i-1][1] - prices[i]);
dp[i][1] = max(dp[i-1][1],dp[i-1][0] + prices[i]);
}
return dp[n-1][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
解法1:动态规划
思路:
详见代码随想录
确定dp数组以及下标的含义
一天一共就有五个状态,
没有操作
第一次买入
第一次卖出
第二次买入
第二次卖出
代码:
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n = prices.size();
vector<int> dp(5,0);
dp[1] = -prices[0];
dp[3] = -prices[0];
for(int i = 1;i<n;i++){
dp[1] = max(dp[1],dp[0]-prices[i]);
dp[2] = max(dp[2],dp[1]+prices[i]);
dp[3] = max(dp[3],dp[2]-prices[i]);
dp[4] = max(dp[4],dp[3]+prices[i]);
}
return dp[4];
}
};
时间复杂度:O(n)
空间复杂度:O(1)
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
解法1:动态规划
思路:
这道题目可以说是动态规划:123.买卖股票的最佳时机III (opens new window)的进阶版,这里要求至多有k次交易。
动规五部曲,分析如下:
- 确定dp数组以及下标的含义
在动态规划:123.买卖股票的最佳时机III (opens new window)中,我是定义了一个二维dp数组,本题其实依然可以用一个二维dp数组。
使用二维数组 dp[i][j] :第i天的状态为j,所剩下的最大现金是dp[i][j]
j的状态表示为:
0 表示不操作
1 第一次买入
2 第一次卖出
3 第二次买入
4 第二次卖出
…
大家应该发现规律了吧 ,除了0以外,偶数就是卖出,奇数就是买入。
题目要求是至多有K笔交易,那么j的范围就定义为 2 * k + 1 就可以了。
所以二维dp数组的C++定义为:
vector<vector> dp(prices.size(), vector(2 * k + 1, 0));
2. 确定递推公式
还要强调一下:dp[i][1],表示的是第i天,买入股票的状态,并不是说一定要第i天买入股票,这是很多同学容易陷入的误区。
达到dp[i][1]状态,有两个具体操作:
操作一:第i天买入股票了,那么dp[i][1] = dp[i - 1][0] - prices[i]
操作二:第i天没有操作,而是沿用前一天买入的状态,即:dp[i][1] = dp[i - 1][1]
选最大的,所以 dp[i][1] = max(dp[i - 1][0] - prices[i], dp[i - 1][1]);
同理dp[i][2]也有两个操作:
操作一:第i天卖出股票了,那么dp[i][2] = dp[i - 1][1] + prices[i]
操作二:第i天没有操作,沿用前一天卖出股票的状态,即:dp[i][2] = dp[i - 1][2]
所以dp[i][2] = max(dp[i - 1][1] + prices[i], dp[i - 1][2])
同理可以类比剩下的状态,代码如下:
for (int j = 0; j < 2 * k - 1; j += 2) {
dp[i][j + 1] = max(dp[i - 1][j + 1], dp[i - 1][j] - prices[i]);
dp[i][j + 2] = max(dp[i - 1][j + 2], dp[i - 1][j + 1] + prices[i]);
}
本题和动态规划:123.买卖股票的最佳时机III (opens new window)最大的区别就是这里要类比j为奇数是买,偶数是卖的状态。
- dp数组如何初始化
第0天没有操作,这个最容易想到,就是0,即:dp[0][0] = 0;
第0天做第一次买入的操作,dp[0][1] = -prices[0];
第0天做第一次卖出的操作,这个初始值应该是多少呢?
首先卖出的操作一定是收获利润,整个股票买卖最差情况也就是没有盈利即全程无操作现金为0,
从递推公式中可以看出每次是取最大值,那么既然是收获利润如果比0还小了就没有必要收获这个利润了。
所以dp[0][2] = 0;
第0天第二次买入操作,初始值应该是多少呢?
不用管第几次,现在手头上没有现金,只要买入,现金就做相应的减少。
第二次买入操作,初始化为:dp[0][3] = -prices[0];
所以同理可以推出dp[0][j]当j为奇数的时候都初始化为 -prices[0]
代码如下:
for (int j = 1; j < 2 * k; j += 2) {
dp[0][j] = -prices[0];
}
在初始化的地方同样要类比j为偶数是卖、奇数是买的状态。
- 确定遍历顺序
从递归公式其实已经可以看出,一定是从前向后遍历,因为dp[i],依靠dp[i - 1]的数值。
代码:
class Solution {
public:
int maxProfit(int k, vector<int>& prices) {
int n = prices.size();
if(n == 0) return 0;
vector<vector<int>> dp(n,vector<int>(2*k+1,0));
//第0天买入
for(int i = 1;i<2*k;i+=2) dp[0][i] = -prices[0];
for(int i = 1;i<n;i++){
for(int j = 0;j<2*k-1;j+=2){
dp[i][j+1] = max(dp[i-1][j+1],dp[i-1][j] - prices[i]);
dp[i][j+2] = max(dp[i-1][j+2],dp[i-1][j+1]+prices[i]);
}
}
return dp[n-1][2*k];
}
};
309. 最佳买卖股票时机含冷冻期
力扣链接
给定一个整数数组prices,其中第 prices[i] 表示第 i 天的股票价格 。
设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
输入: prices = [1,2,3,0,2]
输出: 3
解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]
示例 2:
输入: prices = [1]
输出: 0
解法1:动态规划
思路;
相对于动态规划:122.买卖股票的最佳时机II (opens new window),本题加上了一个冷冻期
在动态规划:122.买卖股票的最佳时机II (opens new window)中有两个状态,持有股票后的最多现金,和不持有股票的最多现金。
动规五部曲,分析如下:
- 确定dp数组以及下标的含义
dp[i][j],第i天状态为j,所剩的最多现金为dp[i][j]。
其实本题很多同学搞的比较懵,是因为出现冷冻期之后,状态其实是比较复杂度,例如今天买入股票、今天卖出股票、今天是冷冻期,都是不能操作股票的。 具体可以区分出如下四个状态:
状态一:买入股票状态(今天买入股票,或者是之前就买入了股票然后没有操作)
卖出股票状态,这里就有两种卖出股票状态:
状态二:两天前就卖出了股票,度过了冷冻期,一直没操作,今天保持卖出股票状态
状态三:今天卖出了股票
状态四:今天为冷冻期状态,但冷冻期状态不可持续,只有一天!
j的状态为:
0:状态一
1:状态二
2:状态三
3:状态四
很多题解为什么讲的比较模糊,是因为把这四个状态合并成三个状态了,其实就是把状态二和状态四合并在一起了。
从代码上来看确实可以合并,但从逻辑上分析合并之后就很难理解了,所以我下面的讲解是按照这四个状态来的,把每一个状态分析清楚。
注意这里的每一个状态,例如状态一,是买入股票状态并不是说今天已经就买入股票,而是说保存买入股票的状态即:可能是前几天买入的,之后一直没操作,所以保持买入股票的状态。
- 确定递推公式
达到买入股票状态(状态一)即:dp[i][0],有两个具体操作:
操作一:前一天就是持有股票状态(状态一),dp[i][0] = dp[i - 1][0]
操作二:今天买入了,有两种情况
- 前一天是冷冻期(状态四),dp[i - 1][3] - prices[i]
- 前一天是保持卖出股票状态(状态二),dp[i - 1][1] - prices[i]
所以操作二取最大值,即:max(dp[i - 1][3], dp[i - 1][1]) - prices[i]
那么dp[i][0] = max(dp[i - 1][0], max(dp[i - 1][3], dp[i - 1][1]) - prices[i]);
达到保持卖出股票状态(状态二)即:dp[i][1],有两个具体操作:
操作一:前一天就是状态二
操作二:前一天是冷冻期(状态四)
dp[i][1] = max(dp[i - 1][1], dp[i - 1][3]);
达到今天就卖出股票状态(状态三),即:dp[i][2] ,只有一个操作:
操作一:昨天一定是买入股票状态(状态一),今天卖出
即:dp[i][2] = dp[i - 1][0] + prices[i];
达到冷冻期状态(状态四),即:dp[i][3],只有一个操作:
操作一:昨天卖出了股票(状态三)
dp[i][3] = dp[i - 1][2];
综上分析,递推代码如下:
dp[i][0] = max(dp[i - 1][0], max(dp[i - 1][3], dp[i - 1][1]) - prices[i]);
dp[i][1] = max(dp[i - 1][1], dp[i - 1][3]);
dp[i][2] = dp[i - 1][0] + prices[i];
dp[i][3] = dp[i - 1][2];
- dp数组如何初始化
这里主要讨论一下第0天如何初始化。
如果是持有股票状态(状态一)那么:dp[0][0] = -prices[0],买入股票所剩现金为负数。
保持卖出股票状态(状态二),第0天没有卖出dp[0][1]初始化为0就行,
今天卖出了股票(状态三),同样dp[0][2]初始化为0,因为最少收益就是0,绝不会是负数。
同理dp[0][3]也初始为0。
- 确定遍历顺序
从递归公式上可以看出,dp[i] 依赖于 dp[i-1],所以是从前向后遍历。
代码:
class Solution {
public:
int maxProfit(vector<int>& prices) {
//0买入 1不持有股票但过了冷 2不持有股票今天刚卖 3冷冻期
int n = prices.size();
if(n == 0) return 0;
vector<vector<int>> dp(n,vector<int>(4,0));
dp[0][0] -= prices[0];
for(int i = 1;i<n;i++){
dp[i][0] = max(dp[i-1][0],max(dp[i-1][1],dp[i-1][3])-prices[i]);
dp[i][1] = max(dp[i-1][3],dp[i-1][1]);
dp[i][2] = dp[i-1][0]+prices[i];
dp[i][3] = dp[i-1][2];
}
return max(dp[n-1][3],max(dp[n-1][2],dp[n-1][1]));
}
};
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
解法1:动态规划
思路:
相对于动态规划:122.买卖股票的最佳时机II (opens new window),本题只需要在计算卖出操作的时候减去手续费就可以了,代码几乎是一样的。
唯一差别在于递推公式部分,所以本篇也就不按照动规五部曲详细讲解了,主要讲解一下递推公式部分。
这里重申一下dp数组的含义:
dp[i][0] 表示第i天持有股票所省最多现金。 dp[i][1] 表示第i天不持有股票所得最多现金
如果第i天持有股票即dp[i][0], 那么可以由两个状态推出来
- 第i-1天就持有股票,那么就保持现状,所得现金就是昨天持有股票的所得现金 即:dp[i - 1][0]
- 第i天买入股票,所得现金就是昨天不持有股票的所得现金减去 今天的股票价格 即:dp[i - 1][1] - prices[i]
所以:dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]);
在来看看如果第i天不持有股票即dp[i][1]的情况, 依然可以由两个状态推出来
- 第i-1天就不持有股票,那么就保持现状,所得现金就是昨天不持有股票的所得现金 即:dp[i - 1][1]
- 第i天卖出股票,所得现金就是按照今天股票价格卖出后所得现金,注意这里需要有手续费了即:dp[i - 1][0] + prices[i] - fee
所以:dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i] - fee);
本题和动态规划:122.买卖股票的最佳时机II (opens new window)的区别就是这里需要多一个减去手续费的操作。
代码:
class Solution {
public:
int maxProfit(vector<int>& prices, int fee) {
//0买入状态 1不持有状态
int n = prices.size();
if(n == 0) return 0;
vector<vector<int>> dp(n,vector<int>(2,0));
dp[0][0] = -prices[0];
for(int i = 1;i<n;i++){
dp[i][0] = max(dp[i-1][0],dp[i-1][1]-prices[i]);
dp[i][1] = max(dp[i-1][1],dp[i-1][0]+prices[i]-fee);
}
return dp[n-1][1];
}
};
……………………子序列问题………………
……………………子序列问题:子序列(不连续)………………
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(n2) 的解决方案吗?
你能将算法的时间复杂度降低到 O(n log(n)) 吗?
解法1:动态规划O(n^2)
思路:
最长上升子序列是动规的经典题目,这里dp[i]是可以根据dp[j] (j < i)推导出来的,那么依然用动规五部曲来分析详细一波:
1.dp[i]的定义
dp[i]表示i之前包括i的最长上升子序列的长度。
2.状态转移方程
位置i的最长升序子序列等于j从0到i-1各个位置的最长升序子序列 + 1 的最大值。
所以:if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1);
注意这里不是要dp[i] 与 dp[j] + 1进行比较,而是我们要取dp[j] + 1的最大值。
3.dp[i]的初始化
每一个i,对应的dp[i](即最长上升子序列)起始大小至少都是1.
4.确定遍历顺序
dp[i] 是有0到i-1各个位置的最长升序子序列 推导而来,那么遍历i一定是从前向后遍历。
j其实就是0到i-1,遍历i的循环在外层,遍历j则在内层,代码如下:
for (int i = 1; i < nums.size(); i++) {
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1);
}
if (dp[i] > result) result = dp[i]; // 取长的子序列
}
5.举例推导dp数组
输入:[0,1,0,3,2],dp数组的变化如下:
代码:
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
if(nums.size() <= 1) return nums.size();
vector<int> dp(nums.size(),1);
int result = 1;
for(int i = 1;i<nums.size();i++){
for(int j = 0;j<i;j++){
if(nums[i]>nums[j]){
dp[i] = max(dp[i],dp[j]+1);
}
}
result = max(result,dp[i]);
}
return result;
}
};
解法2:动态规划+贪心+二分查找 O(n log(n))
思路:
代码:
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
if(nums.size() <= 1) return nums.size();
// tail 数组的定义:长度为 i + 1 的上升子序列的末尾最小是几
vector<int> tail;
// 遍历第 1 个数,直接放在有序数组 tail 的开头
tail.push_back(nums[0]);
// end 表示有序数组 tail 的最后一个已经赋值元素的索引
int end = 0;
for(int i = 1;i < nums.size();i++){
// 【逻辑 1】比 tail 数组实际有效的末尾的那个元素还大
if(nums[i] > tail[end]){
// 直接添加在那个元素的后面,所以 end 先加 1
tail.push_back(nums[i]);
end++;
}else{
// 使用二分查找法,在有序数组 tail 中
// 找到第 1 个大于等于 nums[i] 的元素,尝试让那个元素更小
int l = 0, r = end;
while(l<r){//与寻找左边界一致
int mid = l + (r - l) / 2;
if(tail[mid] < nums[i]){
l = mid + 1;
}else{
r = mid;
}
}
tail[l] = nums[i];
}
}
return end + 1;
}
};
673. 最长递增子序列的个数
力扣链接
给定一个未排序的整数数组 nums , 返回最长递增子序列的个数 。
注意 这个数列必须是 严格 递增的。
示例 1:
输入: [1,3,5,4,7]
输出: 2
解释: 有两个最长递增子序列,分别是 [1, 3, 4, 7] 和[1, 3, 5, 7]。
示例 2:
输入: [2,2,2,2,2]
输出: 5
解释: 最长递增子序列的长度是1,并且存在5个子序列的长度为1,因此输出5。
提示:
1 <= nums.length <= 2000
-106 <= nums[i] <= 106
解法1:动态规划
这道题目我们要一起维护两个数组。
dp[i]:i之前(包括i)最长递增子序列的长度为dp[i]
count[i]:以nums[i]为结尾的字符串,最长递增子序列的个数为count[i]
class Solution {
public:
int findNumberOfLIS(vector<int>& nums) {
int n = nums.size();
vector<int> dp(n,1);//dp[i] i及i之前最长递增子序列长度
vector<int> cnt(n,1);//cnt[i] i及i之前最长递增子序列的个数
int maxLen = 1;
for(int i = 1;i<n;i++){
for(int j = 0;j<i;j++){
if(nums[i] > nums[j]){
if(dp[j]+1 > dp[i]){
cnt[i] = cnt[j];
}else if(dp[j]+1 == dp[i]){
cnt[i] += cnt[j];
}
dp[i] = max(dp[i],dp[j]+1);
}
maxLen = max(maxLen,dp[i]);
}
}
int res = 0;
for(int i = 0;i<nums.size();i++){
if(maxLen == dp[i]) res += cnt[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
解法1:动态规划
class Solution {
public:
int findLengthOfLCIS(vector<int>& nums) {
int n = nums.size();
if(n <= 1) return n;
vector<int> dp(n,1);
int res = 1;
for(int i = 0;i<n-1;i++){
if(nums[i+1] > nums[i]){
dp[i+1] = dp[i] + 1;
}
res = max(res,dp[i+1]);
}
return res;
}
};
解法2:贪心
class Solution {
public:
int findLengthOfLCIS(vector<int>& nums) {
if (nums.size() == 0) return 0;
int result = 1; // 连续子序列最少也是1
int count = 1;
for (int i = 0; i < nums.size() - 1; i++) {
if (nums[i + 1] > nums[i]) { // 连续记录
count++;
} else { // 不连续,count从头开始
count = 1;
}
if (count > result) result = count;
}
return result;
}
};
相似题目:128. 最长连续序列
力扣链接
给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。
请你设计并实现时间复杂度为 O(n) 的算法解决此问题。
示例 1:
输入:nums = [100,4,200,1,3,2]
输出:4
解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。
示例 2:
输入:nums = [0,3,7,2,5,8,4,6,0,1]
输出:9
提示:
0 <= nums.length <= 105
-109 <= nums[i] <= 109
解法1:哈希表
思路:
代码:
class Solution {
public:
int longestConsecutive(vector<int>& nums) {
unordered_set<int> uset;
for(auto& num:nums) uset.insert(num);
int result = 0;
for(auto& x:uset){
if(!uset.count(x-1)){
int y = x;
while(uset.count(y+1)) y++;
result = max(y-x+1,result);
}
}
return result;
}
};
解法2:动态规划
思路:
取出其左右相邻数已有的连续区间长度 left 和 right的隐含意思就是当前加入数字的left和right已经在哈希表中,这也是只需要更新区间端点的原因,注意理解。
hash_dict = {key: value}
这里key很好理解,就是nums里的每一个元素,关键是value的定义。
这里题主所说是对应连续区间的长度, 对此做一点补充说明:
比如key = 5, value = 3,意味着对于5来说,有一个长度为3的连续区间,5属于这个区间(不要纠结5这个元素到底在该区间的哪个位置) 具体来说,这个连续区间可能是[4, 6], [3, 5], [5,7]都是满足定义的,具体看nums里有哪些元素。
ok,理解了hash_dict的定义之后,我们再来看一个很关键的判断:
if num not in hash_dict:
什么意思,即num第一次出现,那么, 对于key=num-1来说,它所对应的value只可能是区间[num - value , num-1]的长度! 即num-1只能是key=num-1对应区间的右端点值!因为其他情况都会与num第一次出现冲突! 同理:对于key=num+1来说,它所对应的value只可能是区间[num+1, num+value]的长度! 即num+1只能是区间的左端点值!
接下来,就好理解了,因为num的出现这两个区间被打通了,出现了一个更长的连续区间[num-hash_dict[num-1], num+ hash_dict[num+1]]~长度为 cur_length = 1 + left + right
最后就是更新这个区间每一个值对应的value,但其实,我们只要更新端点就ok了:
hash_dict[num - left] = cur_length
hash_dict[num + right] = cur_length
代码:
class Solution {
public:
int longestConsecutive(vector<int>& nums) {
unordered_map<int,int> umap;
int leftLen, rightLen, result = 0;
for(auto& num:nums){
if(!umap[num]){
leftLen = umap.count(num-1) ? umap[num-1] : 0;
rightLen = umap.count(num+1) ? umap[num+1] : 0;
result = max(result,leftLen + 1 + rightLen);
umap[num] = leftLen + 1 + rightLen;
umap[num+rightLen] = leftLen + 1 + rightLen;
umap[num-leftLen] = leftLen + 1 + rightLen;
}
}
return result;
}
};
相似题目:354. 俄罗斯套娃信封问题
力扣链接
给你一个二维整数数组 envelopes ,其中 envelopes[i] = [wi, hi] ,表示第 i 个信封的宽度和高度。
当另一个信封的宽度和高度都比这个信封大的时候,这个信封就可以放进另一个信封里,如同俄罗斯套娃一样。
请计算 最多能有多少个 信封能组成一组“俄罗斯套娃”信封(即可以把一个信封放到另一个信封里面)。
注意:不允许旋转信封。
示例 1:
输入:envelopes = [[5,4],[6,4],[6,7],[2,3]]
输出:3
解释:最多信封的个数为 3, 组合为: [2,3] => [5,4] => [6,7]。
示例 2:
输入:envelopes = [[1,1],[1,1],[1,1]]
输出:1
提示:
1 <= envelopes.length <= 105
envelopes[i].length == 2
1 <= wi, hi <= 105
解法1:动态规划+贪心+二分法
思路:
做这道题之前一定要先去把【300.最长递增子序列】这道题弄懂,下面是最长递增子序列的题解:
现在假设你已经理解了最长递增子序列,下面我们就来讲讲俄罗斯套娃信封。
在最长递增子序列中,是要保证缓慢增长,才能增长得更长,本题的套娃信封也是一样的道理,只是多加了一个维度而已。
我们只要保证信封的宽高同时增长得更缓慢,就可以保证能尽可能地套更多的娃。
那么,要怎么保证宽高增长得更缓慢呢?
一种朴素的做法是,先按宽度排序,这样可以保证,按顺序遍历时,宽度这个维度都是满足条件的,我们只要取得高度这个维度的最长递增子序列即可。
但是,如果有多个宽度相同的怎么办呢?
显然,我们要选取高度较小者,这样才能保证整体增长得更缓慢。
所以,我们可以在宽度相同的前提下把高度降序处理,这样相同宽度的高度不会计算到最长递增子序列的结果中。
总得来说,我们需要先对所有信封按宽度升序、高度降序处理,然后,按这个顺序求高度的最长递增子序列即可。
代码与最长递增子序列是非常相似的,一种是动态规划,一种是二分法
代码:
class Solution {
public:
int maxEnvelopes(vector<vector<int>>& envelopes) {
int n = envelopes.size();
if(n <=1) return n;
// 先按宽度升序,再按高度降序
sort(envelopes.begin(),envelopes.end(),[&](const vector<int>& a, const vector<int>& b){
return a[0] < b[0] || (a[0] == b[0] && a[1] > b[1]);
});
vector<int> tail;
tail.push_back(envelopes[0][1]);
int end = 0;
for(int i = 1;i<n;i++){
if(envelopes[i][1] > tail[end]){
tail.push_back(envelopes[i][1]);
end++;
}else{// 二分法寻找envelopes[i][1]应该在tail中的位置
int l = 0, r = end;
while(l<r){
int mid = l + (r-l)/2;
if(tail[mid] < envelopes[i][1]){
l = mid + 1;
}else{
r = mid;
}
}
tail[l] = envelopes[i][1];
}
}
return end + 1;
}
};
解法2:动态规划
LeetCode中总结用动态规划会超时
class Solution {
public int maxEnvelopes(int[][] envelopes) {
int n = envelopes.length;
if (n == 1) {
return 1;
}
// 先按宽度升序,再按高度降序
Arrays.sort(envelopes, (a, b) -> a[0] - b[0] == 0 ? b[1] - a[1] : a[0] - b[0]);
int ans = 0;
// 下面与最长递增子序列的代码是一模一样的了
int[] dp = new int[n];
for (int i = 0; i < n; i++) {
for (int j = 0; j < i; j++) {
if (envelopes[j][1] < envelopes[i][1]) {
dp[i] = Math.max(dp[i], dp[j]);
}
}
ans = Math.max(ans, ++dp[i]);
}
return ans;
}
}
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 仅由小写英文字符组成。
解法1:动态规划+字符串
思路:
动规五部曲分析如下:
- 确定dp数组(dp table)以及下标的含义
dp[i][j]:长度为[0, i - 1]的字符串text1与长度为[0, j - 1]的字符串text2的最长公共子序列为dp[i][j]
有同学会问:为什么要定义长度为[0, i - 1]的字符串text1,定义为长度为[0, i]的字符串text1不香么?
这样定义是为了后面代码实现方便,如果非要定义为为长度为[0, i]的字符串text1也可以,大家可以试一试!
- 确定递推公式
主要就是两大情况: text1[i - 1] 与 text2[j - 1]相同,text1[i - 1] 与 text2[j - 1]不相同
如果text1[i - 1] 与 text2[j - 1]相同,那么找到了一个公共元素,所以dp[i][j] = dp[i - 1][j - 1] + 1;
如果text1[i - 1] 与 text2[j - 1]不相同,那就看看text1[0, i - 2]与text2[0, j - 1]的最长公共子序列 和 text1[0, i - 1]与text2[0, j - 2]的最长公共子序列,取最大的。
即:dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
代码如下:
if (text1[i - 1] == text2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
- dp数组如何初始化
先看看dp[i][0]应该是多少呢?
test1[0, i-1]和空串的最长公共子序列自然是0,所以dp[i][0] = 0;
同理dp[0][j]也是0。
其他下标都是随着递推公式逐步覆盖,初始为多少都可以,那么就统一初始为0。
代码:
vector<vector<int>> dp(text1.size() + 1, vector<int>(text2.size() + 1, 0));
- 确定遍历顺序
从递推公式,可以看出,有三个方向可以推出dp[i][j],如图:
代码:
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
vector<vector<int>> dp(text1.size() + 1, vector<int>(text2.size() + 1, 0));
for (int i = 1; i <= text1.size(); i++) {
for (int j = 1; j <= text2.size(); j++) {
if (text1[i - 1] == text2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[text1.size()][text2.size()];
}
};
相似题目:392. 判断子序列
力扣链接
给定字符串 s 和 t ,判断 s 是否为 t 的子序列。
字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。
进阶:
如果有大量输入的 S,称作 S1, S2, … , Sk 其中 k >= 10亿,你需要依次检查它们是否为 T 的子序列。在这种情况下,你会怎样改变代码?
致谢:
特别感谢 @pbrother 添加此问题并且创建所有测试用例。
示例 1:
输入:s = “abc”, t = “ahbgdc”
输出:true
示例 2:
输入:s = “axc”, t = “ahbgdc”
输出:false
提示:
0 <= s.length <= 100
0 <= t.length <= 10^4
两个字符串都只由小写字符组成。
解法1:动态规划
class Solution {
public:
bool isSubsequence(string s, string t) {
//
int sSize = s.size(), tSize = t.size();
vector<vector<int>> dp(sSize+1,vector<int>(tSize+1,0));
for(int i = 1;i<=sSize;i++){
for(int j = 1;j<=tSize;j++){
if(s[i-1] == t[j-1]){
dp[i][j] = dp[i-1][j-1] + 1;
}else{
dp[i][j] = dp[i][j-1];
}
}
}
if(dp[sSize][tSize] == s.size()) return true;
return false;
}
};
相似题目: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
解法1:动态规划
class Solution {
public:
int findLength(vector<int>& nums1, vector<int>& nums2) {
int m = nums1.size(), n = nums2.size();
vector<vector<int>> dp(m+1,vector<int>(n+1,0));
int res = 0;
for(int i = 1;i<=m;i++){
for(int j = 1;j<=n;j++){
if(nums1[i-1] == nums2[j-1]){
dp[i][j] = dp[i-1][j-1] + 1;
}
res = max(res, dp[i][j]);
}
}
return res;
}
};
相似题目: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
解法1:动态规划
class Solution {
public:
int maxUncrossedLines(vector<int>& nums1, vector<int>& nums2) {
int m = nums1.size(), n = nums2.size();
vector<vector<int>> dp(m+1,vector<int>(n+1,0));
for(int i =1;i<=m;i++){
for(int j = 1;j<=n;j++){
if(nums1[i-1] == nums2[j-1]) dp[i][j] = dp[i-1][j-1] + 1;
else dp[i][j] = max(dp[i][j-1],dp[i-1][j]);
}
}
return dp[m][n];
}
};
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) 的解法,尝试使用更为精妙的 分治法 求解。
通过次数1,165,039提交次数2,125,119
解法1:动态规划
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int n = nums.size();
vector<int> dp(n,0);
dp[0] = nums[0];
int res = nums[0];
for(int i = 1;i<n;i++){
dp[i] = max(dp[i-1]+nums[i],nums[i]);
res = max(res,dp[i]);
}
return res;
}
};
……………………子序列问题:子序列(连续)………………
……………………子序列问题:编辑距离………………
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 由小写英文字母组成
解法1:动态规划
dp[i][j] 表示以下标i-1为结尾的字符串word1,和以下标j-1为结尾的字符串word2,最近编辑距离为dp[i][j]。
class Solution {
public:
int minDistance(string word1, string word2) {
int m = word1.size(), n = word2.size();
vector<vector<int>> dp(m+1,vector<int>(n+1,0));
for(int i = 0;i<=m;i++) dp[i][0] = i;
for(int i = 0;i<=n;i++) dp[0][i] = i;
for(int i = 1;i<=m;i++){
for(int j = 1;j<=n;j++){
if(word1[i-1] == word2[j-1]) dp[i][j] = dp[i-1][j-1];
else{
dp[i][j] = min(dp[i-1][j],min(dp[i][j-1],dp[i-1][j-1]))+1;
}
}
}
return dp[m][n];
}
};
相似题目: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 只包含小写英文字母
解法1:动态规划一
class Solution {
public:
int minDistance(string word1, string word2) {
int m = word1.size(), n = word2.size();
vector<vector<int>> dp(m+1,vector<int>(n+1,0));//
for(int i = 0;i<=m;i++) dp[i][0] = i;
for(int i = 1;i<=n;i++) dp[0][i] = i;
for(int i = 1;i<=m;i++){
for(int j = 1;j<=n;j++){
if(word1[i-1] == word2[j-1]){
dp[i][j] = dp[i-1][j-1];
}else{
dp[i][j] = min(dp[i-1][j-1]+2,min(dp[i][j-1]+1,dp[i-1][j]+1));
}
}
}
return dp[m][n];
}
};
解法2:动态规划二
本题和动态规划:1143.最长公共子序列 (opens new window)基本相同,只要求出两个字符串的最长公共子序列长度即可,那么除了最长公共子序列之外的字符都是必须删除的,最后用两个字符串的总长度减去两个最长公共子序列的长度就是删除的最少步数。
代码如下:
class Solution {
public:
int minDistance(string word1, string word2) {
vector<vector<int>> dp(word1.size()+1, vector<int>(word2.size()+1, 0));
for (int i=1; i<=word1.size(); i++){
for (int j=1; j<=word2.size(); j++){
if (word1[i-1] == word2[j-1]) dp[i][j] = dp[i-1][j-1] + 1;
else dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
}
}
return word1.size()+word2.size()-dp[word1.size()][word2.size()]*2;
}
};
……………………子序列问题:回文………………
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 由小写英文字母组成
解法1:双指针
动态规划的空间复杂度是偏高的,我们再看一下双指针法。
首先确定回文串,就是找中心然后向两边扩散看是不是对称的就可以了。
在遍历中心点的时候,要注意中心点有两种情况。
一个元素可以作为中心点,两个元素也可以作为中心点。
那么有人同学问了,三个元素还可以做中心点呢。其实三个元素就可以由一个元素左右添加元素得到,四个元素则可以由两个元素左右添加元素得到。
所以我们在计算的时候,要注意一个元素为中心点和两个元素为中心点的情况。
这两种情况可以放在一起计算,但分别计算思路更清晰,我倾向于分别计算,代码如下:
class Solution {
public:
int countSubstrings(string s) {
int res = 0;
for(int i = 0;i<s.size();i++){
res+=extend(s,i,i,s.size());
res+=extend(s,i,i+1,s.size());
}
return res;
}
int extend(const string& s, int i, int j, int n){
int res = 0;
while(i >= 0 && j < n && s[i] == s[j]){
i--;
j++;
res++;
}
return res;
}
};
时间复杂度:
O
(
n
2
)
O(n^2)
O(n2)
空间复杂度:
O
(
1
)
O(1)
O(1)
解法2:动态规划
相似题目:5. 最长回文子串
给你一个字符串 s,找到 s 中最长的回文子串。
示例 1:
输入:s = “babad”
输出:“bab”
解释:“aba” 同样是符合题意的答案。
示例 2:
输入:s = “cbbd”
输出:“bb”
提示:
1 <= s.length <= 1000
s 仅由数字和英文字母组成
解法1:双指针(中心扩展法)
class Solution {
public:
int left, right, maxLen = 0;
string longestPalindrome(string s) {
//双指针
int n = s.size();
//int maxLen = 0;
for(int i = 0;i<n;i++){
extend(s,i,i,n);
extend(s,i,i+1,n);
}
return s.substr(left,maxLen);
}
void extend(const string& s, int i, int j, int n){
int res = 0;
while(i >= 0 && j<n && s[i] == s[j]){
if(j-i+1 > maxLen){
left = i;
right = j;
maxLen = j-i+1;
}
i--;
j++;
}
}
};
动态规划
class Solution {
public:
string longestPalindrome(string s) {
int n = s.size();
if(n <= 1) return s;
vector<vector<bool>> dp(n,vector<bool>(n,false));
int maxL = 0;
string res = "";
int left = 0, right = 0;
for(int i = n-1;i>=0;i--){
for(int j = i;j<n;j++){
if(s[i] == s[j]) {
if(j-i<=1) dp[i][j] = true;
else if(dp[i+1][j-1]) dp[i][j] = true;
}
if(dp[i][j] && j-i+1 > maxL){
maxL = j-i+1;
left = i;
right = j;
}
}
}
return s.substr(left,right-left+1);
}
};
相似题目:516. 最长回文子序列
力扣链接
给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。
子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。
示例 1:
输入:s = “bbbab”
输出:4
解释:一个可能的最长回文子序列为 “bbbb” 。
示例 2:
输入:s = “cbbd”
输出:2
解释:一个可能的最长回文子序列为 “bb” 。
提示:
1 <= s.length <= 1000
s 仅由小写英文字母组成
解法1:动态规划
class Solution {
public:
int longestPalindromeSubseq(string s) {
//dp[i][j]表示s的[i,j]区间最长的回文子序列的长度
vector<vector<int>> dp(s.size(),vector<int>(s.size(),0));
for (int i = 0;i<s.size();i++) dp[i][i] = 1;
for(int i = s.size()-2;i>=0;i--){
for(int j = i+1;j<s.size();j++){
if(s[i] == s[j]){
dp[i][j] = dp[i+1][j-1] +2;
}else{
dp[i][j] = max(dp[i+1][j],dp[i][j-1]);
}
}
}
return dp[0][s.size()-1];
}
};
689. 三个无重叠子数组的最大和
力扣链接
给你一个整数数组 nums 和一个整数 k ,找出三个长度为 k 、互不重叠、且 3 * k 项的和最大的子数组,并返回这三个子数组。
以下标的数组形式返回结果,数组中的每一项分别指示每个子数组的起始位置(下标从 0 开始)。如果有多个结果,返回字典序最小的一个。
示例 1:
输入:nums = [1,2,1,2,6,7,5,1], k = 2
输出:[0,3,5]
解释:子数组 [1, 2], [2, 6], [7, 5] 对应的起始下标为 [0, 3, 5]。
也可以取 [2, 1], 但是结果 [1, 3, 5] 在字典序上更大。
示例 2:
输入:nums = [1,2,1,2,1,2,1,2,1], k = 2
输出:[0,2,4]
提示:
1 <= nums.length <= 2 * 104
1 <= nums[i] < 216
1 <= k <= floor(nums.length / 3)
解法1:动态规划
思路:
代码:
class Solution {
public:
vector<int> maxSumOfThreeSubarrays(vector<int>& nums, int k) {
int n = nums.size();
vector<int> preSum(n+1,0);
for(int i = 1;i<=n;i++) preSum[i] = preSum[i-1] + nums[i-1];
//定义dp[i][j]为,在有i个数,子数组个数为j个时,最大和为dp[i][j]
vector<vector<int>> dp(n+1,vector<int>(4));
for(int i = k;i<=n;i++){ //背包容量
for(int j = 1;j<=3;j++){ //物品
dp[i][j] = max(dp[i-1][j],dp[i-k][j-1]+preSum[i]-preSum[i-k]);
}
}
vector<int> result(3,0);
for(int j = 3, i = n, idx = 2;j>0;){
if(dp[i-1][j] >= dp[i-k][j-1]+preSum[i]-preSum[i-k]) i--;
else{
result[idx] = i-k;
idx--;
i -= k;
j--;
}
}
return result;
}
};
for(int j = 3, i = n, idx = 2;j>0;){
if(dp[i-1][j] >= dp[i-k][j-1]+preSum[i]-preSum[i-k]) i--;
这里用dp[i-k][j-1]+preSum[i]-preSum[i-k]而不用dp[i][j]的原因是由于其定义,dp[i][j]无法确定是来自于dp[i-k][j-1]+preSum[i]-preSum[i-k]还是dp[i-1][j]
复杂度分析:
时间复杂度:O(n)
空间复杂度:O(n)
1220. 统计元音字母序列的数目
力扣链接
给你一个整数 n,请你帮忙统计一下我们可以按下述规则形成多少个长度为 n 的字符串:
字符串中的每个字符都应当是小写元音字母(‘a’, ‘e’, ‘i’, ‘o’, ‘u’)
每个元音 ‘a’ 后面都只能跟着 ‘e’
每个元音 ‘e’ 后面只能跟着 ‘a’ 或者是 ‘i’
每个元音 ‘i’ 后面 不能 再跟着另一个 ‘i’
每个元音 ‘o’ 后面只能跟着 ‘i’ 或者是 ‘u’
每个元音 ‘u’ 后面只能跟着 ‘a’
由于答案可能会很大,所以请你返回 模 10^9 + 7 之后的结果。
示例 1:
输入:n = 1
输出:5
解释:所有可能的字符串分别是:“a”, “e”, “i” , “o” 和 “u”。
示例 2:
输入:n = 2
输出:10
解释:所有可能的字符串分别是:“ae”, “ea”, “ei”, “ia”, “ie”, “io”, “iu”, “oi”, “ou” 和 “ua”。
示例 3:
输入:n = 5
输出:68
提示:
1 <= n <= 2 * 10^4
解法1:动态规划
思路:
在约束之下:
a 只能从 e i u 转移过来
e 只能从 a i 转移过来
i 只能从 e 和 o 转移过来
o 只能从 i 转移过来
u 只能从 i 和 o 转移过来
dp[i][0]=dp[i−1][1]+dp[i−1][2]+dp[i−1][4]
dp[i][1]=dp[i−1][0]+dp[i−1][2]
dp[i][2]=dp[i−1][1]+dp[i−1][3]
dp[i][3]=dp[i−1][2]
dp[i][4]=dp[i−1][2]+dp[i−1][3]
代码:
class Solution {
public:
int countVowelPermutation(int n) {
long mod = 1e9+7;
vector<vector<long>> dp(n+1,vector<long>(5));
for(int i = 0;i<5;i++){
dp[1][i] = 1;
}
for(int i = 2;i<=n;i++){
dp[i][0] = (dp[i-1][1]+dp[i-1][2]+dp[i-1][4])% mod;
dp[i][1] = (dp[i-1][0]+dp[i-1][2])% mod;
dp[i][2] = (dp[i-1][1]+dp[i-1][3])% mod;
dp[i][3] = (dp[i-1][2])% mod;
dp[i][4] = (dp[i-1][2]+dp[i-1][3])% mod;
}
int result = 0;
for(int i = 0;i<5;i++){
result = (result + dp[n][i])%mod;
}
return result;
}
};
64. 最小路径和
力扣链接
给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
示例 1:
输入:grid = [[1,3,1],[1,5,1],[4,2,1]]
输出:7
解释:因为路径 1→3→1→1→1 的总和最小。
示例 2:
输入:grid = [[1,2,3],[4,5,6]]
输出:12
提示:
m == grid.length
n == grid[i].length
1 <= m, n <= 200
0 <= grid[i][j] <= 100
解法1:动态规划
代码:
class Solution {
public:
int minPathSum(vector<vector<int>>& grid) {
//动态规划
int m = grid.size(), n = grid[0].size();
vector<vector<int>> dp(m,vector<int>(n));
dp[0][0] = grid[0][0];
for(int i = 1;i<n;i++){
dp[0][i] = dp[0][i-1] + grid[0][i];
}
for(int i = 1;i<m;i++){
dp[i][0] = dp[i-1][0] + grid[i][0];
}
for(int i = 1;i<m;i++){
for(int j = 1;j<n;j++){
dp[i][j] = min(dp[i-1][j]+grid[i][j], dp[i][j-1]+grid[i][j]);
}
}
return dp[m-1][n-1];
}
};
120. 三角形最小路径和
力扣链接
给定一个三角形 triangle ,找出自顶向下的最小路径和。
每一步只能移动到下一行中相邻的结点上。相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。也就是说,如果正位于当前行的下标 i ,那么下一步可以移动到下一行的下标 i 或 i + 1 。
示例 1:
输入:triangle = [[2],[3,4],[6,5,7],[4,1,8,3]]
输出:11
解释:如下面简图所示:
2
3 4
6 5 7
4 1 8 3
自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)。
示例 2:
输入:triangle = [[-10]]
输出:-10
提示:
1 <= triangle.length <= 200
triangle[0].length == 1
triangle[i].length == triangle[i - 1].length + 1
-104 <= triangle[i][j] <= 104
进阶:
你可以只使用 O(n) 的额外空间(n 为三角形的总行数)来解决这个问题吗?
解法1:原地动态规划
class Solution {
public:
int minimumTotal(vector<vector<int>>& triangle) {
for(int i = triangle.size()-2;i>=0;i--){
for(int j = 0;j<triangle[i].size();j++){
triangle[i][j] += min(triangle[i+1][j],triangle[i+1][j+1]);
}
}
return triangle[0][0];
}
};
解法2:动态规划
思路:
定义二维 dp 数组,将解法二中「自顶向下的递归」改为「自底向上的递推」。
1、状态定义:
dp[i][j]表示从点(i,j) 到底边的最小路径和。
2、状态转移:
dp[i][j] = min(dp[i + 1][j], dp[i + 1][j + 1]) + triangle[i][j]
代码:
class Solution {
public:
int minimumTotal(vector<vector<int>>& triangle) {
int n = triangle.size();
vector<vector<int>> dp(n+1,vector<int>(n+1,0));
for(int i = n-1;i>=0;i--){
for(int j = 0;j<triangle[i].size();j++){
dp[i][j] = min(dp[i+1][j],dp[i+1][j+1])+triangle[i][j];
}
}
return dp[0][0];
}
};
时间复杂度:O(N^2),N 为三角形的行数。
空间复杂度:O(N^2),N为三角形的行数。
空间优化:
在上述代码中,我们定义了一个 N 行 N 列 的 dp 数组(N 是三角形的行数)。
但是在实际递推中我们发现,计算 dp[i][j]时,只用到了下一行的 dp[i + 1][j]和 dp[i + 1][j + 1]。
因此 dp 数组不需要定义 N 行,只要定义 1 行就阔以啦。
所以我们稍微修改一下上述代码,将 i 所在的维度去掉(如下),就可以将 O(N^2)的空间复杂度优化成 O(N)
class Solution {
public:
int minimumTotal(vector<vector<int>>& triangle) {
int n = triangle.size();
vector<int> dp(n+1,0);
for(int i = n-1;i>=0;i--){
for(int j = 0;j<triangle[i].size();j++){
dp[j] = min(dp[j],dp[j+1])+triangle[i][j];
}
}
return dp[0];
}
};
375. 猜数字大小 II
力扣链接
我们正在玩一个猜数游戏,游戏规则如下:
我从 1 到 n 之间选择一个数字。
你来猜我选了哪个数字。
如果你猜到正确的数字,就会 赢得游戏 。
如果你猜错了,那么我会告诉你,我选的数字比你的 更大或者更小 ,并且你需要继续猜数。
每当你猜了数字 x 并且猜错了的时候,你需要支付金额为 x 的现金。如果你花光了钱,就会 输掉游戏 。
给你一个特定的数字 n ,返回能够 确保你获胜 的最小现金数,不管我选择那个数字 。
示例 1:
输入:n = 10
输出:16
解释:制胜策略如下:
- 数字范围是 [1,10] 。你先猜测数字为 7 。
- 如果这是我选中的数字,你的总费用为 $0 。否则,你需要支付 $7 。
- 如果我的数字更大,则下一步需要猜测的数字范围是 [8,10] 。你可以猜测数字为 9 。
- 如果这是我选中的数字,你的总费用为 $7 。否则,你需要支付 $9 。
- 如果我的数字更大,那么这个数字一定是 10 。你猜测数字为 10 并赢得游戏,总费用为 $7 + $9 = $16 。
- 如果我的数字更小,那么这个数字一定是 8 。你猜测数字为 8 并赢得游戏,总费用为 $7 + $9 = $16 。
- 如果我的数字更小,则下一步需要猜测的数字范围是 [1,6] 。你可以猜测数字为 3 。
- 如果这是我选中的数字,你的总费用为 $7 。否则,你需要支付 $3 。
- 如果我的数字更大,则下一步需要猜测的数字范围是 [4,6] 。你可以猜测数字为 5 。
- 如果这是我选中的数字,你的总费用为 $7 + $3 = $10 。否则,你需要支付 $5 。
- 如果我的数字更大,那么这个数字一定是 6 。你猜测数字为 6 并赢得游戏,总费用为 $7 + $3 + $5 = $15 。
- 如果我的数字更小,那么这个数字一定是 4 。你猜测数字为 4 并赢得游戏,总费用为 $7 + $3 + $5 = $15 。
- 如果我的数字更小,则下一步需要猜测的数字范围是 [1,2] 。你可以猜测数字为 1 。
- 如果这是我选中的数字,你的总费用为 $7 + $3 = $10 。否则,你需要支付 $1 。
- 如果我的数字更大,那么这个数字一定是 2 。你猜测数字为 2 并赢得游戏,总费用为 $7 + $3 + $1 = $11 。
在最糟糕的情况下,你需要支付 $16 。因此,你只需要 $16 就可以确保自己赢得游戏。
示例 2:
输入:n = 1
输出:0
解释:只有一个可能的数字,所以你可以直接猜 1 并赢得游戏,无需支付任何费用。
示例 3:
输入:n = 2
输出:1
解释:有两个可能的数字 1 和 2 。
- 你可以先猜 1 。
- 如果这是我选中的数字,你的总费用为 $0 。否则,你需要支付 $1 。
- 如果我的数字更大,那么这个数字一定是 2 。你猜测数字为 2 并赢得游戏,总费用为 $1 。
最糟糕的情况下,你需要支付 $1 。
提示:
1 <= n <= 200
解法1:动态规划
思路:
(1)为什么使用动态规划,不使用二分?
动态规划与二分的区别在哪里呢?
使用动态规划的好处在于我可以穷举所有的情况,对于这个题来说,就是指动态规划的方法可以把每一个数字都当作分割点,而二分只能把中间的数字当作分割点。
举个例子:
当n=5:
动态规划:1 2 3 4 5 在第一次猜数时,我们可以猜1,2,3,4,5
二分查找:1 2 3 4 5 在第一次猜数时,我们只能猜3
为什么要使用动态规划猜所有的数字呢?
当n=5,假如我第一次猜3,那么需要7;假如我第一次猜4,只需要6.
很显然6才是正确答案,使用二分法虽然方便,但是是错误的。所以我使用动态规划穷举所有情况。
(2) 实际上,记忆化搜索就是DP的一种实现方式,一般情况下,DP的问题都具有2个要素:
最优子结构,一个大问题的最优解可以拆分成多个子问题的最优解,当子问题足够小,就可以很容易得到结果
子问题重叠,即会反复得求解相同的子问题,如上述的重复计算(分治也是求解子问题,但分治法的子问题是不重叠的)
而解决DP问题的方式也就变成了:
定义最优子结构,上述分析中F(i,j)即为定义的最优子结构
将子问题的解记录在表中,重叠的子问题只需计算一次,再次求解时查表即可
我们要求的是F(1,n)F(1,n),需要先求解F(1,1),F(3,n),F(1,2),F(4,n)…F(1,n-2),F(n,n)F(1,1),F(3,n),F(1,2),F(4,n)…F(1,n−2),F(n,n),要求解这些子式,同样需要对应区间,我们通过二维表帮助分析:
代码:
class Solution {
public:
int getMoneyAmount(int n) {
//动态规划
if(n == 1) return 0;
vector<vector<int>> dp(n+1,vector<int>(n+1,0));
for(int i = n-1;i>=1;i--){
for(int j = i+1;j<=n;j++){
int minn = INT_MAX;
for(int k = i;k<=j-1;k++){
minn = min(k+max(dp[i][k-1],dp[k+1][j]),minn);
}
dp[i][j] = minn;
}
}
return dp[1][n];
}
};
相似题目:374. 猜数字大小
力扣链接
猜数字游戏的规则如下:
每轮游戏,我都会从 1 到 n 随机选择一个数字。 请你猜选出的是哪个数字。
如果你猜错了,我会告诉你,你猜测的数字比我选出的数字是大了还是小了。
你可以通过调用一个预先定义好的接口 int guess(int num) 来获取猜测结果,返回值一共有 3 种可能的情况(-1,1 或 0):
-1:我选出的数字比你猜的数字小 pick < num
1:我选出的数字比你猜的数字大 pick > num
0:我选出的数字和你猜的数字一样。恭喜!你猜对了!pick == num
返回我选出的数字。
示例 1:
输入:n = 10, pick = 6
输出:6
示例 2:
输入:n = 1, pick = 1
输出:1
示例 3:
输入:n = 2, pick = 1
输出:1
示例 4:
输入:n = 2, pick = 2
输出:2
提示:
1 <= n <= 231 - 1
1 <= pick <= n
解法1:二分查找
/**
* Forward declaration of guess API.
* @param num your guess
* @return -1 if num is lower than the guess number
* 1 if num is higher than the guess number
* otherwise return 0
* int guess(int num);
*/
class Solution {
public:
int guessNumber(int n) {
int left = 0, right = n;
while(left < right){
int mid = left + (right-left)/2;
if(guess(mid)<=0) right = mid;
else left = mid + 1;
}
return left;
}
};
264. 丑数 II
力扣链接
给你一个整数 n ,请你找出并返回第 n 个 丑数 。
丑数 就是只包含质因数 2、3 和/或 5 的正整数。
示例 1:
输入:n = 10
输出:12
解释:[1, 2, 3, 4, 5, 6, 8, 9, 10, 12] 是由前 10 个丑数组成的序列。
示例 2:
输入:n = 1
输出:1
解释:1 通常被视为丑数。
提示:
1 <= n <= 1690
解法1:动态规划
思路:
代码:
class Solution {
public:
int nthUglyNumber(int n) {
//动态规划
int p2 = 1,p3 = 1, p5 =1;
vector<int> dp(n+1);
dp[1] = 1;
for(int i =2;i<=n;i++){
int num2 = dp[p2]*2, num3 = dp[p3]*3, num5 = dp[p5]*5;
dp[i] = min(num2,min(num3,num5));
if(dp[i] == num2) p2++;
if(dp[i] == num3) p3++;
if(dp[i] == num5) p5++;
}
return dp[n];
}
};
解法2:小根堆
思路:
代码:
class Solution {
public:
int nthUglyNumber(int n) {
//小根堆
priority_queue<int,vector<int>,greater<int>> pq;
int res = 1;
for(int i = 1;i<n;i++){
pq.push(res*2);
pq.push(res*3);
pq.push(res*5);
res = pq.top();
pq.pop();
while(!pq.empty() && res == pq.top()){
pq.pop();
}
}
return res;
}
};
相似题目:263. 丑数
力扣链接
丑数 就是只包含质因数 2、3 和 5 的正整数。
给你一个整数 n ,请你判断 n 是否为 丑数 。如果是,返回 true ;否则,返回 false 。
示例 1:
输入:n = 6
输出:true
解释:6 = 2 × 3
示例 2:
输入:n = 1
输出:true
解释:1 没有质因数,因此它的全部质因数是 {2, 3, 5} 的空集。习惯上将其视作第一个丑数。
示例 3:
输入:n = 14
输出:false
解释:14 不是丑数,因为它包含了另外一个质因数 7 。
提示:
-231 <= n <= 231 - 1
解法1:递归
class Solution {
public:
bool isUgly(int n) {
//递归
if(n == 0) return false;
if(n == 1) return true;
if(n % 2 == 0) return isUgly(n/2);
if(n % 3 == 0) return isUgly(n/3);
if(n % 5 == 0) return isUgly(n/5);
return false;
}
};
解法2:迭代
class Solution {
public:
bool isUgly(int n) {
vector<int> a = {2,3,5};
for(auto aa:a){
while(n % aa == 0 && n > 0){
n /= aa;
}
}
return n == 1;
}
};
55. 跳跃游戏
力扣链接
给定一个非负整数数组 nums ,你最初位于数组的 第一个下标 。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标。
示例 1:
输入:nums = [2,3,1,1,4]
输出:true
解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。
示例 2:
输入:nums = [3,2,1,0,4]
输出:false
解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。
提示:
1 <= nums.length <= 3 * 104
0 <= nums[i] <= 105
解法1:贪心算法
class Solution {
public:
bool canJump(vector<int>& nums) {
//贪心算法局部最优解:每次取最大跳跃步数(取最大覆盖范围),整体最优解:最后得到整体最大覆盖范围,看是否能到终点。
if (nums.size() == 1) return true;
int cover = 0;//最大覆盖范围,初始最大覆盖范围为自己0
for (int i = 0;i<=cover;i++){//在当前最大覆盖范围内移动
cover = max(i+nums[i],cover);
if (cover >= nums.size()-1) return true;
}
return false;
}
};
解法2:动态规划
class Solution {
public:
bool canJump(vector<int>& nums) {
int n = nums.size();
if(n == 1) return true;
vector<bool> dp(n,false);
dp[0] = true;
//本来使用的是 for(int j=0;j<i;j++)
//但是这样每次从0开始就比较耗时,结果导致虽然案例全部通过,但是超时
for(int i = 1;i<n;i++){
for(int j = i-1;j>=0;j--){
if(nums[j] >= i-j && dp[j]){
dp[i] = true;
break;
}
}
}
return dp[n-1];
}
};
相似题目:45. 跳跃游戏 II
跳跃游戏
给你一个非负整数数组 nums ,你最初位于数组的第一个位置。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
你的目标是使用最少的跳跃次数到达数组的最后一个位置。
假设你总是可以到达数组的最后一个位置。
示例 1:
输入: nums = [2,3,1,1,4]
输出: 2
解释: 跳到最后一个位置的最小跳跃数是 2。
从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。
示例 2:
输入: nums = [2,3,0,1,4]
输出: 2
提示:
1 <= nums.length <= 104
0 <= nums[i] <= 1000
解法1:贪心算法
class Solution {
public:
int jump(vector<int>& nums) {
int result = 0;
if (nums.size() == 1) return 0;
int curCover = 0;//当前最大覆盖范围
int nextCover = 0;//下一步最大覆盖范围
for(int i = 0;i<= curCover;i++){//在当前最大覆盖范围内移动
nextCover = max(i+nums[i],nextCover);
if (nextCover >= nums.size() -1) return result + 1;
if (i == curCover){// 遇到当前覆盖的最远距离下标
result++;
curCover = nextCover;
}
}
return result;
}
};