代码随想录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]);
}
};