代码随想录刷题记录:DP系列

代码随想录DP系列

53. 最大子数组和

考虑每个数组的尾部

dp[i] 代表 [0,i]位置上的最大子数组和,

dp[i] = max(dp[i-1]+arr[i] , arr[i] ); // 选择加入之前的数组 or 重新开始一个数组

当前“连续和”为负数的时候立刻放弃,从下一个元素重新计算“连续和”,因为负数加上下一个元素 “连续和”只会越来越小。

dp[0] = arr[0];

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        int n = nums.size();
        int max_ = nums[0];
        vector<int> dp(n);
        dp[0] = nums[0];
        for(int i =1 ;i< n;i++){
            dp[i] = max(dp[i-1]+nums[i],nums[i]);
            max_ = max(max_,dp[i]);
        }
        return max_;

    }
};
70. 爬楼梯

dp[i] 代表第i步的可能数, 可能来自前1步+1,也可能来自前2步+2 或 +1+1 , 但是+1+1的操作等价于前一步+1,所以只考虑前一步+1 与 前2步+2

递推公式:

​ dp[i] = dp[i-1]+ dp[i-2]

特殊值:

​ dp[1] = 1; dp[2] = 2 ; dp[3] = 3

初始状态:

​ dp[0] = 1; dp[1] =1;

遍历顺序:

​ -->

class Solution {
public:
    int climbStairs(int n) {
        vector<int> res(n+1);
        res[0] = 1;
        res[1] = 1;
        if(n < 2) return 1;
        for(int i = 2 ; i <= n ; i++){
            res[i] = res[i-1] + res[i-2];
        }
        return res[n];
    }
};
746. 使用最小花费爬楼梯

dp[i] 代表第i步的最小花费

dp[i] = min(dp[i-1] , dp[i-2]) + cost[i]

特殊值: 若cost = [10, 15, 20] , dp[0] = 10, dp[1] =min(0, 10) +15 = 15 , dp[2] = 30 , end: dp[3] = 15+cost

初始值:dp[0] = cost[0] , dp[1] = cost[1]

顺序:–>

结果:由于cost[n] 不存在,所以无法直接使用递推公式,只能用倒数第一个与第二个的最小值

class Solution {
public:
    int minCostClimbingStairs(vector<int>& cost) {
        int n = cost.size();
        vector<int> res(n+1);
        res[0] = cost[0];
        res[1] = cost[1];
        if(n < 2) return res[n];
        for(int i = 2 ; i < n; i++){
            res[i] = min(res[i-1], res[i-2]) + cost[i];
        }
        return min(res[n-1], res[n-2]);
    }
};
62. 不同路径

dp [i] [j] = dp[i-1] [ j ] + dp[i] [j-1]

dp[0] [*] = 1 ; dp [ *] [0] = 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];
    }
};
63. 不同路径 II

思路和上一题差不多,只是在DP方程中加上了考虑Block时为0的情况,还有初始化过程中,遇到Block不再赋1;

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));
        for(int i = 0 ; i < m; i ++){
            if(obstacleGrid[i][0] == 1) break;
            dp[i][0] = 1 ;
        }
        for(int j = 0; j < n; j++){
            if(obstacleGrid[0][j] == 1) break;
            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];
                if(obstacleGrid[i][j] == 1) dp[i][j] = 0;
            }
        }

        return dp[m-1][n-1];
    }
};
343. 整数拆分

dp[i] 代表整数 i 拆分后的最大乘积。 假设 i 只拆分出一个 j + (i - j) , 那么有一个乘积 j * ( i - j ) 【理解为2个拆分数】,还有一个乘积 j* dp[i - j]【理解为2个以上的拆分数】;

dp[i] = for j 1->i-1 : max(dp[i] , dp[i-j] * j , (i-j)*j )

特殊值:n = 2; dp[1] = 1; dp[2] = max(1 * 1, 1 * 1 ) = 1;

初始值:dp[0] = 0 ; dp[1] = 1;

->

