团灭 LeetCode 股票买卖问题

目录

1、动态规划架构

1.1、穷举框架

1.2、状态转移框架

2、LeetCode121. 买卖股票的最佳时机

2.1、动态规划

2.2、寻找历史最小值

3、LeetCode122. 买卖股票的最佳时机 II

3.2、峰谷法

3.3、贪心算法(遇到递增就累加)

4、LeetCode123. 买卖股票的最佳时机 III

5、LeetCode188. 买卖股票的最佳时机 IV

6、LeetCode309. 最佳买卖股票时机含冷冻期

7、LeetCode714. 买卖股票的最佳时机含手续费


1、动态规划架构

1.1、穷举框架

        首先,还是一样的思路:如何穷举?这里的穷举思路和上篇文章递归的思想不太一样。

        递归其实是符合我们思考的逻辑的,一步步推进,遇到无法解决的就丢给递归,一不小心就做出来了,可读性还很好。缺点就是一旦出错,你也不容易找到错误出现的原因。比如上篇文章的递归解法,肯定还有计算冗余,但确实不容易找到。

        而这里,我们不用递归思想进行穷举,而是利用「状态」进行穷举。我们具体到每一天,看看总共有几种可能的「状态」,再找出每个「状态」对应的「选择」。我们要穷举所有「状态」,穷举的目的是根据对应的「选择」更新状态。听起来抽象,你只要记住「状态」和「选择」两个词就行,下面实操一下就很容易明白了。

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

       比如说这个问题,每天都有三种「选择」:买入、卖出、无操作,我们用 buy, sell, rest 表示这三种选择。但问题是,并不是每天都可以任意选择这三种选择的,因为 sell 必须在 buy 之后,buy 必须在 sell 之后。那么 rest 操作还应该分两种状态,一种是 buy 之后的 rest(持有了股票),一种是 sell 之后的 rest(没有持有股票)。而且别忘了,我们还有交易次数 k 的限制,就是说你 buy 还只能在 k > 0 的前提下操作。

        很复杂对吧,不要怕,我们现在的目的只是穷举,你有再多的状态,老夫要做的就是一把梭全部列举出来。这个问题的「状态」有三个,第一个是天数,第二个是允许交易的最大次数,第三个是当前的持有状态(即之前说的 rest 的状态,我们不妨用 1 表示持有,0 表示没有持有)。然后我们用一个三维数组就可以装下这几种状态的全部组合:

dp[i][k][0 or 1]
0 <= i <= n-1, 1 <= k <= K
n 为天数,大 K 为最多交易数
此问题共 n × K × 2 种状态,全部穷举就能搞定。

for 0 <= i < n:
    for 1 <= k <= K:
        for s in {0, 1}:
            dp[i][k][s] = max(buy, sell, rest)

 

        而且我们可以用自然语言描述出每一个状态的含义,比如说 dp[3][2][1] 的含义就是:今天是第三天,我现在手上持有着股票,至今最多进行 2 次交易。再比如 dp[2][3][0] 的含义:今天是第二天,我现在手上没有持有股票,至今最多进行 3 次交易。很容易理解,对吧?

        我们想求的最终答案是 dp[n - 1][K][0],即最后一天,最多允许 K 次交易,最多获得多少利润。读者可能问为什么不是 dp[n - 1][K][1]?因为 [1] 代表手上还持有股票,[0] 表示手上的股票已经卖出去了,很显然后者得到的利润一定大于前者。

记住如何解释「状态」,一旦你觉得哪里不好理解,把它翻译成自然语言就容易理解了。

1.2、状态转移框架

        现在,我们完成了「状态」的穷举,我们开始思考每种「状态」有哪些「选择」,应该如何更新「状态」。只看「持有状态」,可以画个状态转移图。

 

          通过这个图可以很清楚地看到,每种状态(0 和 1)是如何转移而来的。根据这个图,我们来写一下状态转移方程:

dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
              max(   选择 rest  ,             选择 sell      )

解释:今天我没有持有股票,有两种可能:
要么是我昨天就没有持有,然后今天选择 rest,所以我今天还是没有持有;
要么是我昨天持有股票,但是今天我 sell 了,所以我今天没有持有股票了。

dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
              max(   选择 rest  ,           选择 buy         )

解释:今天我持有着股票,有两种可能:
要么我昨天就持有着股票,然后今天选择 rest,所以我今天还持有着股票;
要么我昨天本没有持有,但今天我选择 buy,所以今天我就持有股票了。

 

        这个解释应该很清楚了,如果 buy,就要从利润中减去 prices[i],如果 sell,就要给利润增加 prices[i]。今天的最大利润就是这两种可能选择中较大的那个。而且注意 k 的限制,我们在选择 buy 的时候,把 k 减小了 1,很好理解吧,当然你也可以在 sell 的时候减 1,一样的。

        现在,我们已经完成了动态规划中最困难的一步:状态转移方程。如果之前的内容你都可以理解,那么你已经可以秒杀所有问题了,只要套这个框架就行了。不过还差最后一点点,就是定义 base case,即最简单的情况。

