力扣动态规划专题(四)劫舍问题与股票问题 打家劫舍Ⅰ Ⅱ Ⅲ 买卖股票最佳时机Ⅰ Ⅱ Ⅲ IV 步骤及C++实现

文章目录

  • 198. 打家劫舍
  • 213. 打家劫舍 II
  • 337. 打家劫舍 III
  • 121. 买卖股票的最佳时机
    • 动态规划
    • 贪心算法
  • 122. 买卖股票的最佳时机 II
    • 动态规划
    • 贪心算法
  • 123.买卖股票的最佳时机III
  • 188.买卖股票的最佳时机IV
  • 309.最佳买卖股票时机含冷冻期
  • 714.买卖股票的最佳时机含手续费

198. 打家劫舍

力扣动态规划专题(四)劫舍问题与股票问题 打家劫舍Ⅰ Ⅱ Ⅲ 买卖股票最佳时机Ⅰ Ⅱ Ⅲ IV 步骤及C++实现_第1张图片
步骤

  1. 确定dp数组以及下标的含义
    dp[i]:考虑下标i(包括i)以内的房屋,最多可以偷窃的金额为dp[i]

  2. 确定递推公式

  • 决定dp[i]的因素就是第i房间偷还是不偷,当前房屋偷与不偷取决于 前一个房屋和前两个房屋是否被偷了
  • 偷第i房间,那么dp[i] = dp[i - 2] + nums[i] ,第i-1房不能偷,找出下标i-2(包括i-2)以内的房屋,最多可以偷窃的金额为 dp[i-2] +第i房间偷到的钱。
  • 不偷第i房间,那么dp[i] = dp[i - 1],考虑i-1房,不一定要偷i-1房,最多可以偷窃的金额不变,仍为dp[i-1]
  • dp[i]取最大值,dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
  1. dp数组如何初始化
    递推公式的基础依赖于dp[0] 和 dp[1]
    i=0时,dp[0]只能是nums[0],i=1时,dp[1]取nums[0]和nums[1]的最大值
    dp[0]= =nums[0],dp[1]=max(nums[0], nums[1]);

  2. 确定遍历顺序
    dp[i] 是根据dp[i - 2] 和 dp[i - 1] 推导出来的,那么从前到后遍历

  3. 举例推导dp数组
    输入: [2,7,9,3,1]
    力扣动态规划专题(四)劫舍问题与股票问题 打家劫舍Ⅰ Ⅱ Ⅲ 买卖股票最佳时机Ⅰ Ⅱ Ⅲ IV 步骤及C++实现_第2张图片

  4. C++实现

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-1], dp[i-2]+nums[i]);
        }
        return dp[nums.size()-1];
    }
};

213. 打家劫舍 II

力扣动态规划专题(四)劫舍问题与股票问题 打家劫舍Ⅰ Ⅱ Ⅲ 买卖股票最佳时机Ⅰ Ⅱ Ⅲ IV 步骤及C++实现_第3张图片
和198.打家劫舍类似,但是房屋是闭环的,分三种情况

力扣动态规划专题(四)劫舍问题与股票问题 打家劫舍Ⅰ Ⅱ Ⅲ 买卖股票最佳时机Ⅰ Ⅱ Ⅲ IV 步骤及C++实现_第4张图片

情况三考虑包含尾元素,但不一定要选尾部元素,并且对于情况三,取nums[1] 和 nums[3]就是最大的。
同时情况二和情况三都包含了情况一,所以只考虑情况二和情况三就可以了。

