leetcode Go语言题解之买卖股票系列

栏目: Go · 发布时间: 5年前

内容简介:本文题解 leetcode 如下:其实上述题目背景基本是一致的:对于不同的限制条件,将导致不同的解题策略

本文题解 leetcode 如下:

其实上述题目背景基本是一致的: 给定一个非负整数数组,这个数组表示一段时间内某个股票的价格变动情况,我们需要求解交易可获得的最大收益,注意不能同时参与多笔交易(必须在再次购买股票前出售掉之前的股票) ,但上述题目对交易的限制条件是不同的:

  • 121 : 至多进行一次交易
  • 122 : 对交易次数不做限制
  • 123 : 至多进行两次交易
  • 188 : 至多进行 k 次交易
  • 309 : 对交易次数不做限制,但在卖股票当天的后一天无法买入股票(技能冷却一天)
  • 714 : 对交易次数不做限制,但每次卖股票的当天将收取值为 fee 的交易手续费

对于不同的限制条件,将导致不同的解题策略

121 : 至多进行一次交易

题目链接 : leetcode.com/problems/be…

Example 1:
Input: [7,1,5,3,6,4]
Output: 5
Explanation: Buy on day 2 (price = 1) and sell on day 5 (price = 6), profit = 6-1 = 5.
             Not 7-1 = 6, as selling price needs to be larger than buying price.

Example 2:
Input: [7,6,4,3,1]
Output: 0
Explanation: In this case, no transaction is done, i.e. max profit = 0.
复制代码

至多进行一次交易的情况是最直观的,要想获得最大利润, 显然就是要在数组中要找两个点,前者处于低谷,后者处于高峰,且两者的差值最大(低价买高价卖)

如下图所示,以折线图的角度看,从第 1 天买入股票,到第 6 天卖股票,得到的收益是最大的

leetcode Go语言题解之买卖股票系列

当然如果是股票狂跌的情况,自然是找不到这样的两个点的

leetcode Go语言题解之买卖股票系列

假设 i 为交易天数的迭代变量,此时我们可以定义两个变量:

  • minPrice : 从第 0 天到第 i 天股票出现的最低价格
  • result : 从第 0 天到第 i 天卖掉价格为 minPrice 的股票所产生的最大利润

根据上述定义,在未进行交易的初始情况下,可设 minPrice 为无穷大,result 为 0

const (
	INT_MX = 1 << 31
)

// 低价买,高价卖
func maxProfit(prices []int) int {
	result, minPrice := 0, INT_MX
	for _, price := range prices {
		minPrice = minV(minPrice, price)
		result = maxV(result, price-minPrice)
	}
	return result
}


func minV(a, b int) int {
	if a < b {
		return a
	}
	return b
}

func maxV(a, b int) int {
	if a > b {
		return a
	}
	return b
}
复制代码

122 : 对交易次数不做限制

题目链接 : leetcode.com/problems/be…

Example 1:
Input: [7,1,5,3,6,4]
Output: 7
Explanation: Buy on day 2 (price = 1) and sell on day 3 (price = 5), profit = 5-1 = 4.
             Then buy on day 4 (price = 3) and sell on day 5 (price = 6), profit = 6-3 = 3.

Example 2:
Input: [1,2,3,4,5]
Output: 4
Explanation: Buy on day 1 (price = 1) and sell on day 5 (price = 5), profit = 5-1 = 4.
             Note that you cannot buy on day 1, buy on day 2 and sell them later, as you are
             engaging multiple transactions at the same time. You must sell before buying again.

Example 3:
Input: [7,6,4,3,1]
Output: 0
Explanation: In this case, no transaction is done, i.e. max profit = 0.
复制代码

本题对交易次数不做限制,如下图所示,如果仅允许交易一次,那么我们选的交易点是在 a 点买入,在 d 点卖出,但现在我们并不限制任何购买次数,因此交易策略就很简单了:每逢低谷(a 和 c 点)买股票,每逢高峰(b 和 d 点)卖股票

leetcode Go语言题解之买卖股票系列

从图中可以轻易看到:

b-a+d-c = b+(d-a)-c > d-a
复制代码