dp[-1][k][0] = 0
解释:因为 i 是从 0 开始的,所以 i = -1 意味着还没有开始,这时候的利润当然是 0 。
dp[-1][k][1] = -infinity
解释:还没开始的时候,是不可能持有股票的,用负无穷表示这种不可能。
dp[i][0][0] = 0
解释:因为 k 是从 1 开始的,所以 k = 0 意味着根本不允许交易,这时候利润当然是 0 。
dp[i][0][1] = -infinity
解释:不允许交易的情况下,是不可能持有股票的,用负无穷表示这种不可能。

把上面的状态转移方程总结一下:

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

        读者可能会问,这个数组索引是 -1 怎么编程表示出来呢,负无穷怎么表示呢?这都是细节问题,有很多方法实现。现在完整的框架已经完成,下面开始具体化几道题。

2、LeetCode121. 买卖股票的最佳时机

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

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

示例 2:
输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。

2.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])
解释:k = 0 的 base case,所以 dp[i-1][0][0] = 0。

现在发现 k 都是 1,不会改变,即 k 对状态转移已经没有影响了。
可以进行进一步化简去掉所有 k:
dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][1] = max(dp[i-1][1], -prices[i])

直接写出代码:

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

显然 i = 0 时 dp[i-1] 是不合法的。这是因为我们没有对 i 的 base case 进行处理。可以这样处理:

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

2.2、寻找历史最小值

假设给定的数组为:[7, 1, 5, 3, 6, 4]

如果我们在图表上绘制给定数组中的数字,我们将会得到:

                        Profit Graph

       我们来假设自己来购买股票。随着时间的推移,每天我们都可以选择出售股票与否。那么,假设在第 i 天,如果我们要在今天卖股票,那么我们能赚多少钱呢?

        显然,如果我们真的在买卖股票,我们肯定会想:如果我是在历史最低点买的股票就好了!太好了,在题目中,我们只要用一个变量记录一个历史最低价格 minprice,我们就可以假设自己的股票是在那天买的。那么我们在第 i 天卖出股票能得到的利润就是 prices[i] - minprice。

       因此,我们只需要遍历价格数组一遍,记录历史最低点,然后在每一天考虑这么一个问题:如果我是在历史最低点买进的,那么我今天卖出能赚多少钱?当考虑完所有天数之时,我们就得到了最好的答案。

	public int maxProfit(int[] prices) {
		if(prices == null || prices.length == 0){
			return 0;
		}
		int maxProfit =0;
		int minVal = prices[0];
		for(int i = 1; i < prices.length;i++){
			if(prices[i] < minVal){
				minVal = prices[i];
			}else{
				maxProfit = Math.max(maxProfit,prices[i] - minVal);
			}
		}
		return maxProfit;
    }

3、LeetCode122. 买卖股票的最佳时机 II

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

3.1、动态规划

如果 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-1][0] - prices[i])
            = max(dp[i-1][k][1], dp[i-1][k][0] - prices[i])

我们发现数组中的 k 已经不会改变了,也就是说不需要记录 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) {
		if(prices == null || prices.length == 0){
			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], dp[i-1][0] - prices[i]);
		}
		return dp[prices.length-1][0];
	}

3.2、峰谷法

算法:

假设给定的数组为:[7, 1, 5, 3, 6, 4],如果我们在图表上绘制给定数组中的数字,我们将会得到:

Profit Graph

如果我们分析图表,那么我们的兴趣点是连续的峰和谷。

用数学语言描述为:

                 

      关键是我们需要考虑到紧跟谷的每一个峰值以最大化利润。如果我们试图跳过其中一个峰值来获取更多利润,那么我们最终将失去其中一笔交易中获得的利润,从而导致总利润的降低。

例如,在上述情况下,如果我们跳过 peak_i  和 valley_j​ 试图通过考虑差异较大的点以获取更多的利润,获得的净利润总是会小与包含它们而获得的净利润,因为 C 总是小于 A + B

	public int maxProfit(int[] prices) {
		if(prices == null || prices.length == 0){
			return 0;
		}
		
		int sumProfit = 0;
		int peekVal = 0,valleyVal = 0;
		for(int i = 0; i < prices.length; i++){
			while(i+1 < prices.length && prices[i+1] <= prices[i]){
				i++;
			}
			valleyVal = prices[i];
			
			while(i+1 < prices.length && prices[i+1] >= prices[i]){
				i++;
			}
			
			peekVal = prices[i];
			sumProfit += peekVal - valleyVal;
		}
		return sumProfit;
	}

3.3、贪心算法(遇到递增就累加)