class Solution {
public:
    int integerBreak(int n) {
        vector<int> dp(n+1);
        dp[0] = 0;
        dp[1] = 1;
        if (n < 2) return n;
        for(int i = 2 ; 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];
    }
};
96. 不同的二叉搜索树

dp[i] : i 个节点的二叉搜索树可能的数目; 二叉搜索树的数目,可以由左子树可能的数目*右子树可能的数目计算,只要左子树节点+右子树节点+1 = n即可。

dp[n] = for i from 0 to n-1 : dp[i] * dp[ n-1-i ]

特殊值: dp[0] = 1; dp[1] = 1; dp[2] = 2;

遍历顺序: ->

class Solution {
public:
    int numTrees(int n) {
        vector<int> res(n+1,0) ;
        res[0] = 1 ;
        res[1] = 1 ;
        for(int i = 2;i<=n ;i++){
            for(int j = 0 ; j <i ; j++){
                res[i]+= res[j]*res[i-j-1]; 
            }
        }
        return res[n];

    }
};
01 背包

dp[i] [j] 代表 任取[ 0, i ]物品装入最大容量为 j 的容器中的最大价值。

  • dp[i] [j] 如果物品 i 不放,那么就是容量不变的情况下,任选[0, i-1]物品装入的价值,也就是dp[i-1] [j];

  • 如果物品 i 放,那么相当于 j - weight[i] 质量下最大的价值+当前物品价值 , 也就是 dp[i-1] [j - weight [j] ] + value(i)

dp[i] [j] = Max(dp[i-1] [j] , dp[i-1] [j - weight[j]] + value(i))

特殊值: dp[ * ] [0] = 0; dp[0] [1~m] = value(0);

方向: 左上->右下

#include <iostream>
#include <vector>
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 i = weight[0] ; i <= bagweight + 1; i ++) {
		dp[0][i] = value[0];
	}

	for (int i = 1 ; i < weight.size(); i++) {
		for (int j = 0 ; j < bagweight + 1; j ++) {
			if (weight[i] <= j) {
				dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
			} else {
				dp[i][j] = dp[i - 1][j];
			}
		}
	}
	
	for (int i = 0 ; i < weight.size(); i++) {
		for (int j = 0 ; j < bagweight + 1; j ++) {
			cout << dp[i][j] << " ";
		}
		cout << endl;
	}

}

int main() {
	test_2_wei_bag_problem1();
}

01 滚动数组写法

dp[i] 代表容量为 i 时最大的value

dp[i] = max (dp[i] , dp[i - weight[j] ] + value(j)) 【注意实现时要加上 i - weight[j] > 0 的条件】

遍历顺序:

从前往后遍历物品 i ;由于每一个物品只能加入一次,所以必须从后向前遍历 j (背包重量)

初始化: dp[*] = 0

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k5rFaidq-1662465630496)(C:\Users\26421\Documents\markdown笔记本\代码随想录:DP系列.assets\image-20220904091602962.png)]

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]);

    }
}
416. 分割等和子集

回溯法: 两个子集和都为 sum/2

与01背包问题很相似,首先是暴力解法时间直接遍历幂集,复杂度为O(2^n),与01背包一致。其次,01背包每一个元素最多放1次,最终最大价值的背包正好构成一个集合,剩余的没有放入的也构成另一个集合,与本题的两个集合对应。

  • 01背包的最大体积: sum(num)/2
  • 01背包中物品的价值:num
  • 01背包中物品的体积:num

**如果dp[j] == j 说明,集合中的子集总和正好可以凑成总和j,理解这一点很重要。**因为最大容量为 j 的情况下,物品总体积是 <= j 的,由于价值与体积是一个量,所以价值也<= j 。所以,如果没有存在总和为 j 的组合,那么在最大容量为 j 的情况下,最大价值dp[j] < j; 如果存在总和为 j 的组合,那么一定有 dp[j] = j;