说白了, 如果一个很大的上坡路可以分解为若干个上坡路和下坡路,那么这些上坡路的海拔差之和肯定大于等于总体上坡路的海拔差,因为有了低谷对海拔差之和的贡献

// 低价买入,高价卖出
// 找出所有的 peak 和 valley 即可
func maxProfit(prices []int) int {
	n := len(prices)
	if n <= 1 {
		return 0
	}
	index, peak, valley, result := 0, prices[0], prices[0], 0
	for index < n-1 {
		// 找到第一个 prices[index] 小于 prices[index+1]
		for index < n-1 && prices[index] >= prices[index+1] {
			index++
		}
		valley = prices[index]
		// 找到第一个 prices[index] 大于 prices[index+1]
		for index < n-1 && prices[index] <= prices[index+1] {
			index++
		}
		peak = prices[index]
		result += (peak - valley)
	}
	return result
}
复制代码

其实此题没必要如此大费周章,因为不限制交易次数, 所以从直觉上说,只要有得赚(第 i 天的股价大于第 i-1 天的股价),就直接买第 i-1 天的股票,卖第 i 天的股票

func maxProfit(prices []int) int {
	n := len(prices)
	result := 0
	for i := 1; i < n; i++ {
		if prices[i] > prices[i-1] {
			result += prices[i] - prices[i-1]
		}
	}
	return result
}
复制代码

上述解法相对更直观

123 : 至多进行两次交易

题目链接 : leetcode.com/problems/be…

Example 1:
Input: [3,3,5,0,0,3,1,4]
Output: 6
Explanation: Buy on day 4 (price = 0) and sell on day 6 (price = 3), profit = 3-0 = 3.
             Then buy on day 7 (price = 1) and sell on day 8 (price = 4), profit = 4-1 = 3.

Example 2:
Input: [1,2,3,4,5]
Output: 4
Explanation: Buy on day 1 (price = 1) and sell on day 5 (price = 5), profit = 5-1 = 4.
             Note that you cannot buy on day 1, buy on day 2 and sell them later, as you are
             engaging multiple transactions at the same time. You must sell before buying again.

Example 3:
Input: [7,6,4,3,1]
Output: 0
Explanation: In this case, no transaction is done, i.e. max profit = 0.
复制代码

此题实际上相当于 121 的增强版

在 121 中,限制条件是至多交易 1 次,所以要找最低谷和最高峰做差,而本题其实是类似的,只是至多交易 2 次

回顾 121 的状态方程,我们定义了两个状态变量:

  • minPrice : 从第 0 天到第 i 天股票出现的最低价格
  • result : 从第 0 天到第 i 天卖掉价格为 minPrice 的股票所产生的最大利润
minPrice = minV(minPrice, price)
result = maxV(result, price-minPrice)
复制代码

123 这道题也可以用 121 的方式来做,只不过此时需要定义 4 个变量:

  • firstHold : 第一次持有股票的最大收益
  • firstCash : 第一次卖股票的最大收益
  • secondHold : 第二次持有股票的最大收益
  • seconCash : 第二次卖股票的最大收益

状态转移方程如下:

// 第一次持有股票得到的最大收益
// 如果 prices[i] 刚好是最低谷那 firstHold 就一直不变了
// 跟 121 的 minPrice 其实是一个概念,只是这里用负值表示
firstHold = maxV(firstHold, 0-prices[i])
// 第一次卖掉股票得到的最大收益(可以在同一天买卖)
firstCash = maxV(firstCash, firstHold+prices[i])
// 第二次购买股票,包含第一次卖掉股票的最大收益,这里的 firstCash 很关键
secondHold = maxV(secondHold, firstCash-prices[i])
// 第二次卖掉股票得到的最大收益
secondCash = maxV(secondCash, secondHold+prices[i])
复制代码

以 example 1 为例 [3,3,5,0,0,3,1,4]

leetcode Go语言题解之买卖股票系列

其实可以看到,第二次交易是依赖于第一次交易的,而第一次交易的策略刚好与 121 至多交易 1 次的策略一致

188 : 至多进行 k 次交易