算法:

       该解决方案遵循 方法二 的本身使用的逻辑,但有一些轻微的变化。在这种情况下,我们可以简单地继续在斜坡上爬升并持续增加从连续交易中获得的利润,而不是在谷之后寻找每个峰值。最后,我们将有效地使用峰值和谷值,但我们不需要跟踪峰值和谷值对应的成本以及最大利润,但我们可以直接继续增加加数组的连续数字之间的差值,如果第二个数字大于第一个数字,我们获得的总和将是最大利润。这种方法将简化解决方案。
这个例子可以更清楚地展现上述情况:[1, 7, 2, 3, 6, 7, 6, 7],与此数组对应的图形是:

Profit Graph

从上图中,我们可以观察到  A+B+C 的和等于差值 D 所对应的连续峰和谷的高度之差。

class Solution {
    public int maxProfit(int[] prices) {
        int maxprofit = 0;
        for (int i = 1; i < prices.length; i++) {
            if (prices[i] > prices[i - 1])
                maxprofit += prices[i] - prices[i - 1];
        }
        return maxprofit;
    }
}

4、LeetCode123. 买卖股票的最佳时机 III

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

        这道题由于没有消掉 k 的影响,所以必须要对 k 进行穷举,这里 k 取值范围比较小,所以可以不用 for 循环,直接把 k = 1 和 2 的情况全部列举出来也可以:

	public int maxProfit(int[] prices) {
		if(prices == null || prices.length == 0){
			return 0;
		}

		int [][][]dp = new int[prices.length][3][2];
		dp[0][1][0] = 0;
		dp[0][1][1] = -prices[0];

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

		for(int i = 1;i < prices.length; i++){
			dp[i][1][0] = Math.max(dp[i-1][1][0],dp[i-1][1][1] + prices[i]);
			dp[i][1][1] = Math.max(dp[i-1][1][1], 0 - prices[i]);

			dp[i][2][0] = Math.max(dp[i-1][2][0],dp[i-1][2][1] + prices[i]);
			dp[i][2][1] = Math.max(dp[i-1][2][1],dp[i-1][1][0] - prices[i]);
		}
		return dp[prices.length-1][2][0];
	}

5、LeetCode188. 买卖股票的最佳时机 IV

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

        有了上一题 k = 2 的铺垫,这题应该和上一题的第一个解法没啥区别。但是出现了一个超内存的错误,原来是传入的 k 值会非常大,dp 数组太大了。现在想想,交易次数 k 最多有多大呢?

       一次交易由买入和卖出构成,至少需要两天。所以说有效的限制 k 应该不超过 n/2,如果超过,就没有约束作用了,相当于 k = +infinity。这种情况是之前解决过的。

	public int maxProfit(int k, int[] prices) {
		if(prices == null || prices.length == 0){
			return 0;
		}

		if(k > prices.length/2){
			return maxProfit(prices);
		}

		int [][][]dp = new int[prices.length][k+1][2];		
		for(int i = 0;i < prices.length; i++){
			dp[i][0][0] = 0;
			dp[i][0][1] = -Integer.MAX_VALUE;					
			for(int j = 1; j <= k; j++){
				if(i == 0){
					dp[0][j][0] = 0;
					dp[0][j][1] = -prices[0];
				}else{
					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 dp[prices.length-1][k][0];
	}

6、LeetCode309. 最佳买卖股票时机含冷冻期

给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。​

设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):

  • 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
  • 卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
示例:
输入: [1,2,3,0,2]
输出: 3 
解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]

  每次 sell 之后要等一天才能继续交易。只要把这个特点融入上一题的状态转移方程即可:

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])
解释:第 i 天选择 buy 的时候,要从 i-2 的状态转移,而不是 i-1 。

注意:i = 0,1时均可以买股票,其余情况必须是 i - 2 的无股票状态迁移

	public int maxProfit(int[] prices) {
		if(prices == null || prices.length == 0){
			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]);
			if(i >= 2){
				dp[i][1] = Math.max(dp[i-1][1], dp[i-2][0] - prices[i]);
			}else{
				dp[i][1] = Math.max(dp[i-1][1], 0 - prices[i]);//i=1时可以买股票
			}
		}
		return dp[prices.length-1][0];
	}

7、LeetCode714. 买卖股票的最佳时机含手续费

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

  • 注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。

每次交易要支付手续费,只要把手续费从利润中减去即可。改写方程:

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)
解释:相当于买入股票的价格升高了。
在第一个式子里减也是一样的,相当于卖出股票的价格减小了。

直接翻译成代码:

	public int maxProfit(int[] prices, int fee) {
		if(prices == null || prices.length == 0){
			return 0;
		}

		int [][]dp = new int[prices.length][2];
		dp[0][0] = 0;
		dp[0][1] = -prices[0] - fee;
		
		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], dp[i-1][0] - prices[i] -fee);
		}
		return dp[prices.length-1][0];
	}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值