需要满足的条件: 装满容量为 sum/2 的01背包的最大价值正好为 sum/2 (若和为奇数,则false)

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int sum = 0;
        int n = nums.size();
        for(int i = 0; i < n; i++){
            sum += nums[i];
        }
        if(sum % 2 == 1) return false;
        sum = sum/2;

        vector<int> dp(sum+1);
        for(int i = 0; i < n; i++){
            for(int j = sum; j >=nums[i]; j--){
                dp[j] = max(dp[j], dp[j-nums[i]] + nums[i]);
            }
        }
        if(dp[sum] == sum) return true;
        return false;
    }
};
1049. 最后一块石头的重量 II

本题其实就是尽量让石头分成重量相同的两堆,相撞之后剩下的石头最小,这样就化解成01背包问题了

与上一题类似,若dp[target] = target, 则可以均分为两质量相同的堆, 剩余 sum - 2*dp[target] = 0 ,全部销毁。若dp[target] < target , 则剩余 sum - 2 * dp[target];

class Solution {
public:
    int lastStoneWeightII(vector<int>& nums) {
        int sum = 0;
        int n = nums.size();
        for(int i = 0; i < n; i++){
            sum += nums[i];
        }
        int target = sum/2;

        vector<int> dp(target+1);
        for(int i = 0; i < n; i++){
            for(int j = target; j >=nums[i]; j--){
                dp[j] = max(dp[j], dp[j-nums[i]] + nums[i]);
            }
        }
        return (sum - 2 * dp[target]);
    }
};
494. 目标和

将集合分为两部分:加法集合与减法集合,假设加法的总和为x,那么减法对应的绝对值总和就是sum - x。

x- (sum -x) = target , ∴ x = (target + sum) / 2

问题转换为,和为x的集合划分数目

这次和之前遇到的背包问题不一样了,之前都是求容量为j的背包,最多能装多少。本题则是装满有几种方法。其实这就是一个组合问题了。

所以求组合类问题的公式,都是类似这种:

dp[j] += dp[j - nums[i]]

dp[n] 代表和为n的集合划分数目;

​ 考虑nums[i]的话(只要搞到nums[i]),凑成dp[j]就有dp[j - nums[i]] 种方法。

dp[n] = Σ dp[n - nums[i]] (if nums[i] exist)

dp[0] = 1;

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        //首先转化为01背包问题:
        // 视为正集合P与负集合N: Sum(P) - Sum(N) = target
        // 转化一下: Sum(P)*2 = target + Sum(nums)
        // 即要求目标和为(target+Sum(nums))/2 的所有子集合个数,变为01背包问题
        int sum = 0 ;
        int n = nums.size();
        for(int i = 0; i < n ; i++){
            sum += nums[i];
        }
        if(sum < abs(target) || (sum + target)%2 != 0){
            return 0;
        }

        int BagSize = (sum + target) / 2;
        vector<int> dp(BagSize+1) ;
        dp[0] = 1;
        // 背包大小为 BagSize
        // 物品重量nums[]
        
        for(int i = 0 ; i < n ; i ++) {
            for(int j = BagSize ; j >= nums[i] ; j --){
                dp[j] += dp[j - nums[i]];
            }
        }
        return dp[BagSize];
    }
};
474. 一和零

每一个字符串视为一个物品,字符串的 0 1 数 视为物品体积,子集最大的0 1 数视为最大体积, 每一个物品的价值为1,最大价值对应子集最多的元素个数。 与01背包问题不同,本题有 m,n 两个”体积“变量,可以用两个01背包来解,然后取小者。

dp[n] 代表最大1个数为n 的子集所含字符串个数,

dp[j] =max ( dp[j] , dp[j - weight[i] ] + value[i] ) from bagweight to weight[i]

dp [j] = value[0] : 0 ?j > weight[0]

如此定义了单个01背包问题,我们再使用一个dp2来对最大0个数为m的子集处理,最后取min(dp[bagweight] , dp2[bagweiht]) 即可