题目链接 : leetcode.com/problems/be…

Example 1:
Input: [2,4,1], k = 2
Output: 2
Explanation: Buy on day 1 (price = 2) and sell on day 2 (price = 4), profit = 4-2 = 2.

Example 2:
Input: [3,2,6,5,0,3], k = 2
Output: 7
Explanation: Buy on day 2 (price = 2) and sell on day 3 (price = 6), profit = 6-2 = 4.
             Then buy on day 5 (price = 0) and sell on day 6 (price = 3), profit = 3-0 = 3.
复制代码

这题就没办法单纯画图来描述了,但如果懂得 123 题的逻辑,那么这题也就简单了,类比就完事了

在 123 题里我们的状态转移方程如下:

firstHold = maxV(firstHold, 0-prices[i])
firstCash = maxV(firstCash, firstHold+prices[i])
secondHold = maxV(secondHold, firstCash-prices[i])
secondCash = maxV(secondCash, secondHold+prices[i])
复制代码

而此题其实同理,比如如果 k = 3,有:

thirdHold = maxV(thirdHold, secondCash-prices[i])
thirdCash = maxV(thirdCash, thirdHold+prices[i])
复制代码

所以实际上我们可以定义 hold 数组和 cash 数组来表示

hold[k] = maxV(hold[k], cash[k-1]-price)
cash[k] = maxV(cash[k], hold[k]+price)
复制代码

另外如果 k 的值大于或等于给定数组的一半,也就是 k >= len(array)/2 ,那么此问题就转换为 122,也就是交易任意次数了

具体代码如下所示:

const (
	INT_MX = 1 << 31
)

func maxV(a, b int) int {
	if a > b {
		return a
	}
	return b
}

func maxProfit(k int, prices []int) int {
	n, result := len(prices), 0
	if k >= n/2 {
		for i := 1; i < n; i++ {
			if prices[i] > prices[i-1] {
				result += (prices[i] - prices[i-1])
			}
		}
	} else {
		hold, cash := make([]int, k+1), make([]int, k+1)
		for i := 0; i <= k; i++ {
			hold[i], cash[i] = -INT_MX, 0
		}
		for _, price := range prices {
			for i := 1; i <= k; i++ {
				hold[i] = maxV(hold[i], cash[i-1]-price)
				cash[i] = maxV(cash[i], hold[i]+price)
			}
		}
		result = cash[k]
	}
	return result
}

复制代码

309 : 对交易次数不做限制,但在卖股票当天的后一天无法买入股票(技能冷却一天)

题目链接 : leetcode.com/problems/be…

Example:
Input: [1,2,3,0,2]
Output: 3 
Explanation: transactions = [buy, sell, cooldown, buy, sell]
复制代码

Example 的交易策略如下图所示:

leetcode Go语言题解之买卖股票系列

在此题中我们将定义 6 个状态变量(实际上是 3 个,另外 3 个的区别只是天数不同):

  • a0 : 第 i 天没有持有股票的最佳收益
  • a1 : 第 i 天持有股票后的最佳收益
  • a2 : 第 i 天卖出股票后的最佳收益
  • t0 : 第 i-1 天没有持有股票的最佳收益
  • t1 : 第 i-1 天持有股票后的最佳收益
  • t2 : 第 i-1 天卖出股票后的最佳收益

上述这么解释可能有点抽象,以状态图解释:

leetcode Go语言题解之买卖股票系列

从上述状态图,我们可以写出状态转移方程:

a0 = maxV(t0, t2)
a1 = maxV(t1, t0-prices[i])
a2 = t1 + prices[i]
复制代码

第 i 天的最佳收益由第 i-1 天的最佳收益决定,所以算是比较典型的动态规划问题(重叠子问题、最优子结构)

可以看到最后迭代的结果就是从 a0 和 a2 两者中选最大值, 这里的 cooldown (技能冷却)体现在从 a2 到 a0 的状态转变中

初始化的情况,即 i=0 的时候,有:

// 第 0 天没有买股票
a0 = 0
// 第 0 天买股票
a1 = -prices[0]
// 第 0 天卖不了股票
a2 = 0
// t0、t1、t2 未定义
复制代码

