LeetCode股票交易问题系列记录

在LeetCode上有六道股票交易系列的算法习题,虽然这些习题的标签基本都是动态规划,但是初学者似乎很难短时间内写出高效的解决方案。笔者之前在刷这些习题的时候碰到了一个十分不错的想法,即动态规划结合有限状态机的思想来确定状态转移方程,笔者这里简单的记录一下供大家阅读拓展思路。

通用模板

利用“状态”进行穷举,具体到每一天,看看总共有集中可能的“状态“,再找出每个状态对应的选择。伪代码如下:

for 状态1 in 状态1的所有取值:
    for 状态2 in 状态2的所有取值:
        for ...
            dp[状态1][状态2][...] = 择优(选择1, 选择2...)

上述伪代码中,数组 dp 的维度主要受待求解值受多少个因素的影响而决定,在买卖股票的问题中,最大利润受股票交易天数、股票交易次数以及最后一天是否卖出股票共同影响。


枚举状态

在股票交易问题中,每一天都有三种选择:买入、卖出、无操作,分别用不buy、sell和reset表示这三种动作。注意,这三种动作是由顺序关系的,即sell必须在buy之后,buy必须在sell之后,而reset分为两种,即buy之后的reset和sell之后的reset。另外,还有交易次数 k 的限制,即buy只能发生在 k>0 的前提下。

股票交易问题的状态有三个,第一个是天数,第二个是允许交易的最大次数,第三个是当前的持有状态(1表示持有,0表示未持有),可以用一个三维数组表示这些状态的全部组合:

dp[i][k][0 or 1]: 0 <= i <= n-1, 1 <= k <= K,n为天数,K为最多交易次数,即

for 0 <= i < n:
	for 1 <= k <= K:
		for s in {0, 1}:
			dp[i][k][s] = max{buy, sell, reset}	// 当前最大利润

例如, d p [ 3 ] [ 2 ] [ 1 ] dp[3][2][1] dp[3][2][1] 表示当前是第三天且持有股票,最多可进行两次交易; d p [ 2 ] [ 3 ] [ 0 ] dp[2][3][0] dp[2][3][0] 表示当前是第二天且没有持有股票,最多可进行三次交易。


状态转移

状态转移图如下所示:

在这里插入图片描述
根据这个状态转移图可以看到每种状态是如何转变过来的。根据这个图,可以写出状态转移方程如下:

dp[i][k][0] = max{dp[i-1][k][0], dp[i-1][k][1] + prices[i]}
			= max{reset, sell}

dp[i][k][1] = max{dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]}	// k-1表示最多可进行的交易次数减一,因为buy
			= max{reset, buy}

定义基本情况(边界)如下:

dp[-1][k][0] = 0;
dp[-1][k][1] = -infinity;
dp[i][0][0] = 0;
dp[i][0][1] = -infinity;

0121. 买卖股票的最佳时机

给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。如果你最多只允许完成一笔交易(即买入和卖出一支股票),设计一个算法来计算你所能获取的最大利润。注意你不能在买入股票前卖出股票。

示例:

输入: [7,1,5,3,6,4]
输出: 5
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
     注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格。

解题思路:根据状态转移方程以及基本情况,此时 k=1,有

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

将数组 dp 压缩成二维数组,状态转移公式如下:

    public int maxProfit(int[] prices) {
        if (prices.length == 0 || prices.length == 1) return 0;
        int[][] dp = new int[prices.length][2];
        dp[0][0] = 0;
        dp[0][1] = -prices[0];
        for (int i = 1; i < prices.length; 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], -prices[i]);
        }
        return dp[prices.length - 1][0];
    }

进一步压缩空间如下:

    public int maxProfit(int[] prices) {
        int profit = 0, price = Integer.MIN_VALUE;
        for (int i = 0; i < prices.length; i++) {
            profit = Math.max(profit, price + prices[i]);
            price = Math.max(price, -prices[i]);
        }
        return profit;
    }

而负数的最大值,即正数的最小值,因此又可以写为:

    public int maxProfit(int[] prices) {
        int profit = 0, min = Integer.MAX_VALUE;
        for (int i = 0; i < prices.length; i++) {
            profit = Math.max(profit, prices[i] - min);
            min = Math.min(min, prices[i]);
        }
        return profit;
    }

以上代码可以这样理解。在仅交易一次的前提条件下,找到当前价格的最低价,然后再后面数组中找到利润的最大值。时间复杂度为 O(n),空间复杂度为 O(1)


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

给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例:

输入: [7,1,5,3,6,4]
输出: 7
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
     随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3

解题思路:此时 k = + ∞ k=+\infty k=+,此时状态转移方程为:

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=+\infty k=+,可以认为 k ∼ k − 1 k\sim k-1 kk1,那么第二维下标 k 同样可以直接被压缩,则状态转移方程如下:

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

    public int maxProfit(int[] prices) {
        int[][] dp = new int[prices.length][2];
        for (int i = 0; i < prices.length; i++) {
            if (i == 0) {
                dp[0][0] = 0;
                dp[0][1] = -prices[i];
                continue;
            }
            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 prices.length < 2 ? 0 : dp[prices.length - 1][0];
    }

同样压缩空间如下:

    public int maxProfit(int[] prices) {
        if (prices.length == 0) return 0;
        int profit = 0, min = prices[0];
        for (int i = 0; i < prices.length; i++) {
            int pre = profit;
            profit = Math.max(profit, prices[i] - min);
            min = Math.min(min, prices[i] - pre);
        }
        return profit;
    }

时间复杂度为 O(n),空间复杂度为 O(1)


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

给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。注意: 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例:

输入: [3,3,5,0,0,3,1,4]
输出: 6
解释: 在第 4 天(股票价格 = 0)的时候买入,在第 6 天(股票价格 = 3)的时候卖出,这笔交易所能获得利润 = 3-0 = 3 。
     随后,在第 7 天(股票价格 = 1)的时候买入,在第 8 天 (股票价格 = 4)的时候卖出,这笔交易所能获得利润 = 4-1 = 3

解题思路:与上一题不同,这道题里 k = 2 k=2 k=2,此时状态转移方程为

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

    public int maxProfit(int[] prices) {
        int K = 2;
        int[][][] dp = new int[prices.length][K + 1][2];
        for (int i = 0; i < prices.length; i++) {
            for (int k = 1; k <= K; k++) {
                if (i == 0) {
                    dp[0][k][0] = 0;
                    dp[0][k][1] = -prices[i];
                    continue;
                }
                dp[i][k][0] = Math.max(dp[i - 1][k][0], dp[i - 1][k][1] + prices[i]);
                dp[i][k][1] = Math.max(dp[i - 1][k][1], dp[i - 1][k - 1][0] - prices[i]);
            }
        }
        return prices.length >= 2 ? dp[prices.length - 1][K][0] : 0;
    }

注意到 k 的取值有限且较小,那么枚举出数组 dp 的各种取值如下:

dp[i][1][0] = max(dp[i-1][1][0], dp[i-1][1][1] + prices[i]);
dp[i][1][1] = max(dp[i-1][1][1], -prices[i]);	// dp[i-1][0][0] = 0;
dp[i][2][0] = max(dp[i-1][2][0], dp[i-1][2][1] + prices[i]);
dp[i][2][1] = max(dp[i-1][2][1], dp[i-1][1][0] - prices[i]);

空间优化后的代码如下:

    public int maxProfit(int[] prices) {
        if (prices.length == 0) return 0;
        int firstBuy = 0, firstMin = Integer.MAX_VALUE, secondBuy = 0, secondMin = prices[0];
        for (int i = 0; i < prices.length; i++) {
            int temp = firstBuy;
            firstBuy = Math.max(firstBuy, prices[i] - firstMin);
            firstMin = Math.min(firstMin, prices[i]);
            secondBuy = Math.max(secondBuy, prices[i] - secondMin);
            secondMin = Math.min(secondMin, prices[i] - temp);
        }
        return secondBuy;
    }

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

给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。注意: 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例:

输入: [3,2,6,5,0,3], k = 2
输出: 7
解释: 在第 2(股票价格 = 2) 的时候买入,在第 3(股票价格 = 6) 的时候卖出, 这笔交易所能获得利润 = 6-2 = 4 。
     随后,在第 5(股票价格 = 0) 的时候买入,在第 6(股票价格 = 3) 的时候卖出, 这笔交易所能获得利润 = 3-0 = 3

解题思路:注意此处的 k 值作为参数给定,有状态转移公式如下:

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

    public int maxProfit(int k, int[] prices) {
        int[][][] dp = new int[prices.length][k + 1][2];
        for (int i = 0; i < dp.length; i++) {
            for (int j = 1; j <= k; j++) {
                if (i == 0) {
                    dp[0][j][0] = 0;
                    dp[0][j][1] = -prices[i];
                    continue;
                }
                dp[i][j][0] = Math.max(dp[i - 1][j][0], dp[i - 1][j][1] + prices[i]);
                dp[i][j][1] = Math.max(dp[i - 1][j][1], dp[i - 1][j - 1][0] - prices[i]);
            }
        }
        return prices.length > 1 ? dp[prices.length - 1][k][0] : 0;
    }

但是此时需要注意 k 的取值可能会很大,当 k > prices.length >>1 时,就等价于 k = + ∞ k = +\infty k=+,因此上述代码需改写为

    public int maxProfit(int k, int[] prices) {
        if (k > prices.length >> 1) return maxProfit(prices);
        int[][] dp1 = new int[k + 1][2], dp2 = new int[k + 1][2];	// 滚动数组减少空间存储开销
        for (int i = 0; i < prices.length; i++) {
            for (int j = 1; j <= k; j++) {
                if (i == 0) {
                    dp2[j][0] = 0;
                    dp2[j][1] = -prices[i];
                    continue;
                }
                dp2[j][0] = Math.max(dp1[j][0], dp1[j][1] + prices[i]);
                dp2[j][1] = Math.max(dp1[j][1], dp1[j - 1][0] - prices[i]);
            }
            dp1 = dp2;
        }
        return prices.length > 1 ? dp2[k][0] : 0;
    }

    public int maxProfit(int[] prices) {
        if (prices.length == 0) return 0;
        int profit = 0, min = prices[0];
        for (int i = 0; i < prices.length; i++) {
            int pre = profit;
            profit = Math.max(profit, prices[i] - min);
            min = Math.min(min, prices[i] - pre);
        }
        return profit;
    }

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

给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):

  • 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
  • 卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。