// 错误代码
class Solution {
public:
    int findMaxForm(vector<string>& strs, int m, int n) {
        int num = strs.size();
        vector<int> res_zero(m + 1);
        vector<int> res_one(n + 1);
        for(int i = 0; i < num; i++){
            int One = 0 ;
            int Zero = 0;
            for(char c: strs[i]){
                if(c == '0') Zero++;
                else One++;
            }
            //0
            for(int j = m; j >= Zero; j--){
                res_zero[j] = max(res_zero[j], res_zero[j - Zero] + 1);
            }
            for(int j = n; j >= One; j--){
                res_one[j] = max(res_one[j], res_one[j - One] + 1);
            }

            // for(int j = 0; j <= m; j ++){
            //     cout<< res_zero[j] <<" ";
            // }
            // cout<<"\t\t";
            // for(int j = 0; j <= n; j ++){
            //     cout<< res_one[j] <<" ";
            // }
            // cout <<endl;
        }
        return min(res_one[n], res_zero[m]);
    }
};

但是这种方法是错误的! 错误的测试用例如下:

[“10”,“0001”,“111001”,“1”,“0”] ; max0 = 4; max1 = 3;

输出:4 预期结果: 3

对于max0 = 4 有 {“10”,“111001”,“1”,“0”} 4个元素。 对于max1 = 3 有{“10”,“0001”,“1”,“0”} 4 个元素。但是二者并不相同!所以分开求解的方式是有问题的!原因在于每一个字符串的0 1 个数会一同作用在DP数组上,所以应该以字符串为单位进行考虑,而不是字符串的0的个数或者1的个数。

更改dp[i] [j] 为 最大0个数为i,最大1个数为j 对应的最大元素个数

dp[i] [j] = max (dp[i] [j] , dp[i - Zero] [j - One] +1 )

class Solution {
public:
    int findMaxForm(vector<string>& strs, int m, int n) {
        int num = strs.size();
        vector<vector<int>> res(m + 1, vector<int>(n+1, 0));
        for(int i = 0; i < num; i++){
            int One = 0 ;
            int Zero = 0;
            for(char c: strs[i]){
                if(c == '0') Zero++;
                else One++;
            }
            for(int j = m; j >= Zero; j--){
                for(int k = n; k >= One; k--){
                    res[j][k] = max(res[j][k], res[j - Zero][k - One] + 1 ); 
                }
            }
        }
        return res[m][n];
    }
};
完全背包问题

完全背包和01背包问题唯一不同的地方就是,每种物品有无限件

首先在回顾一下01背包的核心代码

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]);
    }
}

我们知道01背包内嵌的循环是从大到小遍历,为了保证每个物品仅被添加一次。

而完全背包的物品是可以添加多次的,所以要从小到大去遍历,即:

// 先遍历物品,再遍历背包
for(int i = 0; i < weight.size(); i++) { // 遍历物品
    for(int j = weight[i]; j <= bagWeight ; j++) { // 遍历背包容量
        dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

    }
}
518. 零钱兑换 II

完全背包(从前向后遍历) + 组合问题(+=)

(需要注意的是, 组合问题与排序问题不一样!)

求组合类问题的公式,都是类似这种:

dp[j] += dp[j - nums[i]]

dp[j] 代表在coin[0~i] 参与下,组成金额为j 的硬币组合数。

假设当前加入无数枚新的面值的硬币,则需要以完全背包的遍历顺序来增加面值的组合数:dp[j] = dp[j] + dp[j - coins[i]];

遍历顺序:完全背包的顺序

初始化:dp[0] = 1; dp[1] = dp[0] + dp[1 - 1] = 1; (若dp[0] = 0, 由于dp方程是+= 操作,那么所有dp[i]都会为0)