当 i > 0 时:

  • 分别初始化 t0、t1、t2 为 a0、a1、a2,然后再利用 t0、t1、t2 计算新的 a0、a1、a2
  • a0 : 第 i 天没有持有股票的最佳收益,那有两种情况,取其最大值
    1. 继续不买股票,所以为第 i-1 天没有持有股票的最佳收益 -> t0
    2. 处于冷却期,因为第 i-1 天卖掉股票后的最佳收益,使得第 i 天进行技能冷却 -> t2
  • a1 : 第 i 天持有股票后的最佳收益,同样也有两种情况,取其最大值
    1. 欢乐持股,所以为第 i-1 天持有股票后的最佳收益 -> t1
    2. 买入股票(从未持股到持股) -> t0-prices[i]
  • a2 : 第 i 天卖出股票后的最佳收益,根据定义只能有一种情况 -> t1+prices[i]
func maxV(a, b int) int {
	if a > b {
		return a
	}
	return b
}

// a0 : 代表没有买入股票的状态
// a1 : 代表买入后等待卖出的状态
// a2 : 代表从 a1 卖出股票的状态
func maxProfit(prices []int) int {
    if len(prices) <= 1 {
        return 0
    }
	a0, a1, a2 := 0, -prices[0], 0
	var t0, t1, t2 int
    for i := 1; i < len(prices); i++ {
		t0, t1, t2 = a0, a1, a2
		a0 = maxV(t0, t2)
		a1 = maxV(t1, t0-prices[i])
		a2 = t1 + prices[i]
	}
	return maxV(a0, a2)
}

复制代码

714 : 对交易次数不做限制,但在每次卖股票的当天将收取值为 fee 的交易手续费

题目链接 : leetcode.com/problems/be…

Example 1:
Input: prices = [1, 3, 2, 8, 4, 9], fee = 2
Output: 8
Explanation: The maximum profit can be achieved by:
Buying at prices[0] = 1
Selling at prices[3] = 8
Buying at prices[4] = 4
Selling at prices[5] = 9
The total profit is ((8 - 1) - 2) + ((9 - 4) - 2) = 8.
复制代码

此题与上一题类似,也属于动态规划问题,但定义的状态相对简单,因此也更好理解些:

  • currentCash : 第 i 天未持有股票时所获得的最大收益
  • currentHold : 第 i 天持有股票时所获得的最大收益

状态图如下:

leetcode Go语言题解之买卖股票系列

相应状态转移方程和代码如下:

// 买卖股票有两种状态,第 i 天结束后:
// 1. 未持有股票(观望股市,或者把先前持有股票卖了)
// 2. 持有股票(欢乐持股,或者买第 i 天的股票)
func maxProfit(prices []int, fee int) int {
    // 初始化为第 0 天,有两种情况:不买第 0 天的股票;买第 0 天的股票
    currentCash, currentHold := 0, -prices[0]
    for i := 1; i < len(prices); i++ {
        currentCash = maxV(currentCash, currentHold+prices[i]-fee)
        // 为什么这里可以用已经计算过的第 i 天的 currentCash
        // 去算 currentHold 呢?因为不限制购买次数
        // 完全可以当天卖再当天买,不影响最后的计算结果的
        currentHold = maxV(currentHold, currentCash-prices[i])
    }
    return currentCash
}复制代码

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

硅谷之火

硅谷之火

保罗·弗赖伯格、迈克尔·斯韦因 / 张华伟 编译 / 中国华侨出版社 / 2014-11-1 / CNY 39.80

《硅谷之火:人与计算机的未来》以生动的故事,介绍了计算机爱好者以怎样的创新精神和不懈的努力,将计算机技术的力量包装在一个小巧玲珑的机壳里,实现了个人拥有计算机的梦想。同时以独特的视角讲述了苹果、微软、太阳微系统、网景、莲花以及甲骨文等公司的创业者们在实现个人计算机梦想的过程中创业的艰辛、守业的艰难、失败的痛苦,在激烈竞争的环境中奋斗的精神以及在技术上不断前进的历程。一起来看看 《硅谷之火》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具