示例:

输入: [1,2,3,0,2]
输出: 3 
解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]

解题思路:题中卖出股票的冷冻期意味着若更新持有股票的状态,只能以前两天的状态进行转移,而更新未持有股票的状态则只需要前一天的状态即可,即

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-1][0] - prices[i]);

此时有 k = + ∞ k = +\infty k=+,那么

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

    public int maxProfit(int[] prices) {
        int[][] dp = new int[prices.length][2];
        for (int i = 0; i < prices.length; i++) {
            if (i == 0) {
                dp[i][0] = 0;
                dp[i][1] = -prices[i];
                continue;
            }
            dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
            if (i == 1) {
                dp[i][1] = Math.max(-prices[i], dp[i - 1][1]);
                continue;
            }
            dp[i][1] = Math.max(dp[i - 1][1], dp[i - 2][0] - prices[i]);
        }
        return prices.length > 1 ? dp[prices.length - 1][0] : 0;
    }

压缩存储空间(注意技巧)如下:

    public int maxProfit(int[] prices) {
        if (prices.length == 0) return 0;
        int profit = 0, min = prices[0], val = 0;
        for (int i = 0; i < prices.length; i++) {
            int pre = profit;
            profit = Math.max(profit, prices[i] - min);
            min = Math.min(min, prices[i] - val);
            val = pre;		// 处理dp[i-2][0]的技巧
        }
        return profit;
    }

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

给定一个整数数组 prices,其中第 i 个元素代表了第 i 天的股票价格 ;非负整数 fee 代表了交易股票的手续费用。你可以无限次地完成交易,但是你每次交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。返回获得利润的最大值。

示例:

输入: prices = [1, 3, 2, 8, 4, 9], fee = 2
输出: 8
解释: 能够达到的最大利润:  
在此处买入 prices[0] = 1
在此处卖出 prices[3] = 8
在此处买入 prices[4] = 4
在此处卖出 prices[5] = 9
总利润: ((8 - 1) - 2) + ((9 - 4) - 2) = 8.

解题思路:在 k = + ∞ k=+\infty k=+ 的情况下,状态转移方程为

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

    public int maxProfit(int[] prices, int fee) {
        int[][] dp = new int[prices.length][2];
        for (int i = 0; i < prices.length; i++) {
            if (i == 0) {
                dp[i][0] = 0;
                dp[i][1] = -prices[i];
                continue;
            }
            dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i] - fee);
            dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
        }
        return prices.length > 1 ? dp[prices.length - 1][0] : 0;
    }

压缩存储空间:

    public int maxProfit(int[] prices, int fee) {
        if (prices.length == 0) return 0;
        int profit = 0, min = prices[0];
        for (int i = 0; i < prices.length; i++) {
            int pre = profit;
            profit = Math.max(profit, prices[i] - min - fee);
            min = Math.min(min, prices[i] - pre);
        }
        return profit;
    }

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值