目录
动态规划理论基础
视频:从此再也不怕动态规划了,动态规划解题方法论大曝光 !| 理论基础 |力扣刷题总结| 动态规划入门_哔哩哔哩_bilibili
- 确定dp数组(dp table)以及下标的含义
- 确定递推公式
- dp数组如何初始化
- 确定遍历顺序
- 举例推导dp数组
dubeg
- 找问题的最好方式就是把dp数组打印出来,看看究竟是不是按照自己思路推导的!
- 做动规的题目,写代码之前一定要把状态转移在dp数组的上具体情况模拟一遍,心中有数,确定最后推出的是想要的结果。
- 然后再写代码,如果代码没通过就打印dp数组,看看是不是和自己预先推导的哪里不一样。
- 如果打印出来和自己预先模拟推导是一样的,那么就是自己的递归公式、初始化或者遍历顺序有问题了。
- 如果和自己预先模拟推导的不一样,那么就是代码实现细节有问题。
509. 斐波那契数
很简单的动规入门题,但简单题使用来掌握方法论的,还是要有动规五部曲来分析。
视频:手把手带你入门动态规划 | LeetCode:509.斐波那契数_哔哩哔哩_bilibili
class Solution {
public:
int fib(int n) {
if(n <= 1) return n;
vector<int> fib(n+1);
fib[0] = 0;
fib[1] = 1;
for(int i = 2; i <=n; i++){
fib[i] = fib[i-1] + fib[i-2];
}
return fib[n];
}
};
70. 爬楼梯
本题大家先自己想一想, 之后会发现,和 斐波那契数 有点关系。
视频:带你学透动态规划-爬楼梯(对应力扣70.爬楼梯)| 动态规划经典入门题目_哔哩哔哩_bilibili
class Solution {
public:
int climbStairs(int n) {
vector<int> dp(n+1);
dp[0] = 0;
dp[1] = 1;
dp[2] = 2;
for(int i = 3; i <= n; i++){
dp[i] = dp[i-1] + dp[i-2];
}
return dp[n];
}
};
746. 使用最小花费爬楼梯
这道题目力扣改了题目描述了,现在的题目描述清晰很多,相当于明确说 第一步是不用花费的。
更改题目描述之后,相当于是 文章中 「拓展」的解法
视频讲解:动态规划开更了!| LeetCode:746. 使用最小花费爬楼梯_哔哩哔哩_bilibili
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost) {
vector<int> dp(cost.size()+1);//注意还有一个楼顶,所以dp会比cost多一个元素
dp[0] = 0;
dp[1] = 0;
for(int i = 2; i <= cost.size(); i++){
dp[i] = min(dp[i-2] + cost[i-2], dp[i-1] + cost[i-1]);
}
return dp[cost.size()];
}
};
62.不同路径
本题大家掌握动态规划的方法就可以。 数论方法 有点非主流,很难想到。
视频讲解:动态规划中如何初始化很重要!| LeetCode:62.不同路径_哔哩哔哩_bilibili
class Solution {
public:
int uniquePaths(int m, int n) {
vector<vector<int>> dp(m, vector<int>(n,0));//注意初始化方式;dp含义是原点到每个位置路径数量
//初始化 边缘的位置只有一种路径可能性
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];
}
};
63. 不同路径 II
视频讲解:动态规划,这次遇到障碍了| LeetCode:63. 不同路径 II_哔哩哔哩_bilibili
class Solution {
public:
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
int m = obstacleGrid.size();//行数
int n = obstacleGrid[0].size();//列数
vector<vector<int>> dp(m , vector<int>(n, 0));
if(obstacleGrid[0][0] == 1 || obstacleGrid[m-1][n-1] == 1) return 0;
//初始化
for(int i = 0; i < m && obstacleGrid[i][0] != 1; i++) dp[i][0] = 1;
//不用再为障碍后面的位置赋值了,因为初始化的时候默认就是0
for(int j = 0; j < m && obstacleGrid[0][j] != 1; j++) dp[0][j] = 1;
//动态规划
for(int i = 1; i < m; i++){
for(int j = 1; j < n; j++){
if(obstacleGrid[i-1][j] == 1 && obstacleGrid[i][j-1] != 1) dp[i][j] = dp[i][j-1];
else if(obstacleGrid[i][j-1] == 1 && obstacleGrid[i-1][j] != 1) dp[i][j] = dp[i-1][j];
else if(obstacleGrid[i-1][j] == 1 && obstacleGrid[i][j-1] == 1) dp[i][j] = 0;
else dp[i][j] = dp[i][j-1] + dp[i-1][j];
// cout << dp[i][j] << endl;
}
}
return dp[m-1][n-1];
}
};
代码随想录里是这样写动态规划逻辑的,在此位置如果是障碍,就不计算dp了,然后后面照常计算
class Solution {
public:
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
int m = obstacleGrid.size();//行数
int n = obstacleGrid[0].size();//列数
vector<vector<int>> dp(m , vector<int>(n, 0));
if(obstacleGrid[0][0] == 1 || obstacleGrid[m-1][n-1] == 1) return 0;
//初始化
for(int i = 0; i < m && obstacleGrid[i][0] != 1; i++) dp[i][0] = 1;
//不用再为障碍后面的位置赋值了,因为初始化的时候默认就是0
for(int j = 0; j < m && obstacleGrid[0][j] != 1; j++) dp[0][j] = 1;
//动态规划
for(int i = 1; i < m; i++){
for(int j = 1; j < n; j++){
if(obstacleGrid[i-1][j] == 1 && obstacleGrid[i][j-1] != 1) dp[i][j] = dp[i][j-1];
else if(obstacleGrid[i][j-1] == 1 && obstacleGrid[i-1][j] != 1) dp[i][j] = dp[i-1][j];
else if(obstacleGrid[i-1][j] == 1 && obstacleGrid[i][j-1] == 1) dp[i][j] = 0;
else dp[i][j] = dp[i][j-1] + dp[i-1][j];
// cout << dp[i][j] << endl;
}
}
return dp[m-1][n-1];
}
};
343. 整数拆分
视频讲解:动态规划,本题关键在于理解递推公式!| LeetCode:343. 整数拆分_哔哩哔哩_bilibili
class Solution {
public:
int integerBreak(int n) {
vector<int> dp(n+1,0);
dp[2] = 1;//1*1
for(int i = 3; i <= n; i++){
for(int j = 1; j <= i/2; j++){
dp[i] = max(dp[i], max(j * (i - j), j * dp[i - j]));//ATTENTION
}
}
return dp[n];
}
};
96.不同的二叉搜索树
视屏讲解:动态规划找到子状态之间的关系很重要!| LeetCode:96.不同的二叉搜索树_哔哩哔哩_bilibili
class Solution {
public:
int numTrees(int n) {
vector<int> dp(n+1);
dp[0] = 1;//ATTENTION
// dp[1] = 1;
// dp[2] = 2;
for(int i = 1; i <= n; i++){
for(int j = 1; j <= i; j++){
dp[i] += dp[j-1] * dp[i-j];//j == i的时候,得用到dp[0],必须得初始化一下
}
}
return dp[n];
}
};
背包问题
01背包问题 二维
视频讲解:带你学透0-1背包问题!| 关于背包问题,你不清楚的地方,这里都讲了!| 动态规划经典问题 | 数据结构与算法_哔哩哔哩_bilibili
代码以此题为例,背包最大重量为4,物品如下表所示,问背包能背的物品最大价值是多少?
重量 | 价值 | |
---|---|---|
物品0 | 1 | 15 |
物品1 | 3 | 20 |
物品2 | 4 | 30 |
1、dp[i][j] 的含义:dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。
2、确定递推公式:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])
其中dp[i - 1][j]是不放i的情况,dp[i - 1][j - weight[i]] + value[i]是放i的情况。可以看出dp[i][j] 是由左上方数值推导出来。
3、初始化
dp[i][0]:一定都是0
dp[0][j]:分情况,如果①当 j < weight[0]的时候,dp[0][j] 应该是 0;②当j >= weight[0]时,dp[0][j] 应该是value[0]
其他位置默认0就行了
——所以只需要考虑:当j >= weight[0]的情况,dp[0][j] 是value[0]
4、确定遍历顺序
先遍历 物品还是先遍历背包重量呢?其实都可以!! 但是先遍历物品更好理解。
5、举例推导dp数组
做动态规划的题目,最好的过程就是自己在纸上举一个例子把对应的dp数组的数值推导一下,然后在动手写代码!
#include <vector>
#include <iostream>
using namespace std;
void test_2_wei_bag_problem1() {
vector<int> weight = {1, 3, 4};
vector<int> value = {15, 20, 30};
int bagweight = 4;
// 二维数组
vector<vector<int>> dp(weight.size(), vector<int>(bagweight + 1, 0));
// 初始化
for (int j = weight[0]; j <= bagweight; j++) {
dp[0][j] = value[0];
}
// weight数组的大小 就是物品个数
for(int i = 1; i < weight.size(); i++) { // 遍历物品
for(int j = 0; j <= bagweight; j++) { // 遍历背包容量
if (j < weight[i]) dp[i][j] = dp[i - 1][j];
else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
}
cout << dp[weight.size() - 1][bagweight] << endl;
}
int main() {
test_2_wei_bag_problem1();
}
可以在线编译器中试一下:C++ 在线工具 | 菜鸟工具 (jyshare.com)
卡码网题目:
题目页面 (kamacoder.com)
#include <vector>
#include <iostream>
using namespace std;
void reseacher_material(){
int M, N;
cin >> M >> N;
vector<int> space(M, 0); // 存储每件物品所占空间
vector<int> value(M, 0); // 存储每件物品价值
for(int i = 0; i < M; i++) cin >> space[i];
for(int i = 0; i < M; i++) cin >> value[i];
// define the dp
vector<vector<int>> dp(M,vector<int>(N+1,0));
// initation
for(int j = space[0]; j <= N; j++){
dp[0][j] = value[0];
}
// dynamic programming
for(int i = 1; i < M; i++){
for(int j = 0; j <= N; j++){
if(j < space[i]) dp[i][j] = dp[i-1][j];
else dp[i][j] = max(dp[i-1][j], dp[i-1][j-space[i]]+ value[i]);
}
}
std::cout << dp[M-1][N] << std::endl;
}
int main(){
reseacher_material();
}
01背包问题 一维(滚动数组)
视频讲解:带你学透01背包问题(滚动数组篇) | 从此对背包问题不再迷茫!_哔哩哔哩_bilibili
1、dp[i]的含义:容量为j的背包,所背的物品价值可以最大为dp[j]
2、递推公式:此时dp[j]有两个选择,一个是取自己dp[j] 相当于 二维dp数组中的dp[i-1][j],即不放物品i,一个是取dp[j - weight[i]] + value[i],即放物品i,指定是取最大的,毕竟是求最大价值。需要理解是上一层的拷贝。
3、初始化:
假设物品价值都是大于0的,所以dp数组初始化的时候,都初始为0就可以了
4、一维dp数组遍历顺序:逆序遍历
5、举例推导dp数组
自编题
void test_1_wei_bag_problem() {
vector<int> weight = {1, 3, 4};
vector<int> value = {15, 20, 30};
int bagWeight = 4;
// 初始化
vector<int> dp(bagWeight + 1, 0);// just initiate 0
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
cout << dp[bagWeight] << endl;
}
int main() {
test_1_wei_bag_problem();
}
卡码网
#include <vector>
#include <iostream>
using namespace std;
void reseacher_material(){
int M, N;
cin >> M >> N;
vector<int> space(M); // 存储每件物品所占空间
vector<int> value(M); // 存储每件物品价值
for(int i = 0; i < M; i++) cin >> space[i];
for(int i = 0; i < M; i++) cin >> value[i];
// define the dp
vector<int> dp(N+1,0);
// initation -- just 0
// dynamic programming
for(int i = 0; i < M; ++i){
for(int j = N; j >= space[i]; --j){
dp[j] = max(dp[j], dp[j-space[i]]+ value[i]);
}
}
cout << dp[N] << endl;
}
int main(){
reseacher_material();
}
416.分割等和子集
本题是 01背包的应用类题目
题目链接:416. 分割等和子集 - 力扣(LeetCode)
视频讲解:动态规划之背包问题,这个包能装满吗?| LeetCode:416.分割等和子集_哔哩哔哩_bilibili
- 698.划分为k个相等的子集
- 473.火柴拼正方形
1、dp数组的含义:容量为j的背包,所背的物品价值最大可以为dp[j]。本题中每一个元素的数值既是重量,也是价值。套到本题,dp[j]表示 背包总容量(所能装的总重量)是j,放进物品后,背的最大重量为dp[j]。
那么如果背包容量为target, dp[target]就是装满 背包之后的重量,所以 当 dp[target] == target 的时候,背包就装满了。
2、递推公式
01背包的递推公式为:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
本题,相当于背包里放入数值,那么物品i的重量是nums[i],其价值也是nums[i]。
所以递推公式:dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
3、初始化
// 题目中说:每个数组中的元素不会超过 100,数组的大小不会超过 200
// 总和不会大于20000,背包最大只需要其中一半,所以10001大小就可以了
vector<int> dp(10001, 0);
4、遍历顺序
对于一维背包,j逆序
5、举例推导dp数组
dp[j]的数值一定是小于等于j的。
如果dp[j] == j 说明,集合中的子集总和正好可以凑成总和j
class Solution {
public:
bool canPartition(vector<int>& nums) {
int sum = 0;
// dp[i]中的i表示背包内总和
// 题目中说:每个数组中的元素不会超过 100,数组的大小不会超过 200
// 总和不会大于20000,背包最大只需要其中一半,所以10001大小就可以了
vector<int> dp(10001, 0);
for (int i = 0; i < nums.size(); i++) {
sum += nums[i];
}
// 也可以使用库函数一步求和
// int sum = accumulate(nums.begin(), nums.end(), 0);
if (sum % 2 == 1) return false;
int target = sum / 2;
// 开始 01背包
for(int i = 0; i < nums.size(); i++) {
for(int j = target; j >= nums[i]; j--) { // 每一个元素一定是不可重复放入,所以从大到小遍历
dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
}
cout <<dp[0]<<dp[1]<<dp[2]<<dp[3]<<dp[4]<<dp[5]<<dp[6]<<dp[7]<<dp[8]<<dp[9]<<dp[10]<<dp[11]<<endl;
}
// 集合中的元素正好可以凑成总和target
if (dp[target] == target) return true;
return false;
}
};
// 二维的版本
class Solution {
public:
bool canPartition(vector<int>& nums) {
int sum = 0;
for(int i = 0; i < nums.size(); i ++){
sum += nums[i];
}
if(sum % 2 == 1) return false;
sum /= 2;
vector<vector<int>> dp(nums.size(), vector<int>(sum + 1, 0));
for(int j = nums[0]; j <= sum; j++){
dp[0][j] = nums[0];
}
for(int i = 1; i < nums.size(); i++){
for(int j = 0; j <= sum; j++){
if(j < nums[i]) dp[i][j] = dp[i - 1][j];
else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - nums[i]] + nums[i]);
}
}
// return dp[nums.size()-1][sum];
if(dp[nums.size()-1][sum] == sum) return true;
return false;
}
};
1049.最后一块石头的重量 II
本题就和 昨天的 416. 分割等和子集 很像了,可以尝试先自己思考做一做。
题目链接:1049最后一块石头的重量 - 力扣(LeetCode)
视频讲解:动态规划之背包问题,这个背包最多能装多少?LeetCode:1049.最后一块石头的重量II_哔哩哔哩_bilibili
class Solution {
public:
int lastStoneWeightII(vector<int>& stones) {
int sum = 0;
for(int i = 0; i < stones.size(); i ++){
sum += stones[i];
}
int target = sum / 2;
vector<int> dp(target + 1, 0);
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] - dp[target];
}
};
494.目标和
大家重点理解 递推公式:dp[j] += dp[j - nums[i]],这个公式后面的提问 我们还会用到。
left 表示将要设置符号为+的元素
right 表示将要设置符号为-的元素
left + right = sum
left - right = target
left - (sum - left) = target
left = (target + sum) / 2;
如果left不能整除,其实是凑不出来的,如果不能嗯整除就return 0 就行
转化成从nums挑选出和为left的元素们,有多少种可能
视频讲解:动态规划之背包问题,装满背包有多少种方法?| LeetCode:494.目标和_哔哩哔哩_bilibili
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
// vector<int> dp(501,0);
int sum = 0;
int left;
for(int i = 0; i < nums.size(); i++) sum += nums[i];
if((target + sum) % 2 == 1) return 0;
left = (target + sum) / 2;
vector<int> dp(left + 1, 0);
dp[0] = 1; // ??
for(int i = 0; i < nums.size(); i++){
for(int j = left; j >= nums[i]; j--){
// dp[j] = dp[j] + dp[j - nums[i]];
dp[j] += dp[j - nums[i]];
}
}
return dp[left];
}
};
474.一和零
通过这道题目,大家先粗略了解, 01背包,完全背包,多重背包的区别,不过不用细扣,因为后面 对于 完全背包,多重背包 还有单独讲解。
视频讲解:动态规划之背包问题,装满这个背包最多用多少个物品?| LeetCode:474.一和零_哔哩哔哩_bilibili
后面的两道题目,都是完全背包的应用,做做感受一下
完全背包理论基础
视频讲解:带你学透完全背包问题! 和 01背包有什么差别?遍历顺序上有什么讲究?_哔哩哔哩_bilibili
518.零钱兑换 II
视频讲解:动态规划之完全背包,装满背包有多少种方法?组合与排列有讲究!| LeetCode:518.零钱兑换II_哔哩哔哩_bilibili
377.组合总和 Ⅳ
视频讲解:动态规划之完全背包,装满背包有几种方法?求排列数?| LeetCode:377.组合总和IV_哔哩哔哩_bilibili
70. 爬楼梯 (进阶)
这道题目 爬楼梯之前我们做过,这次再用完全背包的思路来分析一遍
322. 零钱兑换
如果求组合数就是外层for循环遍历物品,内层for遍历背包。
如果求排列数就是外层for遍历背包,内层for循环遍历物品。
这句话结合本题 大家要好好理解。
视频讲解:动态规划之完全背包,装满背包最少的物品件数是多少?| LeetCode:322.零钱兑换_哔哩哔哩_bilibili
279.完全平方数
本题 和 322. 零钱兑换 基本是一样的,大家先自己尝试做一做
视频讲解:动态规划之完全背包,换汤不换药!| LeetCode:279.完全平方数_哔哩哔哩_bilibili
139.单词拆分
视频讲解:动态规划之完全背包,你的背包如何装满?| LeetCode:139.单词拆分_哔哩哔哩_bilibili
多重背包
你该了解这些!
背包问题总结篇
打家劫舍
198.打家劫舍 ——线性
视频讲解:动态规划,偷不偷这个房间呢?| LeetCode:198.打家劫舍_哔哩哔哩_bilibili
class Solution {
public:
int rob(vector<int>& nums) {
if(nums.size() == 0) return 0;
if(nums.size() == 1) return nums[0];
vector<int> dp(nums.size(), 0);
dp[0] = nums[0];
dp[1] = max(nums[0] , nums[1]);
for(int i = 2; i < nums.size(); i++){
dp[i] = max(dp[i-2] + nums[i] , dp[i-1]);
}
return dp[nums.size()-1];
}
};
213.打家劫舍II ——环形
视频讲解:动态规划,房间连成环了那还偷不偷呢?| LeetCode:213.打家劫舍II_哔哩哔哩_bilibili
class Solution {
public:
int rob(vector<int>& nums) {
if(nums.size() == 0) return 0;
if(nums.size() == 1) return nums[0];
int result1 = robRange(nums, 0, nums.size()-2);
int result2 = robRange(nums, 1, nums.size()-1);
return max(result1, result2);
}
int robRange(vector<int>& nums, int start, int end){
if(end == start) 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-2] + nums[i], dp[i-1]);
}
return dp[end];
}
};
337.打家劫舍III ——二叉树
class Solution {
public:
int rob(TreeNode* root) {
//后序遍历
//递归1 : 判断结束条件
if(root == NULL) return 0;
if(root->left == NULL && root->right == NULL) return root->val;
//偷当前节点
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 max(val1,val2);
}
};
//#include <stddef.h>
#include <windows.h>
#include <queue>
#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;
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) {}
};
TreeNode* buildTree(const vector<int>& vals) {
if (vals.empty()) return nullptr;
TreeNode *root = new TreeNode(vals[0]);
queue<TreeNode*> q;
q.push(root);
size_t i = 1;
while (!q.empty() && i < vals.size()) {
TreeNode *node = q.front();
q.pop();
if (vals[i] != NULL) {
node->left = new TreeNode(vals[i]);
q.push(node->left);
}
++i;
if (i < vals.size() && vals[i] != NULL) {
node->right = new TreeNode(vals[i]);
q.push(node->right);
}
++i;
}
return root;
}
class Solution {
public:
int rob(TreeNode* root) {
//后序遍历
//递归1 : 判断结束条件
if (root == NULL) return 0;
if (root->left == NULL && root->right == NULL) return root->val;
//偷当前节点
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 max(val1, val2);
}
};
//TreeNode *input= new TreeNode(3);
int main() {
//定义二叉树
//方法一:
//vector<int> values = { 3, 2, 3, NULL, 3, NULL, 1 };
//TreeNode *root = buildTree(values);
//方法二:
TreeNode* root = new TreeNode(3);
root->left = new TreeNode(2);
root->right = new TreeNode(3);
root->left->right = new TreeNode(3);
root->right->right = new TreeNode(1);
Solution ceshi;
std::cout << ceshi.rob(root) << std::endl;
}
买卖股票的最佳时机
121. 买卖股票的最佳时机
视频讲解:动态规划之 LeetCode:121.买卖股票的最佳时机1_哔哩哔哩_bilibili
class Solution {
public:
int maxProfit(vector<int>& prices) {
vector<vector<int>> dp(prices.size(),vector<int>(2, 0));
dp[0][0] = -prices[0];//持有
dp[0][1] = 0;//不持有
for(int i = 1; i < prices.size(); i++){
dp[i][0] = max(dp[i-1][0], -prices[i]);
dp[i][1] = max(dp[i-1][1], dp[i-1][0]+prices[i]);
}
return dp[prices.size()-1][1];
}
};
122.买卖股票的最佳时机II
视频讲解:动态规划,股票问题第二弹 | LeetCode:122.买卖股票的最佳时机II_哔哩哔哩_bilibili
方法一:贪心算法
class Solution {
public:
int maxProfit(vector<int>& prices) {
int res = 0;
for(int i = 1; i < prices.size(); i++){
if(prices[i] - prices[i-1] > 0) res += prices[i] - prices[i-1];
}
return res;
}
};
方法二:动态规划
class Solution {
public:
int maxProfit(vector<int>& prices) {
vector<vector<int>> dp(prices.size(), vector<int>(2,0));
dp[0][0] = -prices[0];//持有
dp[0][1] = 0;
for(int i = 1; i < prices.size(); 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[prices.size() - 1][1];
}
};
123.买卖股票的最佳时机III
这道题一下子就难度上来了,关键在于至多买卖两次,这意味着可以买卖一次,可以买卖两次,也可以不买卖。
视频讲解:动态规划,股票至多买卖两次,怎么求? | LeetCode:123.买卖股票最佳时机III_哔哩哔哩_bilibili
188.买卖股票的最佳时机IV
本题是123.买卖股票的最佳时机III 的进阶版
视频讲解:动态规划来决定最佳时机,至多可以买卖K次!| LeetCode:188.买卖股票最佳时机4_哔哩哔哩_bilibili
309.最佳买卖股票时机含冷冻期
本题加了一个冷冻期,状态就多了,有点难度,大家要把各个状态分清,思路才能清晰
视频讲解:动态规划来决定最佳时机,这次有冷冻期!| LeetCode:309.买卖股票的最佳时机含冷冻期_哔哩哔哩_bilibili
714.买卖股票的最佳时机含手续费
相对122.买卖股票的最佳时机II ,本题只需要在计算卖出操作的时候减去手续费就可以了,代码几乎是一样的,可以尝试自己做一做。
视频讲解:动态规划来决定最佳时机,这次含手续费!| LeetCode:714.买卖股票的最佳时机含手续费_哔哩哔哩_bilibili
子序列
一种是一个序列里的递增个数,一种是两个序列里的公共个数
一种问法是子序列,是可以索引不连续的;一种问法是要求索引连续的
注意dp的定义,常常需要要求以nums[i]为结尾之类的,因此还需要res取最大值
300.最长递增子序列
今天开始正式子序列系列,本题是比较简单的,感受感受一下子序列题目的思路。
视频讲解:动态规划之子序列问题,元素不连续!| LeetCode:300.最长递增子序列_哔哩哔哩_bilibili
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
if(nums.size() == 0) return 0;
int n = nums.size();
int res = 1;
vector<int> dp(n, 1);
for(int i = 0; i < nums.size(); i++){
for(int j = 0; j < i; j++){
if(nums[j] < nums[i]){
dp[i] = max(dp[i], dp[j] + 1);//取dp[j] + 1的最大值
}
}
res = res > dp[i] ? res : dp[i];
}
return res;
}
};
674. 最长连续递增序列
本题相对于昨天的动态规划:300.最长递增子序列 最大的区别在于“连续”。 先尝试自己做做,感受一下区别
视频讲解:动态规划之子序列问题,重点在于连续!| LeetCode:674.最长连续递增序列_哔哩哔哩_bilibili
class Solution {
public:
int findLengthOfLCIS(vector<int>& nums) {
int res = 1;
if(nums.size() == 0) return 0;
vector<int> dp(nums.size(), 1);
for(int i = 1; i < nums.size(); i ++){
if(nums[i] > nums[i-1]){
dp[i] = dp[i - 1] + 1;
}else{}
res = res > dp[i] ? res : dp[i];
}
return res;
}
};
718. 最长重复子数组
稍有难度,要使用二维dp数组了
视频讲解:动态规划之子序列问题,想清楚DP数组的定义 | LeetCode:718.最长重复子数组_哔哩哔哩_bilibili
class Solution {
public:
int findLength(vector<int>& nums1, vector<int>& nums2) {
// 定义dp + 初始化0
int res = 0;
vector<vector<int>> dp(nums1.size() + 1,vector<int>(nums2.size() + 1,0));
for(int i = 1; i < nums1.size() + 1; i++){
for(int j = 1; j < nums2.size() + 1; j++){
if(nums1[i - 1] == nums2[j - 1]){
dp[i][j] = dp[i - 1][j - 1] + 1;
if(res < dp[i][j]) res = dp[i][j];
}
}
}
return res;
}
};
1143.最长公共子序列
体会一下本题和 718. 最长重复子数组 的区别
视频讲解:动态规划子序列问题经典题目 | LeetCode:1143.最长公共子序列_哔哩哔哩_bilibili
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
int n1 = text1.size(), n2 = text2.size();
vector<vector<int>> dp(n1 + 1, vector<int>(n2 + 1, 0));
int res = 0;
for(int i = 1; i <= n1; i++){
for(int j = 1; j <= n2; j++){
if(text1[i - 1] == text2[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[n1][n2];
}
};
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
// initial dp
int res = 0;
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;
if(res < dp[i][j]) res = dp[i][j];
}else{
dp[i][j] = max(dp[i][j - 1], dp[i - 1][j]);//不好想
if(res < dp[i][j]) res = dp[i][j];
}
}
}
return res;
}
};
1035.不相交的线
其实本题和 1143.最长公共子序列 是一模一样的,大家尝试自己做一做。
视频讲解:动态规划之子序列问题,换汤不换药 | LeetCode:1035.不相交的线_哔哩哔哩_bilibili
53. 最大子序和
这道题我们用贪心做过,这次 再用dp来做一遍
贪心算法:
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int res = INT_MIN ;
int sum = 0 ;
for(int i = 0; i < nums.size(); i++){
sum = sum + nums[i];
if(sum < 0){
sum = 0;
}else{
res = res > sum ? res : sum;
}
}
return res ;
}
};
动态规划:
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int n = nums.size();
if(n == 0) return 0;
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 = res > dp[i] ? res : dp[i];
}
return res;
}
};
编辑距离
392.判断子序列
这道题目算是 编辑距离问题 的入门题目(毕竟这里只是涉及到减法),慢慢的,后面就要来解决真正的 编辑距离问题了
动态规划
双指针
class Solution {
public:
bool isSubsequence(string s, string t) {
int n = s.length(), m = t.length();
int i = 0, j = 0;
while (i < n && j < m) {
if (s[i] == t[j]) {
i++;
}
j++;
}
return i == n;
}
};
我的
class Solution {
public:
bool isSubsequence(string s, string t) {
int j ;
int index = 0;
for(int i = 0; i < s.size(); i ++){
for(j = index; j < t.size(); j++){
if(t[j] == s[i]) {
index = j + 1;
break;
}
}
if(j == t.size()) {
return false;
}
}
return true;
}
};
115.不同的子序列
但相对于刚讲过 392.判断子序列,本题 就有难度了 ,感受一下本题和 392.判断子序列 的区别。
583. 两个字符串的删除操作
本题和动态规划:115.不同的子序列 相比,其实就是两个字符串都可以删除了,情况虽说复杂一些,但整体思路是不变的。
72. 编辑距离
最终我们迎来了编辑距离这道题目,之前安排题目都是为了 编辑距离做铺垫。
编辑距离总结篇
做一个总结吧
647. 回文子串
动态规划解决的经典题目,如果没接触过的话,别硬想 直接看题解。
class Solution {
public:
int countSubstrings(string s) {
int n = s.size();
int res = 0;
if(n < 2) return n;
// n = 2,3,4...
// dp[i][j] = dp[i+1]dp[j-1] & s[i] == s[j]
//定义dp
vector<vector<bool>> dp(n,vector<bool>(n, false));//行数,列数,赋值false
for(int i = n - 1; i >= 0; i --){
for(int j = i; j < n; j++){
if(s[i] == s[j]){
if(j - i < 2){//长度为1的子串一定是回文!如果s[i]==s[j]那么长度为2的也一定是子串!
dp[i][j] = true;
res ++;
}else if(dp[i + 1][j - 1] == true){
dp[i][j] = true;
res ++;
}
}
}
}
return res;
}
};
class Solution {
public:
string longestPalindrome(string s) {
int n = s.size();
//int res = 0;
int maxStr = 1;//只要n >= 2,最小长度最小就是1
int begin = 0;//用来记起始位置
if(n < 2) return s;
// n = 2,3,4...
// dp[i][j] = dp[i+1]dp[j-1] & s[i] == s[j]
//定义dp
vector<vector<bool>> dp(n,vector<bool>(n, false));//行数,列数,赋值false
for(int i = n - 1; i >= 0; i --){
for(int j = i; j < n; j++){
if(s[i] == s[j]){
if(j - i < 2){//长度为1的子串一定是回文!如果s[i]==s[j]那么长度为2的也一定是子串!
dp[i][j] = true;
if(maxStr < j - i + 1){
maxStr = j - i + 1;
begin = i;
}
//res ++;
}else if(dp[i + 1][j - 1] == true){
dp[i][j] = true;
if(maxStr < j - i + 1){
maxStr = j - i + 1;
begin = i;
}
//res ++;
}
}
}
}
return s.substr(begin, maxStr);
}
};
class Solution {
public:
string longestPalindrome(string s) {
int n = s.size();
int maxLen = 1;//就算所有字符都不同,那么最小也是1!
//比如ab,dp[1][2] = false,但是子串长度是1
int begin = 0;
// 考虑 空字符串 与 单个字符
if(n < 2) return s;
// 定义dp
vector<vector<int>> dp(n, vector<int>(n));//dp[i][j]表示s[i..j]是否为回文串
// 初始化 所有长度为1的子串都是回文串
for(int i = 0; i < s.size(); i++){
dp[i][i] = true;
}
//枚举子串长度
for(int L = 2; L <= n; L++){
//枚举左边界
for(int i = 0; i < n; i++){
int j = i + L - 1;//左闭右闭
if(j >= n) break; //如果右边界过界,跳出循环
if(s[i] != s[j]) dp[i][j] = false;//边界不满足跳出循环
else{//边界满足
if(j - i < 3){//L = 0, 1, 2,也是为了写递推函数的时候不越界
dp[i][j] = true;
}else{
dp[i][j] = dp[i + 1][j - 1];
}
}
// 统计回文子串的长度和起点位置
if(dp[i][j] == true && maxLen < L){
maxLen = L ;
begin = i;
}
}
}
return s.substr(begin, maxLen);
}
};
516.最长回文子序列
647. 回文子串,求的是回文子串,而本题要求的是回文子序列, 大家要搞清楚两者之间的区别。
https://programmercarl.com/0516.%E6%9C%80%E9%95%BF%E5%9B%9E%E6%96%87%E5%AD%90%E5%BA%8F%E5%88%97.html
动态规划总结篇
https://programmercarl.com/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E6%80%BB%E7%BB%93%E7%AF%87.html