步骤

  1. 确定dp数组以及下标的含义
    dp[i]:考虑下标i(包括i)以内的房屋,最多可以偷窃的金额为dp[i]

  2. 确定递推公式

  • 决定dp[i]的因素就是第i房间偷还是不偷,当前房屋偷与不偷取决于 前一个房屋和前两个房屋是否被偷了
  • 偷第i房间,那么dp[i] = dp[i - 2] + nums[i] ,第i-1房不能偷,找出下标i-2(包括i-2)以内的房屋,最多可以偷窃的金额为 dp[i-2] +第i房间偷到的钱。
  • 不偷第i房间,那么dp[i] = dp[i - 1],考虑i-1房,不一定要偷i-1房,最多可以偷窃的金额不变,仍为dp[i-1]
  • dp[i]取最大值,dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
  1. dp数组如何初始化
    递推公式的基础依赖于dp[0] 和 dp[1]
    i=0时,dp[0]只能是nums[0],i=1时,dp[1]取nums[0]和nums[1]的最大值
    dp[0]= =nums[0],dp[1]=max(nums[0], nums[1]);

  2. 确定遍历顺序
    dp[i] 是根据dp[i - 2] 和 dp[i - 1] 推导出来的,那么从前到后遍历

  3. C++实现

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(start == end) return nums[start];
        vector<int> 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-1], dp[i-2]+nums[i]);
        }
        return dp[end];
    }
};

337. 打家劫舍 III

力扣动态规划专题(四)劫舍问题与股票问题 打家劫舍Ⅰ Ⅱ Ⅲ 买卖股票最佳时机Ⅰ Ⅱ Ⅲ IV 步骤及C++实现_第5张图片
和198.打家劫舍类似,但是房屋是二叉树形结构,对于树的遍历方式,前中后序(深度优先搜索),层序遍历(广度优先搜索)。本题一定是后序遍历,因为通过递归函数的返回值来做下一步计算。

与198.打家劫舍,213.打家劫舍II一样,关键在于当前节点抢还是不抢。如果抢了当前节点,两个孩子就不能动,如果没抢当前节点,就可以考虑抢左右孩子

动态规划使用状态转移容器来记录状态的变化,可以使用一个长度为2的数组,记录当前节点偷与不偷所得到的的最大金钱。

步骤

  1. 确定递归函数的参数和返回值
  • 要求一个节点 偷与不偷的两个状态所得到的金钱,那么返回值就是一个长度为2的数组也就是dp数组。
  • dp数组以及下标的含义:dp数组是一个长度为2的数组,下标为0记录不偷该节点所得到的的最大金钱,下标为1记录偷该节点所得到的的最大金钱。
  1. 确定终止条件
    遍历的过程中,如果遇到空节点的话,无论偷还是不偷都是0,返回,相当于dp数组的初始化

  2. 确定遍历顺序

  • 使用后序遍历,要通过递归函数的返回值来做下一步计算。
  • 通过递归左节点,得到左节点偷与不偷的金钱。
  • 通过递归右节点,得到右节点偷与不偷的金钱。
  1. 确定单层递归的逻辑
  • 偷当前节点,左右孩子不能偷,val1 = cur->val + left[0] + right[0];
  • 不偷当前节点,左右孩子可以偷,选最大的,val2 = max(left[0], left[1]) + max(right[0], right[1]);
  • 当前节点的状态为{val2, val1}; 即 {不偷当前节点得到的最大金钱,偷当前节点得到的最大金钱}
  1. 举例推导dp数组
    示例1
    力扣动态规划专题(四)劫舍问题与股票问题 打家劫舍Ⅰ Ⅱ Ⅲ 买卖股票最佳时机Ⅰ Ⅱ Ⅲ IV 步骤及C++实现_第6张图片

  2. C++实现

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    int rob(TreeNode* root) {
        vector<int> result = robTree(root);
        return max(result[0], result[1]);
    }
    vector<int> robTree(TreeNode* cur)
    {
        if(cur == nullptr) return vector<int>{0, 0};
        vector<int> left = robTree(cur->left);
        vector<int> right = robTree(cur->right);
        //偷cur 不偷左右节点
        int var1 = cur->val + left[0] + right[0];
        //不偷cur 可以偷或者不偷左右节点 取较大值
        int var2 = max(left[0], left[1]) + max(right[0], right[1]);
        return {var2, var1};
    }
};

121. 买卖股票的最佳时机

力扣动态规划专题(四)劫舍问题与股票问题 打家劫舍Ⅰ Ⅱ Ⅲ 买卖股票最佳时机Ⅰ Ⅱ Ⅲ IV 步骤及C++实现_第7张图片

动态规划

