除了C++基础最重要的就是算法了
刷力扣到目前为止一共就是刷了几十道吧,并且之前刷力扣都是刷完之后就忘了,没有认真总结过。而且还要学习一些网络编程、系统编程知识,心思顾不上那么多。
现在从动态规划开始认真总结,每天刷个几道,不能断,希望自己坚持住。
不要认为做了就是会了,要真正搞懂;不要认为搞懂了就能真正写出来了,要自己能独立写出来;不能怕麻烦,要有耐心
以下内容参考了许多代码随想录:代码随想录
动态规划,英文:Dynamic Programming,简称DP,如果某一问题有很多重叠子问题,使用动态规划是最有效的
动规是由前一个状态推导出来的,而贪心是局部直接选最优的(贪心、动态规划、二叉树、回溯)
在后面解题都是按照这个步骤,解题步骤分为五步:
动态规划最容易出现代码错误,最好方法是:打印出dp数组,所以第一步很重要
写代码之前一定要把递推公式在dp数组的上具体情况模拟一遍,心中有数,确定最后推出的是想要的结果。然后再写代码,如果代码没通过就打印dp数组
动态规划的题:
分为基础题目、背包问题、打家劫舍、股票问题、子序列问题
题目都较为简单,主要是为了打基础,重点练习解题五步骤
尤其是斐波那契数什么都给出来了,而其他的也可以转化为斐波那契数的解题
力扣
确定dp数组(dp table)以及下标的含义
vector dp(n+1);
其中dp[i] i就是是第i个斐波那契数,dp[i]就是斐波那契数
确定递推公式
dp[i] = dp[i - 1] + dp[i - 2];
dp数组如何初始化
dp[0] = 0;dp[1] = 1;
确定遍历顺序
从前到后
举例推导dp数组
vector < int > v;
这时候v的size为0,如果直接进行访问 v[i] 会报错。
这里可以使用 v.resize(n),或者v.resize(n, m) 来初始化
前者是使用n个0来初始化,后者是使用n个m来初始化。
初始化时尽量使用vector < int > v(n);初始化n个元素为0的数组
class Solution {
public:
int fib(int n) {
if(n <= 1) {return n;}//这里不能忘了,否则当n=1时,会返回初始化dp[1]的1
vector dp(n + 1);//初始化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];
}
};
力扣
爬楼梯其实就是在当前楼梯可以上一阶也可以上两阶,所以如果是n阶台阶,那么要不然他是n-1阶或者是n-2阶上来的
可转化为斐波那契数公式,步骤同斐波那契数
只是要多初始化一个dp[2]
class Solution {
public:
int climbStairs(int n) {
if (n <= 2) return n;
vector 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];
}
};
力扣
确定dp数组(dp table)以及下标的含义
int n = cost.size();
vector dp(n+1);
dp[i]表示第i个台阶的最低花费
确定递推公式
dp[i] = min(dp[i-1] + cost[i-1], dp[i-2]+cost[i-2]);
dp数组如何初始化
dp[0] = 0;
dp[1] = 0;
确定遍历顺序
从前到后
举例推导dp数组
class Solution {
public:
int minCostClimbingStairs(vector& cost) {
int n = cost.size();
vector dp(n);
dp[0] = 0;
dp[1] = 0;
for (int i = 2; i < n; i ++) {
dp[i] = min(dp[i-1] + cost[i-1], dp[i-2]+cost[i-2]);
}
return min(dp[n-1]+cost[n-1] ,dp[n-2] +cost[n-2]);
}
};
刚开始自己想的是上面的,但是不够简洁,看到return那里可以挪动到for循环里,只用dp数组建立为dp(n+1)
从这里看出来自己代码能力弱。。。。。。
class Solution {
public:
int minCostClimbingStairs(vector& cost) {
int n = cost.size();//cost数组的大小用以作为dp数组大小
vector dp(n+1); //主要是改变了这里
for (int i = 2; i <= n; i ++) {
dp[i] = min(dp[i-1] + cost[i-1], dp[i-2]+cost[i-2]);
}
return dp[n];
}
};
力扣
这个题刚上来一脸懵逼,但是仔细想想是一维变成了二维,那么就按照前面一维的思路+二维大的特点来写
另外二维数组初始化:
vector
需要知道的是起点是[0][0],终点是[m-1][n-1]
dp数组下标及其含义
vector> dp(m,vector(n))
数组dp[i][j]表示从起始位置[0][0]到[i][j]的总的路径数
递推公式
dp[i][j] = dp[i-1][j] + dp[i][j-1]
数组初始化
由于是二维数组:初始化需要将dp[0][j]和dp[i][0]都要初始化,
因为从[0][0]到[0][j]或者[i][0]都是一条路径
for(int i = 0; i < m; i++) {
dp[i][0] = 1;
}
for(int i = 0; i < n; i++) {
dp[0][i] = 1;
}
遍历顺序
从左到右,从上到下
举例
class Solution {
public:
int uniquePaths(int m, int n) {
vector> dp(m, vector(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++){//注意这里i和j都是1
for(int j = 1; j < n; j++){
dp[i][j] = dp[i][j-1] + dp[i-1][j];
}
}
return dp[m-1][n-1];
}
};
力扣
这题又说明了自己的菜鸡编程水平。。。。有思路就是写不出来障碍物的具体表示,写完了又调试了好久
解题步骤和不同路径相同,不同路径II说白了就是不同路径加了一个障碍物,分为:
障碍物在终点和起点;
if (obstacleGrid[m - 1][n - 1] == 1 || obstacleGrid[0][0] == 1) //如果在起点或终点出现了障碍,直接返回0
return 0;
障碍物在第一排或者第一列;
for (int i = 0; i < m && obstacleGrid[i][0] == 0; i++) dp[i][0] = 1;
for (int j = 0; j < n && obstacleGrid[0][j] == 0; j++) dp[0][j] = 1;
障碍物在其他地方
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
if (obstacleGrid[i][j] == 1) continue;//障碍物在其他地方
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
和不同路径不同的还有要求出二维数组的size
行的size:obstacleGrid.size()
列的size:obstacleGrid[0].size()
class Solution {
public:
int uniquePathsWithObstacles(vector>& obstacleGrid) {
int m = obstacleGrid.size();
int n = obstacleGrid[0].size();
if(obstacleGrid[0][0] == 1 || obstacleGrid[m-1][n-1] == 1) {
return 0;
}
vector> dp(m, vector(n,0));
for(int i = 0; i < m && obstacleGrid[i][0] == 0; i++) {
dp[i][0] = 1;
}
for(int j = 0; j < n && obstacleGrid[0][j] == 0; j++) {
dp[0][j] = 1;
}
for(int i = 1; i < m; i++){
for(int j = 1; j < n; j++){
if (obstacleGrid[i][j] == 1){
continue;
}
dp[i][j] = dp[i][j-1] + dp[i-1][j];
}
}
return dp[m-1][n-1];
}
};
力扣
递推公式是看了代码随想录,这题的代码倒是好写,就是递推公式还是相当难想的
确定dp数组,以及下标
dp[i]表示拆分数字i所得到的最大乘积
递推公式
dp[i]是因为需要从零开始遍历,更新dp[i];
j*(i-j)是拆分出来两个
j*dp[i-1]是拆分出来两个以上
dp[i] = max(dp[i],j*(i-j),j*dp[i-1]);
数组初始化
dp[2] = 1;
其实隐含了dp[0] = 0;dp[1] = 0
确定递推方向
从前到后
举例
class Solution {
public:
int integerBreak(int n) {
vector dp(n+1);
dp[2] = 1;
for(int i = 3; i<=n; i++) {
for(int j = 1; j <= i / 2; j++){
dp[i] = max({dp[i], j*dp[i-j],j*(i - j)});
}
}
return dp[n];
}
};
力扣
先略过,等到看完二叉树回来看!
掌握01背包、完全背包(多重背包看情况,主要是01背包、完全背包,力扣上好像没有多重背包)
完全背包是01背包稍作变化而来,即:完全背包的物品数量是无限的
背包问题的理论基础重中之重是01背包
下面是背包问题的图:
力扣都是01背包应用方面的题目,也就是需要转化为01背包问题,重点就是如何转化为01背包问题。
解决01背包分为使用一维数组和二维数组,利用一道题目进行讲解:
背包重量bagweight、物品重量weight,利用数组weight、背包价值value,利用数组value
重量 | 价值 | |
物品0 | 1 | 15 |
物品1 | 3 | 20 |
物品2 | 4 | 30 |
问背包能背的物品最大价值?
一维数组
确定数组dp以及下标
dp[j]表示背包容量为j时的最大价值
递推公式
i表示物品
dp[j] = max(dp[j], dp[j-weight[i]]+value[i])
其中,max中的dp[j]表示如果不放物品i,用上次背包容量j的最大价值;(有两个for循环,第一个for循环用来更新物品i,第二个for循环用来更新背包容量j)
dp[j-weight[i]]+value[i]表示当背包容量(重量)j能够容得下物品i的重量,则此时的物品价值
初始化数组
一维数组初始化dp[j=0],背包容量为0时最大价值肯定也为零,即dp[0]=0
遍历顺序(第二个for循环一定要是倒序的)
for(int i=0; i=weight[i]; j--){
dp[j] = max(dp[j], dp[j-weight[i]]+value[i])
}
}
举例说明
如果正序会导致,多次放入同一物品
那么打印出来的数组应该是:
当i=0,此时只有物品0:
0 | 15 | 15 | 15 | 15 |
当i=1,此时有物品0和1:
20 | 35 |
当i=2,此时有物品0、1和2:
35 |
注意是:从后向前更新dp数组,储存最大值
第一层先遍历i=0,直到i=物品数组weight.size()(物品数)
第二层先遍历j=最大背包容量bagweight,直到j=weight[i](直到背包容量不能容下当前遍历的i的物品)
void test_1_wei_bag_problem() {
vector weight = {1, 3, 4};
vector value = {15, 20, 30};
int bagWeight = 4;
// 初始化
vector dp(bagWeight + 1, 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();
}
二维数组
一维相比较二维,代码量减少了许多,所以使用一维的比较好
二维数组和一维数组类似,就是不用更新dp数组,具体可查看代码随想录 (programmercarl.com)
void test_2_wei_bag_problem1() {
vector weight = {1, 3, 4};
vector value = {15, 20, 30};
int bagweight = 4;
// 二维数组
vector> dp(weight.size(), vector(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();
}
力扣
此题照着上面的01背包做一下
需要明确:
bagweight = sum/2;
weight=value=nums[i]
步骤:
确定数组和下标:
dp[j]表示背包重量为j时的最大容量价值
动态规划公式:
dp[j] = max(dp[j],dp[j - nums[i]] + nums[i]);
初始化:
dp[0]=0;
遍历顺序:
for(int i = 0; i < nums.size(); i++) {
for(int j = sum / 2; j >= nums[i]; j--){
dp[j] = max(dp[j],dp[j - nums[i]] + nums[i]);
}
}
class Solution {
public:
bool canPartition(vector& nums) {
int sum = 0;
for (int a = 0; a < nums.size(); a++) {
sum += nums[a];
}
if (sum % 2 == 1) return false;//如果总和除去2,余1说明要返回false
vector dp(20000,0);
for(int i = 0; i < nums.size(); i++) {
for(int j = sum / 2; j >= nums[i]; j--) {
dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
}
}
return dp[sum / 2] == sum / 2;
}
};
力扣
此问题与上一题及其相似,只不过返回值不一样,这一题返回sum-dp[sum/2]-dp[sum/2]
dp[sum/2]代表粉碎的石头,在计算target的时候,target = sum / 2 因为是向下取整,所以sum - dp[target] 一定是大于等于dp[target]的
确定dp数组以及下标:
dp[j]表示背包容量为j时的最大容量价值
动态规划公式:
dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
初始化:
dp[0] = 0;
遍历顺序
for(int i = 0; i < stones.size(); i++) {
for(int j = sum / 2; j > stones[i]; j--){
dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
}
}
class Solution {
public:
int lastStoneWeightII(vector& stones) {
int sum = 0;
for(int a = 0; a < stones.size(); a++) {
sum += stones[a];
}
vector dp(20000,0);
for(int i = 0; i < stones.size(); i++) {
for(int j = sum / 2; j >= stones[i]; j--){
dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
}
}
return sum - dp[sum / 2] - dp[sum / 2];
}
};
力扣
这题有难度啊---动态规划公式需要推导
这个题真的是一点思路都没有。。。。。。硬往动态规划上凑也凑不出来,,体会到确定dp数组和下标对于写动态规划的重要性
参考代码随想录 (programmercarl.com),光看这里的动态规划公式推导都看了半天,不过真的很巧妙,想明白了其实挺简单
首先需要说明dp[j]表示背包容量为j时,有dp[j]种填充满的方法
利用nums = [1,1,1,1,1], s = 3举例
bagsize = (sum + s) / 2,为什么呢?因为要利用nums数组里的元素加或者减得到最后的结果s,元素和设为x,则元素减为sum-x,从而x - (sum-x) = s,只有sum是个定值,所以x可表示s
得x = (sum + s) / 2,所以只用求出nums数组里的元素加为x有多少种方法,就可求出运算结果等于 target
的不同 表达式 的数目
即dp[j]表示背包容量为j时,有dp[j]种填充满的方法
然后说明动态规划公式由来
代码随想录 (programmercarl.com)里面说明简洁明了,直接截屏为下图
五步骤
确定dp数组和下标
dp[j]表示背包容量为j时,有dp[j]种填充满的方法
动态规划公式
dp[j] += dp[j - nums[i]];
初始化
dp[0] = 1;
就算只有一个元素,也应该有一种方法(如果数组[0] ,target = 0,那么 bagSize = (target + sum) / 2 = 0。 dp[0]也应该是1, 也就是说给数组里的元素 0 前面无论放加法还是减法,都是 1 种方法。)
遍历顺序
for(int i = 0; i < nums.size(); i++) {
for (int j = bagsize; j >= nums[i]; j--) {
dp[j] += dp[j - nums[i]];
}
}
举例
代码:
class Solution {
public:
int findTargetSumWays(vector& nums, int target) {
int sum = 0;
for (int a = 0; a < nums.size(); a++) {
sum += nums[a];
}
int bagsize = (sum + target) / 2;
if ((sum + target) % 2 == 1) { //这种没有方案
return 0;
}
if (abs(target) > sum) { //abs函数求绝对值 //这种也没有方案
return 0;
}
vector 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];
}
};
力扣
较难,这一题先过
确定dp数组和下标
dp[j]表示背包容量为j时最大子集
动态规划公式
dp[j] =
初始化
遍历顺序
举例
完全背包和01背包问题唯一不同的地方就是,每种物品有无限件
同样leetcode上没有纯完全背包问题,都是需要完全背包的各种应用,需要转化成完全背包问题
完全背包就是每件物品都有无限个(也就是可以放入背包多次),这个和01背包体现在代码上就是在遍历第二层for循环时,采用正序
01背包内嵌的循环是从大到小遍历,为了保证每个物品仅被添加一次
完全背包内嵌的循环是从小到大遍历,为了保证每个物品可多次被添加
力扣
这一题和01背包中的目标和很像,哈哈哈哈,就是遍历顺序不同
不用怎么思考就能做出来
确定数组dp以及下标
dp[j]表示达到总金额j的组合数
动态规划公式
dp[j] += dp[j - coins[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]];
}
}
举例
代码:
class Solution {
public:
int change(int amount, vector& coins) {
vector dp(amount + 1);
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];
}
};
注意:物品和背包容量的遍历顺序不同,涉及到了是排列还是组合:
外层物品内层背包容量:是组合(本题),不强调元素之间的顺序
外层背包容量内层物品:是排列(下一题),强调元素之间的顺序
力扣
这题就是排列问题,与上题不同的是:遍历顺序
直接给出代码,五步骤参考上题
另外:遍历的变化,要加上i - nums[j] >= 0判断,另外这题的dp[i]+dp[i - nums[j]]还要
class Solution {
public:
int combinationSum4(vector& nums, int target) {
vector 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] >= 0 && dp[i] < INT_MAX - dp[i - nums[j]]) {
dp[i] += dp[i - nums[j]];
}
}
}
return dp[target];
}
};
力扣
题目解答是转化为斐波那契数,属于简单题
代码随想录 (programmercarl.com)将其转化为了一步一个台阶,两个台阶,三个台阶,.......,直到 m个台阶。问有多少种不同的方法可以爬到楼顶呢?
1阶,2阶,.... m阶就是物品,楼顶就是背包。
每一阶可以重复使用,例如跳了1阶,还可以继续跳1阶。
问跳到楼顶有几种方法其实就是问装满背包有几种方法
这样就和组合总和IV属于同一问题了
class Solution {
public:
int climbStairs(int n) {
vector dp(n + 1, 0);
dp[0] = 1;
for (int i = 1; i <= n; i++) { // 遍历背包
for (int j = 1; j <= m; j++) { // 遍历物品
if (i - j >= 0) dp[i] += dp[i - j];
}
}
return dp[n];
}
};
力扣
真的是难受,,就是过了个周末回来前面积攒的动态规划的知识好多都不能形成体系,脑子里一片浆糊。。。。服服服了,,,啊啊啊啊啊
这个题最重要的是能够想出动态规划公式:
刚开始想的是:dp[j] = min(dp[j], dp[j - coins[i]]),但是很明显的是dp[j - coins[i]]中的coins[i]便是硬币数组中的一个硬币的价值,dp[j - coins[i]]表示凑够价值为j - coins[i]的最小硬币数,所以再加个1就表示dp[j]的硬币数。
确定dp数组以及下标
dp[j]表示凑成总金额j所需的最小硬币数
动态规划公式
dp[j] = min(dp[j], dp[j - coins[i]]+ 1)
初始化
dp[0] = 0
遍历顺序
for(int i = 0; i < coins.size(); i++) {
for(int j = coins; j <= amount; j++){
dp[j] = min(dp[j], dp[j - coins[i]]+ 1);
}
}
举例
代码(代码随想录 (programmercarl.com))
class Solution {
public:
int coinChange(vector& coins, int amount) {
vector 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 - coins[i]]已经达到了最大值了
dp[j] = min(dp[j], dp[j - coins[i]]+ 1);
}
}
}
if(dp[amount] == INT_MAX) return -1;
return dp[amount];
}
};
力扣
在初始化dp时一旦动态规划公式求最小,一定要初始为最大值,这样dp[j]在递推的时候才不会被初始值覆盖
求最大时同理
确定数组dp以及下标
dp[j]表示整数j的最少完全平方数量
pi[i]表示i的完全平方
动态规划公式
dp[j] = min(dp[j], dp[j - pi[i]] + 1);
初始化
题目规定n大于1
//dp[1] = 1;
一定要初始化为dp[0] = 0,其代表了和为0的完全平方数的最小数量,那么dp[0]一定是0
遍历顺序
for(int i = 1; i <= n; i++) {
pi[i] = i * i;
for(int j = pi[i]; j <= n;j++){
dp[j] = min(dp[j], dp[j - pi[i]] + 1);
}
}
代码
class Solution {
public:
int numSquares(int n) {
vector dp(n+1, INT_MAX);
vector pi(n + 1, 0);
dp[0] = 0;
for(int a = 1; a <= n; a++) {
pi[a] = a * a;
}
for(int i = 1; i <= n; i++) {
for(int j = pi[i]; j <= n; j++) {
if (dp[j - pi[i]] != INT_MAX){
dp[j] = min(dp[j], dp[j - pi[i]] + 1);
}
}
}
return dp[n];
}
};
力扣
字典wordDict里的单词代表了物品,字符串s代表背包
这个题有思路,但是要是具体做出来还是十分费力的。先略过
数组dp以及下标
dp[j]表示字典元素的组合方式
动态规划公式
dp[j] =
力扣
这个题能够感受到当前状态和之前的状态有关,但是对于递推公式自己还无法相处出来
现在对于递推公式感觉一定要在当前状态推之前状态,当前状态是怎么收到之前状态的影响。
比如这个题:当前状态dp[i]表示偷盗包括第i个房间的小偷偷盗的最高金额。
那个当前状态受前面状态的影响,当前的i房间是可偷盗或者不偷盗的,当前不偷盗就说明第i-1个房间偷盗的总金额更高
当前的i房间偷盗的话就是dp[i - 2] + nums[i]总金额最高
确定dp以及数组下标
dp[i]表示偷盗包含i房间的总金额
动态规划公式
dp[i] = max(dp[i - 1], dp[i - 2] + nums[i]);
初始化
dp[0] = nums[0];
dp[1] = max(nums[0], nums[1]);
遍历顺序
从前到后
代码
class Solution {
public:
int rob(vector& nums) {
vector dp(nums.size());
if (nums.size() == 1) {//不加无法通过nums = [0]
return nums[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];
}
};
力扣
打家劫舍这几个题真有意思,这次打家劫舍升级版,都是专业的小偷,哈哈哈哈
这个题就是区分出来要不要偷首尾,所以要区分成:
去掉nums[start],即不偷首家
去掉nums[end],即不偷危机尾家。
其他和上一题是一样的,直接上代码:
// 注意注释中的情况二情况三,(就是去掉尾家和首家)以及把198.打家劫舍的代码抽离出来了
class Solution {
public:
int rob(vector& 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);
}
// 198.打家劫舍的逻辑
int robRange(vector& nums, int start, int end) {
if (end == start) return nums[start];
vector dp(nums.size());
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];
}
};
力扣
这个题是和二叉树有关的,目前对二叉树还不太了解,等回来做
力扣
这个题第一个想到的就是暴力解法,但是由于算法超时不会通过
以下解法均来自代码随想录代码随想录 (programmercarl.com)
个人是第一次发现二维数组还可以这么用
dp[i][0]表示第i天持有股票所拥有的现金,所以:dp[i][0] = max(dp[i - 1][0], -prices[i])
dp[i][1]表示第i天不持有股票锁拥有的现金,所以:dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i])
初始化 dp[0][0] = -prices[0](第一天就买)
dp[0][1] = 0(第一天卖出)
真的是巧妙啊
class Solution {
public:
int maxProfit(vector& prices) {
int len = prices.size();
if (len == 0) return 0;
vector> dp(len, vector(2));
dp[0][0] -= prices[0];
dp[0][1] = 0;
for(int i = 1; i < len; i++) {//注意这里是i = 1,不是i = 0
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[len - 1][1];
}
};
力扣
首先这题可以这样解
class Solution {
public:
int maxProfit(vector& prices) {
int result = 0;
for (int i = 0; i < prices.size() - 1; i++) {
if ((prices[i + 1] - prices[i]) > 0) {
//result +=max(prices[i + 1] - prices[i],0);
result +=prices[i + 1] - prices[i];
}
}
return result;
}
};
动态规划:
如果第i天持有股票即dp[i][0], 那么可以由两个状态推出来
- 第i-1天就持有股票,那么就保持现状,所得现金就是昨天持有股票的所得现金 即:dp[i - 1][0]
- 第i天买入股票,所得现金就是昨天不持有股票的所得现金减去 今天的股票价格 即:dp[i - 1][1] - prices[i]
class Solution {
public:
int maxProfit(vector& prices) {
int len = prices.size();
vector> dp(len, vector(2, 0));
dp[0][0] -= prices[0];
dp[0][1] = 0;
for (int i = 1; i < len; i++) {
dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]); // 注意这里是和121. 买卖股票的最佳时机唯一不同的地方。
dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i]);
}
return dp[len - 1][1];
}
};
力扣
一天一共有五个操作:
0、没有操作
dp[i][0] = dp[i - 1][0]
1、第一次持有股票
有两种方式:之前天就拥有股票,或者是当天持有股票
dp[i][1] = max(dp[i - 1][0] - prices[i], dp[i - 1][1])
2、第一次不持有股票(同1)
dp[i][2] = max(dp[i - 1][2], dp[i - 1][1] + prices[i])
3、第二次持有股票
dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] - prices[i])
4、第二次不持有股票
dp[i][4] = max(dp[i - 1][4], dp[i - 1][3] + prices[i])
初始化:
dp[0][0] = 0;
dp[0][1] = -prices[0];
dp[0][2] = 0;
dp[0][3] = -prices[0];
dp[0][4] = 0;
代码:
class Solution {
public:
int maxProfit(vector& prices) {
if (prices.size() == 0) return 0;
vector> dp(prices.size(), vector(5, 0));
dp[0][1] = -prices[0];
dp[0][3] = -prices[0];
for (int i = 1; i < prices.size(); i++) {
dp[i][0] = dp[i - 1][0];
dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
dp[i][2] = max(dp[i - 1][2], dp[i - 1][1] + prices[i]);
dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] - prices[i]);
dp[i][4] = max(dp[i - 1][4], dp[i - 1][3] + prices[i]);
}
return dp[prices.size() - 1][4];
}
};
力扣
这一题和上一题的区别在于将最多可以买卖两次换成了k次,跟上题同理就是将二维数组的
其实就是偶数卖出,奇数买入
dp[i][j+1]和dp[i][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)
class Solution {
public:
int maxProfit(int k, vector& prices) {
if (prices.size() == 0) return 0;
vector> dp(prices.size(), vector(2 * k + 1, 0));
for (int j = 1; j < 2 * k; j += 2) {
dp[0][j] = -prices[0];
}
for (int i = 1;i < prices.size(); 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[prices.size() - 1][2 * k];
}
};
力扣
买卖股票再加上一个冷冻期
则状态0、持有股票
dp[i][0] = max({dp[i - 1][3] - prices[i], dp[i - 1][0], dp[i - 1][1] + prices[i]});
状态1、不持有股票状态(之前就不持有股票或者是之前)
dp[i][1] = max(dp[i - 1][1], dp[i-1][3])
状态2、当天不持有股票
dp[i][2] = dp[i - 1][0] + prices[i]
状态3、冷冻期
dp[i][3] = dp[i- 1][2]
class Solution {
public:
int maxProfit(vector& prices) {
int n = prices.size();
if (n == 0) return 0;
vector> dp(n, vector(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][3] - prices[i], 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];
}
return max(dp[n - 1][3], max(dp[n - 1][1], dp[n - 1][2]));
}
};
力扣
这个题相比较买卖股票的最佳时机II就是多加了一个手续费,在卖出时加上就行
class Solution {
public:
int maxProfit(vector& prices, int fee) {
int n = prices.size();
vector> dp(n, vector(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 max(dp[n - 1][0], dp[n - 1][1]);
}
};
子序列问题是动态规划解决的经典问题,当前下标i的递增子序列长度,其实和i之前的下标j的子序列长度有关系
力扣
一定要知道动态规划公式中,dp[j - 1] + 1就是为了寻找最大递增子数组,而dp[i] = max(dp[i], dp[j - 1] + 1)中的dp[i]是为了不被覆盖掉大的递增子数组
确定dp数组以及其下标
dp[i]表示包括nums[i]的最长递增子序列的长度
动态规划公式
if(nums[i] > nums[j]) dp[i] = max(dp[i], dp[j - 1] + 1)
初始化
dp[0] = 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)
}
if(dp[i] > result) result = dp[i];
}
class Solution {
public:
int lengthOfLIS(vector& nums) {
if (nums.size() <= 1) return nums.size();
vector dp(nums.size(), 1);
int result = 0;
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]; // 取长的子序列
}
return result;
}
};
力扣
718连续的关键点就是dp[i][j]只能由dp[i-1][j-1]来推出来;
这个题不连续的关键点是还可以由text1[i - 1] 与 text2[j - 1]不相同,那就看看text1[0, i - 2]与text2[0, j - 1]的最长公共子序列 和 text1[0, i - 1]与text2[0, j - 2]的最长公共子序列,取最大的
这个题主要就是确定递推公式
如果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]的最长公共子序列,取最大的
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
vector> dp(text1.size() + 1, vector(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()];
}
};
力扣
本来觉着这个题最重要的就是如何确定相等时不相交
直线不能相交,这就是说明在数组A中 找到一个与数组B相同的子序列,且这个子序列不能改变相对顺序,只要相对顺序不改变,链接相同数字的直线就不会相交。
其实要确定,就是在求两个数组的最长公共子数组,和上一个题一模一样。
class Solution {
public:
int maxUncrossedLines(vector& A, vector& B) {
vector> dp(A.size() + 1, vector(B.size() + 1, 0));
for (int i = 1; i <= A.size(); i++) {
for (int j = 1; j <= B.size(); j++) {
if (A[i - 1] == B[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[A.size()][B.size()];
}
};
力扣
最长连续递增序列相比较最长递增序列的代码区别就是:
一维数组表示就行,话不多说,五步骤也不写了,思路和最长递增序列相似,直接看代码
这么来看连续递增序列和不连续的递增序列:一个是用一维,一个是用二维
class Solution {
public:
int findLengthOfLCIS(vector& nums) {
if (nums.size() == 0) return 0;
int result = 1;
vector dp(nums.size() ,1);
for (int i = 1; i < nums.size(); i++) {
if (nums[i] > nums[i - 1]) { // 连续记录
dp[i] = dp[i - 1] + 1;
}
if (dp[i] > result) result = dp[i];
}
return result;
}
};
力扣
连续的关键点就是dp[i][j]只能由dp[i-1][j-1]来推出来
在确定dp数组以及下标时不定义以下标i为结尾的原因?
class Solution { public: int findLength(vector
& nums1, vector & nums2) { vector > dp (nums1.size() + 1, vector (nums2.size() + 1, 0)); int result = 0; // 要对第一行,第一列经行初始化 for (int i = 0; i < nums1.size(); i++) if (nums1[i] == nums2[0]) dp[i][0] = 1; for (int j = 0; j < nums2.size(); j++) if (nums1[0] == nums2[j]) dp[0][j] = 1; for (int i = 0; i < nums1.size(); i++) { for (int j = 0; j < nums2.size(); j++) { if (nums1[i] == nums2[j] && i > 0 && j > 0) { // 防止 i-1 出现负数 dp[i][j] = dp[i - 1][j - 1] + 1; } if (dp[i][j] > result) result = dp[i][j]; } } return result; } };
确定dp数组以及下标
dp[i][j]表示以下标i-1为结尾的A,和以下标i-1为结尾的B,最长重复子数组的长度为dp[i][j]
动态规划公式
由dp的定义可知当A[i-1]和B[i-1]相等时,dp+1。则:
dp[i][j] = dp[i-1][j-1] + 1;
初始化
dp[i][0] = 0;dp[0][j] = 0
为什么要这么定义,是因为:举个例子A[0]如果和B[0]相同的话,dp[1][1] = dp[0][0] + 1,只有dp[0][0]初始为0,正好符合递推公式逐步累加起来
遍历顺序
从动态规划公式可看出,从i = i; j = 1开始
由于dp数组定义的是以下标i-1为结尾的A,和以下标i-1为结尾的B,所以i和j都要到=A.size()和=B.size()
for (int i = 1; i <= A.size(); i++) {
for (int j = 1; j <= B.size(); j++) {
dp[i][j] = dp[i-1][j-1] + 1;
}
}
代码:
class Solution {
public:
int findLength(vector& nums1, vector& nums2) {
vector> dp (nums1.size() + 1, vector(nums2.size() + 1, 0));
//将dp[i][0]和dp[0][j]都初始化为0,在dp定义时已经初始化好了
int result = 0;
for(int i = 1; i <= nums1.size(); i++) {
for (int j = 1; j <= nums2.size(); j++) {
if(nums1[i - 1] == nums2[j - 1]){
dp[i][j] = dp[i-1][j-1] + 1;
}
if (dp[i][j] > result) result = dp[i][j];
}
}
return result;
}
};
力扣
这个题使用贪心算法也可以做出来
这里使用动态规划
主要是确定动态规划的公式
确定数组以及下标
dp[i]包括下标i(以nums[i]为结尾)的最大连续子序列和为dp[i]
动态规划公式
dp[i] = max(dp[i-1] + nums[i], nums[i])
if(dp[i] > result) result = dp[i];
当前dp[i]可以通过上一次数组相加之和得到,也可通过当前nums[i](由于数组相加之和,小于nums[i],直接初始化了得到最大值)
编辑距离又是动态规划常见题型,但是都比较难
力扣
这个题是编辑距离题的入门,并且这个题和最长重复子数组、最长公共子序列相似
动态规划公式理论:
- if (s[i - 1] == t[j - 1])
- t中找到了一个字符在s中也出现了
dp[i][j] = dp
- if (s[i - 1] != t[j - 1])
- 相当于t要删除元素,继续匹配
代码
class Solution {
public:
bool isSubsequence(string s, string t) {
vector> dp(s.size() + 1, vector(t.size() + 1, 0));
for (int i = 1; i <= s.size(); i++) {
for (int j = 1; j <= t.size(); 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[s.size()][t.size()] == s.size()) return true;
return false;
}
};
力扣
这个题是做的为数不多的困难题,确实困难啊,。。。
直接来五步骤
确定dp数组以及下标
dp[i][j]表示在字符串s[i-1]中出现t[i-1]的次数
动态规划公式
if (s[i-1] == t[i-1]) {
dp[i][j] = dp[i-1][j-1] + dp[i-1][j]
} else{
dp[i][j] = dp[i-1][j]
}
初始化
dp[i][0] = 1 表示:以i-1为结尾的s可以随便删除元素,出现空字符串的个数为1
dp[0][j] = 0 表示:空字符串s可以随便删除元素,出现以j-1为结尾的字符串t的个数
dp[0][0] = 1,空字符串s,可以删除0个元素,变成空字符串t
确定遍历顺序
for (int i = 1; i <= s.size(); i++) {
for (int j = 1; j <= t.size(); j++) {
if (s[i - 1] == t[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
} else {
dp[i][j] = dp[i - 1][j];
}
}
}
这个题吧,一步一个坑
首先是定义数组,int类型的不够,要定义uint64_t (第一次这样定义)
动态规划公式
要考虑两种情况:s[i-1] !=t[j-1]以及s[i-1]==t[j-1]
当s[i - 1] 与 t[j - 1]不相等时,dp[i][j]只有一部分组成,不用s[i - 1]来匹配(就是模拟在s中删除这个元素),即:dp[i - 1][j]
当s[i - 1] 与 t[j - 1]相等时:
一部分是用s[i - 1]来匹配,那么个数为dp[i - 1][j - 1]。即不需要考虑当前s子串和t子串的最后一位字母,所以只需要 dp[i-1][j-1]。
一部分是不用s[i - 1]来匹配,个数为dp[i - 1][j]。
代码:
class Solution {
public:
int numDistinct(string s, string t) {
vector> dp(s.size() + 1, vector(t.size() + 1));
for (int i = 0; i < s.size(); i++) dp[i][0] = 1;
for (int j = 1; j < t.size(); j++) dp[0][j] = 0;
for (int i = 1; i <= s.size(); i++) {
for (int j = 1; j <= t.size(); j++) {
if (s[i - 1] == t[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
} else {
dp[i][j] = dp[i - 1][j];
}
}
}
return dp[s.size()][t.size()];
}
};
力扣
具体步骤参考代码随想录 (programmercarl.com)
class Solution {
public:
int minDistance(string word1, string word2) {
vector> dp(word1.size() + 1, vector(word2.size() + 1));
for (int i = 0; i <= word1.size(); i++) dp[i][0] = i;
for (int j = 0; j <= word2.size(); j++) dp[0][j] = j;
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];
} else {
dp[i][j] = min(dp[i - 1][j] + 1, dp[i][j - 1] + 1);
}
}
}
return dp[word1.size()][word2.size()];
}
};
力扣
分析过程参考代码随想录 (programmercarl.com)
class Solution {
public:
int minDistance(string word1, string word2) {
vector> dp(word1.size() + 1, vector(word2.size() + 1, 0));
for (int i = 0; i <= word1.size(); i++) dp[i][0] = i;
for (int j = 0; j <= word2.size(); j++) dp[0][j] = j;
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];
}
else {
dp[i][j] = min({dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]}) + 1;
}
}
}
return dp[word1.size()][word2.size()];
}
};
回文就是正读和反读都一样
力扣
这个题可以采用动态规划或者双指针法,动态规划复杂度较高,参考代码随想录 (programmercarl.com)
class Solution {
public:
int countSubstrings(string s) {
vector> dp(s.size(), vector(s.size(), false));
int result = 0;
for (int i = s.size() - 1; i >= 0; i--) { // 注意遍历顺序
for (int j = i; j < s.size(); j++) {
if (s[i] == s[j]) {
if (j - i <= 1) { // 情况一 和 情况二
result++;
dp[i][j] = true;
} else if (dp[i + 1][j - 1]) { // 情况三
result++;
dp[i][j] = true;
}
}
}
}
return result;
}
};
力扣
这个题相比较上一题还简单一些,参考代码随想录 (programmercarl.com)
class Solution {
public:
int longestPalindromeSubseq(string s) {
vector> dp(s.size(), vector(s.size(), 0));
for (int i = 0; i < s.size(); i++) dp[i][i] = 1;
for (int i = s.size() - 1; 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];
}
};
动态规划算告一段落,后面接着刷二叉树,再接着是回溯,再接着是数据结构基础,再接着是双指针,贪心
当然动态规划还是要回顾的,要不然会忘记
加油加油