团灭系列之动态规划买卖股票问题

导读

本系列一共分6道题,分别是

Leetcode题号题目
121买卖股票的最佳时机
122买卖股票的最佳时机 II
123买卖股票的最佳时机 III
188买卖股票的最佳时机 IV
309最佳买卖股票时机含冷冻期
714买卖股票的最佳时机含手续费

以上算法均是用动态规划的思想求解,下面分别看每个题是如何解决的。

通用模板:

状态数组dp[i][k][0,1]:即第i天,最多允许交易k次,当前持有[1]或不持有[0]所获得的最大利润
base case:
dp[-1][k][0] = dp[i][0][0] = 0
dp[-1][k][1] = dp[i][0][1] = -infinity
状态转移方程:
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
即第i天不持有,两种情况:本来第i-1天就不持有,直接把状态带过来了;或者第i-1天持有,在第i天以prices[i]的价格卖掉了
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
即第i天持有,两种情况:本来第i-1天就持有,第i天没卖,或第i-1天不持有,在第i天以prices[i]的价格买入
1.买卖股票的最佳时机Leetcode121

动态规划(DP method)
因为题目有一个时间顺序,即卖必须是在买之后发生,故每遍历一个新的价格,更新最小股票价格min_price和最大利润max_profile,若用动态规划思想的话,即定义两个数组min_pricei和max_profilei

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int min_price = INT_MAX;                    // 最小价格初始为无穷大
        int max_profile = 0;                        // 最大利润初始为0
        
        for(int i = 0; i < prices.size(); i++) {
            min_price = min(min_price, prices[i]);                      // 每次都要记录最低价格
            max_profile = max(max_profile, prices[i] - min_price);      //
        }

        return max_profile;
    }
};
2.买卖股票的最佳时机 II / 能进行无数次的股票交易LeetCode122

难度:简单Easy

在本系列模板上直接做修改, d p [ i ] [ k ] [ 0 , 1 ] dp[i][k][0,1] dp[i][k][0,1]由于不考虑交易次数,故中间一维k可以忽略,变为 d p [ i ] [ 0 , 1 ] dp[i][0,1] dp[i][0,1]
初始状态:
dp[0][0] = dp[i][0] = 0
dp[0][1] = dp[i][1] = -prices[0]
状态转移方程: 当k=∞时,则可认为此时k和k-1是一样的,故可以把其忽略
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]) = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k][0] - prices[i]) = max(dp[i-1][1], dp[i-1][0] - prices[i])
即状态方程为i行2列的一个二维状态数组,如下:

第0天不持有第0天持有
第1天不持有第1天持有
第2天不持有第2天持有
…………
第i天不持有第i天持有

对于第i天持有/不持有,只看前一天持有/不持有的情况,故可以定义两个量preNonHold和preHold,来记录前一天持有/不持有的情况

class Solution {
    public int maxProfit(int[] prices) {
        int len = prices.length;                                    // 养成好习惯
        if (len < 2) {
            return 0;
        }
        int[][] dp = new int[len][2];

        // 初始状态
        dp[0][0] = 0;                                               // 第0天不买入
        dp[0][1] = -prices[0];                                      // 第0天买入

        for (int i = 1; i < len; i++) {
            dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
            dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
        }
        return dp[len - 1][0];                                      // 返回的肯定是手里不持有的情况
    }
}

简单方法:
定义一个变量tmp,当某天的价格比前一天高时,就计算差值并加到tmp里,小于就不计算。

class Solution {
public:
    int maxProfit(vector<int> &prices) {
        int res = 0;
        for (int i = 1; i < prices.size(); ++i) 
            res += max(prices[i] - prices[i - 1], 0);               // 比前一天小的情况就不算了
        return res;
    }
};
3.买卖股票的最佳时机 III / 只能进行两次的股票交易LeetCode123

难度:困难Hard

还是在本系列通用模板上进行修改,状态方程:dp[i][k][0,1]:第[i]天,最多允许交易k次,不持有股票[0]或持有股票[1]
base case:
一共涉及到两次交易: k=1或k=2,以下两种情况均是
dp[0][0] = 0: 第1天不持有;
dp[0][1] = -prices[0]: 第1天持有;
状态转移方程:
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
即之前不持有,或者前一天持有,当天卖掉,完成第k次交易.
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
即之前持有,或者之前不持有,当前买入,开启新的一次交易
注意买了再卖算同一次交易(一次完整的交易),卖了在买,买的时候算下一次交易了(k须+1)