class Solution {
public:
    int change(int amount, vector<int>& coins) {
        vector<int> dp(amount+1, 0);
        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. 组合总和 Ⅳ

完全背包 (从前向后遍历)+ 排列问题(考虑顺序)(+=,内层循环遍历物品)

如果求组合数就是外层for循环遍历物品,内层for遍历背包

如果求排列数就是外层for遍历背包,内层for循环遍历物品

dp[n] 代表target为n时的排列个数

内层循环:for each i: dp[n] += dp[n - weight[i]] ;每一个位置都有weight.size()种可能(物品个数),如果符合要求(未越界),则累加。

初始化: dp[0] = 1; (由于全是+=操作,为0的话全是0)

class Solution {
public:
    int combinationSum4(vector<int>& nums, int target) {
        vector<int> res(target + 1);
        res[0] = 1;

        for(int i = 0 ; i <= target; i ++){
            for(int j = 0; j < nums.size(); j ++){
                int num_j = nums[j];
                if(i >= num_j && res[i] < INT_MAX - res[i - num_j]){
                    res[i] += res[i - num_j];
                }
            }
        }
        return res[target];
    }
    
};

C++测试用例有两个数相加超过int的数据,所以需要在if里加上dp[i] < INT_MAX - dp[i - num]。

70. 爬楼梯

给爷爬!!

dp[i] = dp[i-1] + dp[i-2]

class Solution {
public:
    int climbStairs(int n) {
        vector<int> res(n+1);
        res[0] = 1;
        res[1] = 1;
        if(n < 2) return 1;
        for(int i = 2 ; i <= n ; i++){
            res[i] = res[i-1] + res[i-2];
        }
        return res[n];
    }
};

楼梯的高度相当于最大体积,爬1和爬2段,相当于物品的体积为1或2,可以无限使用,而可能数则是排列数。所以本题相当于一个完全背包的排列问题

class Solution {
public:
    int climbStairs(int n) {
        vector<int> res(n+1);
        res[0] = 1;
        
        int nums[2] = {1,2};
        int len = 2;

        for(int i = 0; i <= n; i++){
            for(int j =0; j < len; j++){
                if(i >= nums[j])
                res[i] += res[i - nums[j]];
            }
        }
        return res[n];
    }
};
322. 零钱兑换

计算并返回可以凑成总金额所需的 最少的硬币个数 ,相当于价值要尽可能小。由于每一个硬币都是无限的,所以是一个完全背包问题。

​ BagSize:总金额

​ Weight:硬币的数额

​ Value: 1(返回的是硬币的个数)

dp[i] 代表总金额为 i 的最少硬币个数

dp[i] =min(dp[i], dp[i - weight[j]] + value[j])

初始化:dp[*] = INT_MAX ; dp[0] = 0;

遍历顺序: 完全背包遍历顺序

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        vector<int> res(amount + 1, INT_MAX );
        res[0] = 0;

        for(int i = 0;  i < coins.size(); i++){
            for(int j = coins[i]; j <= amount ; j++){
                if(res[j - coins[i]] == INT_MAX) continue;
                res[j] = min(res[j], res[j - coins[i]] + 1 );
            }
        }
        return res[amount]==INT_MAX? -1:res[amount];

    }
};
279. 完全平方数

与上一题一致,为完全背包最小价值问题

class Solution {
public:
    int numSquares(int n) {
        int m = sqrt(n);
        vector<int> res(n+1, INT_MAX);
        res[0] = 0;
        for(int i = 1; i <= m; i++){
            for(int j = i*i ; j <= n; j++){
                if(res[j - i*i] == INT_MAX) continue;
                res[j] = min(res[j] , res[j-i*i]+1);
            }
        }
        return res[n];
    }
};
139. 单词拆分

完全01背包 + 排列问题; 看s是否在排列中?

dp[i] =true 代表s[0:i] 可以由字典中的单词组成,

dp[i] = true if dp[i - len(word)] && s[i-len(word) : i] equal “word”

dp[0] = true;

遍历顺序:内层循环物品(词典中的word) ,而且j递增