步骤

  1. 确定dp数组以及下标的含义
  • dp[i][0],表示第i天持有股票所得最多现金
  • dp[i][1],表示第i天不持有股票所得最多现金
  • “持有”不代表就是当天“买入”,也有可能是昨天就买入了,今天保持持有的状态
  1. 确定递推公式
  • dp[i][0]可以由两个状态推出来

    • 第i-1天就持有股票,所得现金=昨天持有股票的所得现金,dp[i - 1][0]
    • 第i天买入股票,所得现金=买入今天的股票后所得现金即,-prices[i]
    • dp[i][0]取所得现金最大的,dp[i][0] = max(dp[i - 1][0], -prices[i]);
  • dp[i][1]可以由两个状态推出来

    • 第i-1天就不持有股票,所得现金=昨天不持有股票的所得现金,dp[i - 1][1]
    • 第i天卖出股票,所得现金=按照今天股票价格卖出后所得现金,prices[i] + dp[i - 1][0]
    • dp[i][1]取所得现金最大的,dp[i][1] = max(dp[i - 1][1], prices[i] + dp[i - 1][0]);
  1. dp数组如何初始化
  • dp[0][0]表示第0天持有股票,买入了股票,dp[0][0] = -prices[0];
  • dp[0][1]表示第0天不持有股票,没有买股票,现金为0,dp[0][1] = 0;
  1. 确定遍历顺序
    dp[i]由dp[i - 1]推导出而来,从前向后遍历

  2. 举例推导dp数组
    输入: 输入:[7,1,5,3,6,4]
    力扣动态规划专题(四)劫舍问题与股票问题 打家劫舍Ⅰ Ⅱ Ⅲ 买卖股票最佳时机Ⅰ Ⅱ Ⅲ IV 步骤及C++实现_第8张图片
    最终结果是dp[5][1],而不是dp[5][0],因为不持有股票状态所得金钱一定比持有股票状态得到的多

  3. C++实现

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        //二维dp数组
        int len = prices.size();
        if(len == 0) return 0;
        vector<vector<int>> dp(len, vector<int>(2));
        dp[0][0] = -prices[0];
        dp[0][1] = 0;
        for(int i=1; i<len; i++)
        {
            dp[i][0] = max(dp[i-1][0], -prices[i]);
            dp[i][1] = max(dp[i-1][1], prices[i]+dp[i-1][0]);
        }
        return dp[len - 1][1];

        /*
        //二维dp滚轮数组
        int len = prices.size();
        if(len == 0) return 0;
        vector> dp(2, vector(2));
        dp[0][0] -= prices[0];
        dp[0][1] = 0;
        for(int i=1; i
    }
};

贪心算法

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        //贪心算法
        int low = INT_MAX;
        int result = 0;
        for(int i=0; i<prices.size(); i++)
        {
            low = min(low, prices[i]);// 取最左最小价格
            result = max(result, prices[i] - low);// 直接取最大区间利润
        }
        return result;
    }
};

122. 买卖股票的最佳时机 II

力扣动态规划专题(四)劫舍问题与股票问题 打家劫舍Ⅰ Ⅱ Ⅲ 买卖股票最佳时机Ⅰ Ⅱ Ⅲ IV 步骤及C++实现_第9张图片

动态规划

本题和121. 买卖股票的最佳时机的唯一区别是股票可以买卖多次,注意最多只能持有 一只股票,再次购买前要出售掉之前的股票。区别主要是体现在递推公式上,其他都一样。

步骤

  1. 确定dp数组以及下标的含义
  • dp[i][0],表示第i天持有股票所得最多现金
  • dp[i][1],表示第i天不持有股票所得最多现金
  • “持有”不代表就是当天“买入”,也有可能是昨天就买入了,今天保持持有的状态
  1. 确定递推公式
  • dp[i][0]可以由两个状态推出来

    • 第i-1天就持有股票,所得现金=昨天持有股票的所得现金,dp[i - 1][0]
    • 第i天买入股票,所得现金=昨天不持有股票的所得现金减去今天的股票价格,dp[i - 1][1] - prices[i],区别点
    • dp[i][0]取所得现金最大的,dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]);
  • dp[i][1]可以由两个状态推出来

    • 第i-1天就不持有股票,所得现金=昨天不持有股票的所得现金,dp[i - 1][1]
    • 第i天卖出股票,所得现金=按照今天股票价格卖出后所得现金,dp[i - 1][0] + prices[i]
    • dp[i][1]取所得现金最大的,dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i]);
  1. dp数组如何初始化
  • dp[0][0]表示第0天持有股票,dp[0][0] -= prices[0];
  • dp[0][1]表示第0天不持有股票,没有买股票,现金为0,dp[0][1] = 0;
  1. 确定遍历顺序
    dp[i]由dp[i - 1]推导出而来,从前向后遍历

  2. C++实现

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], dp[i-1][1] - prices[i]);//第i天买了股票
            dp[i][1] = max(dp[i-1][1], dp[i-1][0] + prices[i]);//第i天卖了股票
        }
        return dp[n-1][1];

        /*
        //二维dp滚轮数组
        int len = prices.size();
        if(len == 0) return 0;
        vector> dp(2, vector(2));
        dp[0][0] -= prices[0];
        dp[0][1] = 0;
        for(int i=1; i
    }
};

贪心算法

class Solution {
public:
    //贪心算法
    int maxProfit(vector<int>& prices) {
        int result = 0;
        for(int i=1; i<prices.size(); i++)//i从1开始,第二天才有利润
        {
            //累加每天的正利润,最后求的最大利润
            result += max(prices[i]-prices[i-1], 0);
        }
        return result;
    }
};

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

力扣动态规划专题(四)劫舍问题与股票问题 打家劫舍Ⅰ Ⅱ Ⅲ 买卖股票最佳时机Ⅰ Ⅱ Ⅲ IV 步骤及C++实现_第10张图片

和122.买卖股票的最佳时机II主要区别在于至多买卖两次,这意味着可以买卖一次,可以买卖两次,也可以不买卖。因此,可以假设一天有五种状态来记录:
0-没有操作
1-第一次持有股票
2-第一次不持有股票
3-第二次持有股票
4-第二次不持有股票

步骤

  1. 确定dp数组以及下标的含义
    dp[i][j]:i表示第i天,j为 [0 - 4] 五个状态,dp[i][j]表示第i天状态j所剩最大现金。
    要注意的是,dp[i][1]表示的是第i天持有股票的状态,并不一定是第i天买入股票,有可能 第 i-1天 就买入了,那么 dp[i][1] 延续持有股票的这个状态。

  2. 确定递推公式
    五种状态的推导

  • dp[i][1],持有股票,两个状态中取最大值

    • 第i天买入股票,那么dp[i][1] = dp[i-1][0] - prices[i]
    • 第i天没有操作,而是沿用前一天买入的状态,即:dp[i][1] = dp[i - 1][1]
    • dp[i][0]取所得现金最大的,dp[i][1] = max(dp[i-1][0] - prices[i], dp[i - 1][1]);
  • dp[i][2],不持有股票,两个状态中取最大值

    • 第i天卖出股票了,那么dp[i][2] = dp[i - 1][1] + prices[i]
    • 第i天没有操作,沿用前一天卖出股票的状态,即:dp[i][2] = dp[i - 1][2]
    • dp[i][2]取所得现金最大的,dp[i][2] = max(dp[i - 1][1] + prices[i], dp[i - 1][2]);
  • 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]);

  1. dp数组如何初始化
  • 第0天没有操作,dp[0][0] = 0;
  • 第0天第一次买入的操作,dp[0][1] = -prices[0];
  • 第0天第一次卖出的操作,当天买入,当天卖出,dp[0][2] = 0;
  • 第0天第二次买入操作,第二次买入依赖于第一次卖出的状态,相当于第0天买入-卖出-再买入,dp[0][3] = -prices[0];
  • 第0天第二次买入操作,第0天买入-卖出-再买入-再卖出,dp[0][4] = 0;
  1. 确定遍历顺序
    dp[i]依赖dp[i - 1],从前向后遍历

  2. 举例推导dp数组
    输入[1,2,3,4,5]
    力扣动态规划专题(四)劫舍问题与股票问题 打家劫舍Ⅰ Ⅱ Ⅲ 买卖股票最佳时机Ⅰ Ⅱ Ⅲ IV 步骤及C++实现_第11张图片

红色框为最后两次卖出的状态,现金最多的时候一定是卖出的状态,而两次卖出的状态现金最大一定是最后一次卖出,因为dp[i][j]表示第i天状态j所剩最大现金。

  1. C++实现

保存每一天的五种状态

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        if(prices.size() == 0) return 0;
        //保存每一天的五种状态
        vector<vector<int>> dp(prices.size(), vector<int>(5, 0));//第0天没有操作 第一次卖出 第二次卖出操作
        dp[0][1] = -prices[0];//第0天第一次买股票
        dp[0][3] = -prices[0];//第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];//第二次不持有股票的时候所得现金最多
    }
};

滚动数组-优化空间-只保存当天的五种状态

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        if(prices.size() == 0) return 0;
        //保存当天的五种状态
        vector<int> dp(5, 0);
        dp[1] = -prices[0];
        dp[3] = -prices[0];
        for(int i=1; i<prices.size(); i++)
        {
            dp[1] = max(dp[1], dp[0] - prices[i]);//max()里的dp都是前一天的状态
            dp[2] = max(dp[2], dp[1] + prices[i]);
            dp[3] = max(dp[3], dp[2] - prices[i]);
            dp[4] = max(dp[4], dp[3] + prices[i]);
        }
        return dp[4];//第二次不持有股票的时候所得现金最多
    }
};

188.买卖股票的最佳时机IV

力扣动态规划专题(四)劫舍问题与股票问题 打家劫舍Ⅰ Ⅱ Ⅲ 买卖股票最佳时机Ⅰ Ⅱ Ⅲ IV 步骤及C++实现_第12张图片

在 123.买卖股票的最佳时机III的基础上,要求至多有k次交易,那么当天有2k+1次操作:
0-不操作(可以不定义)
1-第一次持有股票
2-第一次不持有股票
3-第二次持有股票
4-第二次不持有股票
5-第三次持有股票

除了0以外,偶数就是卖出,奇数就是买入

步骤

  1. 确定dp数组以及下标的含义
    dp[i][j]:i表示第i天,j有2k+1种状态,dp[i][j]表示第i天状态j所剩最大现金。
    要注意的是,dp[i][1]表示的是第i天持有股票的状态,并不一定是第i天买入股票,有可能 第 i-1天 就买入了,那么 dp[i][1] 延续持有股票的这个状态。

  2. 确定递推公式

  • dp[i][1],持有股票,两个状态中取最大值

    • 第i天买入股票,dp[i][1] = dp[i-1][0] - prices[i]
    • 第i天没有操作,沿用前一天买入的状态,即:dp[i][1] = dp[i - 1][1]
    • dp[i][0]取所得现金最大的,dp[i][1] = max(dp[i-1][0] - prices[i], dp[i - 1][1]);
  • dp[i][2],不持有股票,两个状态中取最大值

    • 第i天卖出股票了,那么dp[i][2] = dp[i - 1][1] + prices[i]
    • 第i天没有操作,沿用前一天卖出股票的状态,即:dp[i][2] = dp[i - 1][2]
    • dp[i][2]取所得现金最大的,dp[i][2] = max(dp[i - 1][1] + prices[i], dp[i - 1][2]);

和123.买卖股票的最佳时机III 最大的区别就是j为奇数是买入,偶数是卖出状态

  1. dp数组如何初始化
  • 第0天没有操作,dp[0][0] = 0;
  • 第0天第一次买入的操作,dp[0][1] = -prices[0];
  • 第0天第一次卖出的操作,当天买入,当天卖出,dp[0][2] = 0;
  • 第0天第二次买入操作,第二次买入依赖于第一次卖出的状态,相当于第0天买入-卖出-再买入,dp[0][3] = -prices[0];
  • 第0天第二次买入操作,第0天买入-卖出-再买入-再卖出,dp[0][4] = 0;

dp[0][j]当j为奇数时都初始化为 -prices[0],j为偶数是卖、奇数是买的状态

  1. 确定遍历顺序
    dp[i]依赖dp[i - 1],从前向后遍历

  2. 举例推导dp数组
    输入[1,2,3,4,5],k=2
    力扣动态规划专题(四)劫舍问题与股票问题 打家劫舍Ⅰ Ⅱ Ⅲ 买卖股票最佳时机Ⅰ Ⅱ Ⅲ IV 步骤及C++实现_第13张图片

最后一次卖出,一定是利润最大的,dp[prices.size() - 1][2 * k],红框部分

  1. C++实现
    注意怎么模拟j为偶数是卖、奇数是买的状态
class Solution {
public:
    int maxProfit(int k, vector<int>& prices) {
        if(prices.size() == 0) return 0;
        vector<vector<int>> dp(prices.size(), vector<int>(2*k+1, 0));
        //初始化dp[i][j] j为奇数的状态
        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];
    }
};

309.最佳买卖股票时机含冷冻期

力扣动态规划专题(四)劫舍问题与股票问题 打家劫舍Ⅰ Ⅱ Ⅲ 买卖股票最佳时机Ⅰ Ⅱ Ⅲ IV 步骤及C++实现_第14张图片
步骤

  1. 确定dp数组以及下标的含义
    dp[i][j],第i天状态为j,所剩的最多现金为dp[i][j]。
    在122.买卖股票的最佳时机II的基础上加了一个冷冻期,相比于122题有两个状态,持有股票和不持有股票,而本题有四个状态:
  • j=0,状态一:持有股票状态,今天买入股票,或者之前买入了股票然后没有操作,一直持有
  • j=1,状态二:不持有股票状态,保持卖出股票的状态,比如两天前卖出了股票,度过一天冷冻期,或者前一天卖出股票状态,一直没操作
  • j=2,状态三:不持有股票状态,今天卖出股票
  • j=3,状态四:今天为冷冻期状态,但冷冻期状态不可持续,只有一天
    要注意的是,冷冻期的前一天,只能是状态三—今天卖出股票状态,如果是状态二—不持有股票状态,就不一定是卖出股票的操作了。
  1. 确定递推公式
  • dp[i][0],状态一,持有股票状态的两种方法
    1)前一天持有股票,dp[i][0] = dp[i - 1][0]
    2)前一天是冷冻期,dp[i - 1][3] - prices[i];或者前一天是保持卖出状态,dp[i - 1][1] - prices[i]
    dp[i][0]取所得现金最大的,dp[i][0] = max(dp[i - 1][0], dp[i - 1][3] - prices[i], dp[i - 1][1] - prices[i]);

  • dp[i][1],状态二,保持卖出股票状态的两种方法
    1)前一天是保持卖出状态,dp[i - 1][1]
    2)前一天是冷冻期,dp[i - 1][3]
    dp[i][1]取所得现金最大的,dp[i][1] = max(dp[i - 1][1], dp[i - 1][3]);

  • dp[i][2],状态三,今天卖出股票状态
    前一天只能是持有股票,今天卖出,dp[i][2] = dp[i - 1][0] + prices[i];

  • dp[i][3],状态四,达到冷冻期状态
    前一天只能是卖出股票状态,dp[i][3] = dp[i - 1][2];

  1. dp数组如何初始化——第0天如何初始化
  • 状态一,持有股票的话一定是当天买入股票,dp[0][0] = -prices[0]
  • 状态二,保持卖出股票状态,如果i为1,第1天买入股票,那么dp[i - 1][1] - prices[i] = dp[0][1] - prices[1]。而dp[0][1],也就是第0天的状态二,只能初始为0。如果初始为其他数值,说明第1天买入股票后手里还剩有现金
  • 状态三,今天卖出了股票,i=1,第1天买入股票再卖出股票,dp[i - 1][1] - prices[i] + prices[i] = dp[0][1] - prices[1] + prices[1]。那么dp[0][2]只能初始化为0。
  • 状态四,同上分析,dp[0][3]也初始为0
  1. 确定遍历顺序
    dp[i]由dp[i - 1]推导出而来,从前向后遍历

  2. 举例推导dp数组
    输入[1,2,3,0,2]
    力扣动态规划专题(四)劫舍问题与股票问题 打家劫舍Ⅰ Ⅱ Ⅲ 买卖股票最佳时机Ⅰ Ⅱ Ⅲ IV 步骤及C++实现_第15张图片
    最后结果是取状态二,状态三,和状态四的最大值。状态四是冷冻期,最后一天如果是冷冻期也可能是最大值。

  3. C++实现

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int n = prices.size();
        if(n == 0) return 0;
        //0-状态一 持有股票
        //1-状态二 保持卖出 之前的某一天卖出了股票
        //2-状态三 今天卖出
        //3-状态四 冷冻期
        vector<vector<int>> dp(n, vector<int>(4, 0));//状态二三四 都初始化为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], 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][1], max(dp[n-1][2], dp[n-1][3]));
    }
};

714.买卖股票的最佳时机含手续费

力扣动态规划专题(四)劫舍问题与股票问题 打家劫舍Ⅰ Ⅱ Ⅲ 买卖股票最佳时机Ⅰ Ⅱ Ⅲ IV 步骤及C++实现_第16张图片
相对于122.买卖股票的最佳时机II,本题只需要在计算卖出操作的时候减去手续费,其他一样,主要区别体现在递推公式

步骤

  1. 确定dp数组以及下标的含义
  • dp[i][0],表示第i天持有股票所得最多现金,之前的某一天买入股票,不一定是今天买入
  • dp[i][1],表示第i天不持有股票所得最多现金
  1. 确定递推公式
  • dp[i][0]可以由两个状态推出来

    • 第i-1天就持有股票,所得现金=昨天持有股票的所得现金,dp[i - 1][0]
    • 第i天买入股票,所得现金=昨天不持有股票的所得现金减去今天的股票价格,dp[i - 1][1] - prices[i]
    • dp[i][0]取所得现金最大的,dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]);
  • dp[i][1]可以由两个状态推出来

    • 第i-1天就不持有股票,所得现金=昨天不持有股票的所得现金,dp[i - 1][1]
    • 第i天卖出股票,所得现金=按照今天股票价格卖出后所得现金,需要给手续费,dp[i - 1][0] + prices[i] - fee
    • dp[i][1]取所得现金最大的,dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i] - fee);
  1. dp数组如何初始化
  • dp[0][0]表示第0天持有股票,买入了股票,dp[0][0] = -prices[0];
  • dp[0][1]表示第0天不持有股票,没有买股票,现金为0,dp[0][1] = 0;
  1. 确定遍历顺序
    dp[i]由dp[i - 1]推导出而来,从前向后遍历

  2. C++实现

class Solution {
public:
    int maxProfit(vector<int>& prices, int fee) {
        int len = prices.size();
        vector<vector<int>> dp(len, vector<int>(2, 0));
        dp[0][0] = -prices[0];
        //0-持有股票
        //1-不持有股票
        for(int i=1; i<len; i++)
        {
            //前一天持有股票,前一天不持有股票,今天买入股票
            dp[i][0] = max(dp[i-1][0], dp[i-1][1] - prices[i]);
            //前一天不持有股票,前一天持有股票,今天卖出去,但要给手续费
            dp[i][1] = max(dp[i-1][1], dp[i-1][0] + prices[i] - fee);
        }
        return dp[len-1][1];
        /*//贪心算法
        int result = 0;
        int minprice = prices[0];
        for(int i=1; i= minprice && prices[i] <= minprice+fee) continue;

            //计算利润 最后一天计算利润才是真的卖出日期
            if(prices[i] > minprice+fee)
            {
                result += prices[i] - minprice - fee;
                minprice = prices[i] - fee;//每天要更新最低价格
            }
        }
        return result;*/
    }
};

劫舍系列分别是198题数组上连续元素二选一,213题成环之后连续元素二选一,377题在树上连续元素二选一,所能得到的最大价值。

股票系列分别是从买卖一次到买卖多次,从最多买卖两次到最多买卖k次,从冷冻期再到手续费:121题只能买卖一次,122题可以买卖多次,123题最多买卖两次,188题最多买卖k次,309题买卖多次且卖出一天有冷冻期,714题买卖多次且有手续费。

你可能感兴趣的:(LeetCode,动态规划,leetcode,c++)