class Solution {
    public int maxProfit(int[] prices) {
        if (prices.length == 0) return 0;
        
        int n = prices.length;
        int[][][] dp = new int[n][3][2];                 // k的第0维没什么用
        dp[0][1][0] = 0;                                 // 第一次交易,无持有,第[0]天肯定为0
        dp[0][1][1] = -prices[0];                        // 第一次交易,持有(买入第一天的股票)
        dp[0][2][0] = 0;                                 // 第二次交易,无持有,第[0]天肯定为0
        dp[0][2][1] = -prices[0];                        // 第一次交易,持有(买入第一天的股票)(不合逻辑)
        
        for (int i = 1; i < n; i++) {
            dp[i][1][0] = Math.max(dp[i-1][1][0], dp[i-1][1][1] + prices[i]);
            // 第1次交易,不持有: 要么前一天不持有,要么前一天持有,当天卖了,完成第1次交易;
            dp[i][1][1] = Math.max(dp[i-1][1][1], -prices[i]);
            // 第1次交易,持有: 要么前一天就持有,要么买入一只股票,开启第1次交易;
            dp[i][2][0] = Math.max(dp[i-1][2][0], dp[i-1][2][1] + prices[i]);
            // 第2次交易,不持有: 要么前一天不持有,要么前一天持有,当天卖了,完成第2次交易;
            dp[i][2][1] = Math.max(dp[i-1][2][1], dp[i-1][1][0] - prices[i]);
            // 第2次交易,持有: 要么前一天就持有,要么前一天没持有,买入当前的股票,开启第2次交易
        }
        return dp[n-1][2][0];                               // 第[2]维为0代表完成第k次交易了
    }
}
class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        # 特判
        if prices is None:
            return 0
        
        n = len(prices)
        # 创建三维数组: 第[0]维是天数,第[1]维是交易次数(大小为3,第1个元素没用),第[2]维是是否持有(大小为2)
        # 注意python定义三维数组的时候有坑, 先定义第[2]维,再定义第[1]维,最后定义第[0]维
        dp = [[[0 for i in range(2)] for j in range(3) ] for k in range(n)]
        
        dp[0][1][0] = 0;                                 # 第一次交易,无持有,第[0]天肯定为0
        dp[0][1][1] = -prices[0];                        # 第一次交易,持有(买入第一天的股票)
        dp[0][2][0] = 0;                                 # 第二次交易,无持有,第[0]天肯定为0
        dp[0][2][1] = -prices[0];                        # 第一次交易,持有(买入第一天的股票)(不合逻辑)
        
        for i in range(1, n):
            dp[i][1][0] = max(dp[i-1][1][0], dp[i-1][1][1] + prices[i])
            # 第1次交易,不持有: 要么前一天不持有,要么前一天持有,当天卖了,完成第1次交易;
            dp[i][1][1] = max(dp[i-1][1][1], -prices[i])
            # 第1次交易,持有: 要么前一天就持有,要么买入一只股票,开启第1次交易;
            dp[i][2][0] = max(dp[i-1][2][0], dp[i-1][2][1] + prices[i])
            # 第2次交易,不持有: 要么前一天不持有,要么前一天持有,当天卖了,完成第2次交易;
            dp[i][2][1] = max(dp[i-1][2][1], dp[i-1][1][0] - prices[i])
            # 第2次交易,持有: 要么前一天就持有,要么前一天没持有,买入当前的股票,开启第2次交易
        
        return dp[n - 1][2][0]                          # 第[2]维为0代表完成第k次交易了
4.买卖股票的最佳时机 IV / 只能进行 k 次的股票交易LeetCode188

难度:困难Hard

idea: 动态规划(DP method)
状态方程:dp[i][k][0,1]:第[i]天,最多允许交易k次,不持有股票[0]或持有股票[1]
base case:
dp[0][k][0] = dp[i][0][0] = dp[i][0][1] = 0
dp[0][k][1] = -prices[0]
状态转移方程:
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
当k = any integer时,上面的状态转移方程没有可简化的地方了
也不能像上一题一样枚举k,故此时只能遍历k