#include<string.h>
class Solution {
public:
    bool wordBreak(string s, vector<string>& wordDict) {

        // convert wordDict to map
        unordered_set<string> wordmap(wordDict.begin(), wordDict.end());
        vector<bool> dp(s.size()+1);
        dp[0] = true;

        for(int i = 1; i <= s.size(); i++){
            for(int j = 0; j < wordDict.size(); j ++){
                // 比较 s[i - x : i] 与 dict中word 是否相同
                // 方案1: 遍历dict中的word, 去比较s[i - len(word) :i] 与 word ; 
                // 方案2: 遍历s[i - j : i]子串,去字典中查找是否存在
                string word = wordDict[j];
                int len = word.size();
                if(i >= len && dp[i - len] ){
                    if(s.substr(i-len, len)== word){
                        dp[i] = true;
                    }
                    
                }
            }
        }
        return dp[s.size()];
        

    }
};
#include<string.h>
class Solution {
public:
    bool wordBreak(string s, vector<string>& wordDict) {

        // convert wordDict to map
        unordered_set<string> wordmap(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 ++){
                // 比较 s[i - x : i] 与 dict中word 是否相同
                // 方案1: 遍历dict中的word, 去比较s[i - len(word) :i] 与 word ; 
                // 方案2: 遍历s[i - j : i]子串,去字典中查找是否存在
                string substring = s.substr(i - j , j);
                if(wordmap.find(substring) != wordmap.end() && dp[i-j]){
                        dp[i] = true;
                    }
                   
                }
            }
        return dp[s.size()];
    }
};
多重背包

有N种物品和一个容量为V 的背包。第i种物品最多有Mi件可用,每件耗费的空间是Ci ,价值是Wi 。求解将哪些物品装入背包可使这些物品的耗费的空间 总和不超过背包容量,且价值总和最大。

每件物品最多有Mi件可用,把Mi件摊开,其实就是一个01背包问题了。

    for (int i = 0; i < nums.size(); i++) {
        while (nums[i] > 1) { // nums[i]保留到1,把其他物品都展开
            weight.push_back(weight[i]);
            value.push_back(value[i]);
            nums[i]--;
        }
    }
198. 打家劫舍

dp[n] 代表第n家最大的收益,可以选择偷(dp(n-2) + value(n)) 也可以选择不偷(dp(n-1))

dp[n] = max(dp[n-1] , dp[n-2]+value[n])

dp[0] = value[0];

class Solution {
public:
    int rob(vector<int>& nums) {
        vector<int> dp(nums.size());
        if(nums.size()==1){
            return nums[0];
        }
        dp[0] = nums[0];
        dp[1] = max(nums[0],nums[1]);
        int  res = max(dp[0],dp[1]);
        for(int i =2;i<nums.size();i++){
            dp[i] = max(dp[i-1],dp[i-2]+nums[i]);
            res= max(res,dp[i]);
        }
        return res;
    }
};
213. 打家劫舍 II

环形问题: 考虑[1:n] , [0:n-1], [1:n-1]三种情况满足非环形,取最大值。而[1:n-1]是[1:n]的子问题,dp[n] = max (dp[n-1] , dp[n-2] + value[n]) 已经做了比较,所以只需要考虑[1:n] [0:n-1]两种情况。

class Solution {
public:
    int rob(vector<int>& nums) {
        int n = nums.size();
        if(n == 0) return 0;
        if(n == 1) return nums[0];
        if(n == 2) return max(nums[1],nums[0]);

        vector<int> res(n);
        vector<int> res2(n);
        res[0] = nums[0];    res2[1] = nums[1];
        res[1] = max(nums[0], nums[1]);  res2[2] = max(nums[1], nums[2]);


        for(int i = 2; i < n -1 ; i++){
            res[i] = max (res[i-1], res[i-2] + nums[i]);
        }
        for(int i = 3; i < n ; i++){
            res2[i] = max (res2[i-1], res2[i-2] + nums[i]);
        }

        return max(res[n-2] , res2[n-1]);

    }
};
337. 打家劫舍 III

需要遍历二叉树,而且需要dp。

先序、后续、中序 的序列都不满足相邻元素的距离一定为1,所以无法用序列来做。

按照节点N、左孩子L、右孩子R的划分,

可以来自自身节点与不与自身相邻的子树的节点:DP[N] = value(N) + DP[L’L] + DP[L’R] + DP[R’R] + DP[R’L]