class Solution {
    public int maxProfit(int k, int[] prices) {
        if (prices.length == 0) return 0;
        
        int n = prices.length;
        int max_k = k;
        if (max_k >= n / 2) {                                 // 相当于可以交易无限次了
            int res = 0;
            for (int i = 1; i < n; i++) {
                int value = prices[i] - prices[i - 1];
                if (value > 0) res += value;
            }
            return res;
        }        
        
        int[][][] dp = new int[n][max_k + 1][2];
        
        // 赋初值,无论第几次的交易在第[0]天持有为"-prices[0]";无持有为"0"
        for (int kk = 0; kk <= max_k; kk++) {
            dp[0][kk][0] = 0;
        }
        for (int kk = 0; kk <= max_k; kk++) {
            dp[0][kk][1] = -prices[0];
        }        
        
        // 令i=1,kk=1时,代入下式发现有3个值需要初始化: dp[0][0][0],dp[0][1][1],dp[0][1][0]
        for (int i = 1; i < n; i++) {
            for (int kk = 1; kk <= max_k; kk++) {         // 一次交易至少需要两天,故说有效的限制k不超过n/2
                dp[i][kk][0] = Math.max(dp[i-1][kk][0], dp[i-1][kk][1] + prices[i]);
                dp[i][kk][1] = Math.max(dp[i-1][kk][1], dp[i-1][kk-1][0] - prices[i]);
            }
        }
        return dp[n-1][max_k][0];
    }
}
5.最佳买卖股票时机含冷冻期 / 需要冷却期的股票交易LeetCode309

idea: 动态规划(DP method)
状态方程:dp[i][k][0,1]:第[i]天,最多允许交易k次,不持有股票[0]或持有股票[1]
base case:
dp[-1][k][0] = dp[i][0][0] = 0
dp[-1][k][1] = dp[i][0][1] = -infinity
状态转移方程:
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
当k=∞, 并且含有冷冻期,即每次sell之后要等一天才能继续交易
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
dp[i][k][1] = max(dp[i-1][k][1], dp[i-2][k][0] - prices[i])
注:第[i]天buy的时候,上一次交易需要在[i-2]天卖,而不是i-1,因为第[i-1]天卖了不能在第[i]天买

class Solution {
    public int maxProfit(int[] prices) {
        if (prices.length == 0 || prices.length == 1) return 0;
        
        int n = prices.length;
        int[][] dp = new int[n][2];
        // 因为下面的for循环需要从i=2开始,所以需要赋4个初值
        dp[0][0] = 0;
        dp[0][1] = -prices[0];
        dp[1][0] = Math.max(dp[0][0], dp[0][1] + prices[1]);
        dp[1][1] = Math.max(-prices[0], -prices[1]);
        
        for (int i = 2; i < n; i++) {
            dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]);
            dp[i][1] = Math.max(dp[i-1][1], dp[i-2][0] - prices[i]);
        }
        return dp[n - 1][0];
    }
}
6.买卖股票的最佳时机含手续费 / 需要交易费用的股票交易LeetCode714

idea: 动态规划(DP method) 状态方程:dp[i][k][0,1]:第[i]天,最多允许交易k次,不持有股票[0]或持有股票[1]
base case:
dp[-1][k][0] = dp[i][0][0] = 0
dp[-1][k][1] = dp[i][0][1] = -infinity
状态转移方程:
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
当k=∞时,则可认为此时k和k-1是一样的,又因每次交易要支付手续费,故只要把手续费从利润中减去即可
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k][0] - prices[i] - fee)

class Solution {
    public int maxProfit(int[] prices, int fee) {
        // 特判
        if (prices.length == 0) return 0;
        
        int n = prices.length;
        int[][] dp = new int[n][2];
        dp[0][0] = 0;
        dp[0][1] = -prices[0] - fee;                                 // 初始也要减手续费        
        
        for (int i = 1; i < n; i++) {
            dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]);
            dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] - prices[i] - fee);
        }
        return dp[n - 1][0];
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值