也可以来自左孩子右孩子的和:DP[N] = DP[L] + DP[R]

class Solution {
public:
    int rob(TreeNode* root) {
        if(root == nullptr) return 0;
        int val1 = root->val;
        if(root -> left) val1 +=  rob(root -> left ->right) + rob(root -> left ->left) ;
        if(root -> right) val1 += rob(root ->right ->left) + rob(root -> right ->right);
        int val2 = rob(root -> left) + rob(root -> right);
        return max(val1 , val2);
    }
};

这样会超时,原因是rob(left -> left ->right) 此类深度为2的操作 与 rob(root -> left) 此类深度为1的操作 有很多重复。所以需要用一个map来记录计算中间结果避免重复运算。

class Solution {
public:
    unordered_map<TreeNode *, int> umap; //
    int rob(TreeNode* root) {
        if(umap[root]) return umap[root]; //
        if(root == nullptr) return 0;
        int val1 = root->val;
        if(root -> left) val1 +=  rob(root -> left ->right) + rob(root -> left ->left) ;
        if(root -> right) val1 += rob(root ->right ->left) + rob(root -> right ->right);
        int val2 = rob(root -> left) + rob(root -> right);

        umap[root] = max(val1 ,val2);//
        return max(val1 , val2);
    }
};
121. 买卖股票的最佳时机

法1: 暴力遍历(贪心) 取每一天与前面天(最小值)的差值。 On^2

法2:DP

考虑买入动作与卖出动作。

DP1[i] 代表第i天 持有状态 欠的最少钱 ;选择在今天前买入欠的最少钱Or今天买入欠的钱; DP1[i] = max(DP1[i-1] ,0 -value[i] )

DP2[i] 代表第i天 卖出状态 挣的最多钱;前一天已经卖出则保持,前一天未卖出则今天卖出; DP2[i] = max(DP2[i-1] , DP1[i-1] + value[i])

DP1[0] = -value[0] ; DP2[0] = 0;

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int n = prices.size();
        vector<vector<int>> dp(n, vector<int>(2,0));

        dp[0][0] = -prices[0];
        dp[0][1] = 0;

        for(int i = 1; i < n; 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[(n-1)][1];
    }
};
122. 买卖股票的最佳时机 II

可以多次买入卖出,但是每一次买入卖出是一个原子操作。第二次买入的时候,欠的钱 = 前一次卖出时挣的钱+这一次买入的价格。

dp[i] [0] 代表 第i天持有状态时 欠的钱

dp[i] [1] 代表 第i天卖出状态 有的钱

dp[i] [0] = max(dp[i-1] [0] ,dp[i-1] [1] -value(i))

dp[i] [1] = max(dp[i-1] [1], dp[i -1] [0] + value(i))

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int len = prices.size();
        vector<vector<int>> dp(len, vector<int>(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]);
            cout << dp[i][0] <<" "<<dp[i][1]<<endl;
        }
        
        return dp[len - 1][1];

    }
};
123. 买卖股票的最佳时机 III

只能买入卖出两次! 可以拆解为 0次,1次,2次

in 0 out 0 --> in 1 out 0 in 1 out 1 --> in 2 out 1 in 2 out 2

对于买入1次的,是如何控制的呢?看 第i天的买入状态欠的最少钱DP1[i]的更新 ,要么是第i天不买入(保持i-1天的买入状态),要么是第i天买入,此时由于本金是 0 , 只能买入1次,所以是 0 - value[i]

DP1[i] = max(DP1[i-1] ,0 -value[i] )

DP2[i] = max(DP2[i-1] , DP1[i-1] + value[i])

对于买入无数次的,唯一的不同在于买入时,可能已经在之前的交易中赚到了钱,本金不再是 0 ,而是 dp2[i-1]

那么,买入两次,该怎么进行约束呢?通过控制本金。

可以将买入两次,视为2次买入1次。由此得到 第一次买卖{DP[i] [0] DP[i] [1] },第二次买卖 {DP[i] [2] DP[i] [3]}

dp[i] [0] = max(dp[i-1] [0], 0 - value[i]);

dp[i] [1] = max(dp[i-1] [1], dp[i-1] [0] + value[i]);

dp[i] [2] = max(dp[i-1] [2], dp[i-1] [1] - value[i]); // 第二次买入时的本金,只可能来自第一次卖出后

dp[i] [3] = max(dp[i-1] [3], dp[i-1] [2] + value[i]);

初始化:dp[0] [0] = -value[0]; dp[0] [1] = 0; dp[0] [2] = -value[0] ; dp[0] [3] = 0;

遍历顺序:观察dp方程,从上到下,从左到右

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int n = prices.size();
        vector<vector<int>> dp(n,vector<int>(4,0));

        dp[0][0] = -prices[0];
        dp[0][1] = 0;
        dp[0][2] = -prices[0];
        dp[0][3] = 0;

        for(int i = 1; i < n ; i++){
            dp[i][0] = max(dp[i-1][0] , 0 - prices[i]);
            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]) ;
        }
        return dp[n-1][3];
    }
};
188. 买卖股票的最佳时机 IV

由2次 拓展到 k次,自然想到将dp[i] [0~3] 拓展到 dp[i] [0 ~ 2k-1],

买入操作对应的dp: dp[i] [j] = max(dp[i-1] [j] , dp[i-1] [j-1] - prices[i]); //此处dp[] [j-1] 对应上一次卖出

卖出操作对应的dp: dp[i] [j] = max(dp[i-1] [j], dp[i-1] [j-1] + prices[i]) // dp[] [j-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));

    
        for(int j = 0 ; j < 2*k+1 ; j++){
            if((j % 2) ==1) dp[0][j] = -prices[0];
        }
        
        for(int i = 1; i < n ; i ++){
            for(int  j = 1 ; j < 2*k+1 ; j ++){
                if(j % 2 == 1 ){
                        dp[i][j] = max(dp[i-1][j] , dp[i-1][j-1] - prices[i]);
                    }
                else{
                        dp[i][j] = max(dp[i-1][j] , dp[i-1][j-1] + prices[i]);
                    }
            }
        }

        // for(int i = 0 ; i < n ; i++){
        //     for(int j = 0 ; j < 2*k+1 ; j++){
        //         cout<< dp[i][j] <<"\t";
        //     }
        //     cout<<endl;
        // }
        return dp[n-1][2*k];

    }
};
309. 最佳买卖股票时机含冷冻期
  • 无限次交易,每次交易是原子操作
  • 卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。

dp[i] [0]代表第i天买入状态,dp[i] [0] = max (dp[i-1] [0] , dp[i-2] [1] - value[i]) 保持昨天状态 or 买入

dp[i] [1]代表第i天卖出状态,dp[i] [1] = max (dp[i-1] [1], dp[i-2] [0] + value[i] ) 保持昨天状态 or 卖出

上面的DP是错误的,是因为没有区分卖出后的状态 (冷冻期 非冷冻期)

DP[i] [0] 为买入状态的收益(可以保持上一次买入,也可以从非冻结状态买入)

DP[i] [1] 为冻结状态的收益(只有当前卖出时才冻结)

DP[i] [2] 为非冻结的收益 (可以刚刚解封,也可以非冻结一直保持)

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        if (prices.empty()) {
            return 0;
        }

        int n = prices.size();
        // f[i][0]: 手上持有股票的最大收益
        // f[i][1]: 手上不持有股票,并且处于冷冻期中的累计最大收益
        // f[i][2]: 手上不持有股票,并且不在冷冻期中的累计最大收益
        vector<vector<int>> f(n, vector<int>(3));
        f[0][0] = -prices[0];
        for (int i = 1; i < n; ++i) {
            f[i][0] = max(f[i - 1][0], f[i - 1][2] - prices[i]);
            f[i][1] = f[i - 1][0] + prices[i];
            f[i][2] = max(f[i - 1][1], f[i - 1][2]);
        }
        return max(f[n - 1][1], f[n - 1][2]);
    }
};
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值