文章目录
股票问题有很多很多技巧
题目
-
买卖股票的最佳时机
只能买卖一次 -
买卖股票的最佳时机 II
可以买卖多次,但是买入之前手上不能拥有股票 -
买卖股票的最佳时机 III
最多可以买卖两次 -
买卖股票的最佳时机 IV
给定k,最多可以买卖k次 -
最佳买卖股票时机含冷冻期
卖出股票后第二天无法买入 -
IPO
-
买卖股票的最佳时机含手续费
可以买卖多次,但是每次都要支付给定的手续费 -
股票价格跨度
121. 买卖股票的最佳时机
暴力
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n = (int)prices.size(), ans = 0;
for (int i = 0; i < n; ++i){
for (int j = i + 1; j < n; ++j) {
ans = max(ans, prices[j] - prices[i]);
}
}
return ans;
}
};
O(n^2)
记录极小值
考虑这个数组,我们一定能从中找出极小值,(对于边界元素如果它小于相邻元素,那么称它也是极小值),最终的答案一定是在极小值和它之后的元素产生的
如果整个数组只有一个极小值,那么整个数组单减或单增,此时答案很容易求出,如果单增,那么答案就是首尾元素相减,如果单减,那么答案就是0
如果整个数组有多个极小值,那么整个数组一共有多个大于0的差值对,为了使差值尽可能的大,每当我们遇到一个新数字的时候,尝试更新之前记录的极小值的差值,我们记录最大的差值对,就是这道题的解法
考虑到数组中可能会有重复值的情况,例如[3,1,1,2]
,为了减少判断,我们需要去掉相邻的重复数字,使得数组变为[3,1,2]
最后还要处理一下边界值
整个代码有点复杂
class Solution:
def maxProfit(self, prices: List[int]) -> int:
if not prices or len(prices) == 1:
return 0
for i in range(len(prices)-1,0,-1):
if prices[i] == prices[i-1]:
del prices[i]
# 去重后可能小于1
if not prices or len(prices) == 1:
return 0
ans = 0
md = defaultdict(int)
# 特殊判断边界值
if prices[0] < prices[1]:
md[0] = 0
for i in range(1,len(prices)-1):
# 更新之前极小值的最大值
for k in md.keys():
md[k] = max(md[k],prices[i])
# 找到一个极小值,记录它
if prices[i-1] > prices[i] < prices[i+1]:
md[i] = 0
# 特殊判断最后一个值
for k in md.keys():
md[k] = max(md[k],prices[-1])
# 记录最大差值对
for k,v in md.items():
ans = max(ans,v-prices[k])
return ans
尽管很慢,但还是通过了,实际上这还是
O
(
n
2
)
O(n^2)
O(n2)的解法,但是常数项比暴力法的小很多,因为这个解法的第二层循环需要遍历的个数与字典中key的个数有关,而key是极小值,极小值不会超过
l
e
n
(
n
)
/
/
2
+
1
len(n)//2+1
len(n)//2+1。
另外leetcode上涉及到数组的,有茫茫多这种摆动数组的形状,这个解法的思路就参考了:376. 摆动序列
这一题的解法,里面也是涉及到极值的解法。
O(n)解法
是上一个方法的进阶思想
虽然一个数组可能有多个极小值点,但是可能出现的答案只会是最小极值点产生的,假如最小极值点的下标是i,在i之后存在元素prices[j]
使得prices[j]-prices[i]
最大,即在第i天买入,第j天卖出的收益最大
那么此时ans = prices[j] - prices[i]
,如果此时答案不是prices[j] - prices[i]
,那么意味着存在更大的差值d > prices[j] - prices[i]
考虑到prices[i]已经是最小极值点,右边的数字无法更换,那么如果这样的d存在,一定是有一个更大的prices[t]
存在,使得d = prices[t] -prices[i] > prices[j] - prices[i]
,与假设矛盾(在i之后存在元素prices[j]
使得prices[j]-prices[i]
最大,但是出现了更大的差值对)
所以我们可以证明答案一定由最小的极值参与的等式构成的
那么我们就不需要记录所有极值点了,而且遍历一次我们一定能找到一个极小值点,一次遍历就能解决这个问题
class Solution:
def maxProfit(self, prices: List[int]) -> int:
pre_min = 0xffff
ans = 0
for i in range(len(prices)):
# 更新最小值
pre_min = min(pre_min,prices[i])
# 尝试更新最大差值
ans = max(ans,pre_max - pre_min)
return ans
我们用pre_min来记录最小值,上面分析过了,ans的等式构成中,只有最小值是不变的,所以在往后遍历的过程中,我们不断尝试更新ans,一趟遍历完之后ans就是最终的答案了
时间复杂度 O ( n ) O(n) O(n)
空间复杂度 O ( 1 ) O(1) O(1)
DP
上面的O(n)解法,其实就是优化过后的DP,对于最大收益,我们只需要不断更新最大值就好,用DP的话,就是开一个n的数组来记录大小
dp[i]表示,第i天为止,最大的收益,dp[-1]就是答案
在第i天,有两种状态,一种是什么也不做,另一种是在这一天卖出
什么也不做的时候,dp[i] = dp[i-1],继承前一天的状态
如果在这一天卖出,那么收益p = b - a
,b已知,是prices[i]
,但是a仍未知道,我们想要获得最大的收益,自然是第i天之间最小的股票价格,那么我们还是要用pre_min
来记录最小值
所以
d
p
[
i
]
=
m
a
x
(
d
p
[
i
−
1
]
,
p
r
i
c
e
s
[
i
]
−
p
r
e
_
m
i
n
)
dp[i] = max(dp[i-1],prices[i]-pre\_min)
dp[i]=max(dp[i−1],prices[i]−pre_min)
写出代码
class Solution:
def maxProfit(self, prices: List[int]) -> int:
if not prices or len(prices) == 1:
return 0
dp = [0]*len(prices)
pre_min = prices[0]
for i in range(1,len(prices)):
pre_min = min(pre_min,prices[i])
dp[i] = max(dp[i-1],prices[i]-pre_min)
return dp[-1]
回过头来看,O(n)遍历方法是优化后的dp,因为它压缩了存储空间
时间复杂度 O ( n ) O(n) O(n)
空间复杂度 O ( n ) O(n) O(n)
122. 买卖股票的最佳时机 II
现在可以多次交易,能够获取最大利益就行
贪心
考虑到我们可以交易多次,并且交易的时候没有冷却时间,那么我们可以做如下贪心
对于prices[i],如果大于prices[i-1],那么我们就累计prices[i] - prices[i-1]
,因为这相当于在第i-1天买入,在第i天卖出
如此遍历一遍数组,那么我们就能得到最终的答案
ans = 0
for i in range(1,len(prices)):
if prices[i] > prices[i-1]:
ans += prices[i] - prices[i-1]
下面尝试证明一下这个贪心是正确的
假如这个数组是单调递增的,即
a
1
<
a
2
<
.
.
.
<
a
n
a_1 < a_2 < ... < an
a1<a2<...<an,那么我们此时的最大差值显然是
a
n
s
=
a
n
−
a
1
ans = a_n - a_1
ans=an−a1,但是这个等式
a
n
s
=
(
a
2
−
a
1
)
+
(
a
3
−
a
2
)
+
.
.
.
+
(
a
n
−
a
n
−
1
)
ans = (a_2 - a_1) + (a_3 - a_2) + ... + (a_n - a_{n-1})
ans=(a2−a1)+(a3−a2)+...+(an−an−1)同样成立,即我们通过计算n-1组差值来间接求得
a
n
−
a
1
a_n - a_1
an−a1的值
如果考虑这个函数非单调递增,即存在
i
<
j
i < j
i<j ,有
a
i
>
a
j
a_i > a_j
ai>aj, 由于
a
j
−
a
i
<
0
a_j-a_i<0
aj−ai<0,我们不会统计它,所以ans仍然可以正确的求出
class Solution:
def maxProfit(self, prices: List[int]) -> int:
if not prices or len(prices) == 1:
return 0
ans = 0
for i in range(1,len(prices)):
if prices[i] > prices[i-1]:
ans += prices[i] - prices[i-1]
return ans
时间复杂度 O ( n ) O(n) O(n)
空间复杂度 O ( 1 ) O(1) O(1)
DP
由于这个问题改动并不大,我们可以在第一题dp的基础上改进
注意我们现在可以买卖多次,所以dp不需要记录之前的最小值了,而是和贪心的思想一样,直接对prices[i]和prices[i-1]作差
dp[i]表示第i天时,最大收益
则
d
p
[
i
]
=
m
a
x
(
d
p
[
i
]
,
d
p
[
i
−
1
]
+
p
r
i
c
e
s
[
i
]
−
p
r
i
c
e
s
[
i
−
1
]
)
dp[i] = max(dp[i],dp[i-1] + prices[i] - prices[i-1])
dp[i]=max(dp[i],dp[i−1]+prices[i]−prices[i−1])
class Solution:
def maxProfit(self, prices: List[int]) -> int:
if not prices or len(prices) == 1:
return 0
ans = 0
dp = [0] * len(prices)
for i in range(1,len(prices)):
dp[i] = max(dp[i-1] ,dp[i-1] + prices[i] - prices[i-1])
return dp[-1]
123. 买卖股票的最佳时机 III
比较难,先跳过
124. 买卖股票的最佳时机 IV
比较难,先跳过
127. 买卖股票的最佳时机含手续费
DP
这道题相当于在122. 买卖股票的最佳时机 II
的基础上加上了一个手续费的惩罚
回顾一下122的DP方程
d
p
[
i
]
=
m
a
x
(
d
p
[
i
−
1
]
,
d
p
[
i
−
1
]
+
p
r
i
c
e
s
[
i
]
−
p
r
i
c
e
s
[
i
−
1
]
−
f
e
e
)
dp[i] = max(dp[i-1], dp[i-1] + prices[i] - prices[i-1] - fee)
dp[i]=max(dp[i−1],dp[i−1]+prices[i]−prices[i−1]−fee)
现在来考虑当前的题目
假如我们在第i天卖出了股票,则这一天的收益相当于
prices[i] - 购入价格 - fee
但是注意,区别于之前的题目,由于手续费的惩罚,我们不能盲目的买卖股票
假设有[2,4,6], fee = 1
,如果我们频繁的买卖,那么最终收益是4 - 2 - 1 + 6 - 4 -1 = 2
,如果我们只在第1天买入,第3天卖出,那么最终收益是3
既然这样,那么购入价格就不能像之前那样简单的记为prices[i-1]
了
那么现在问题就是如何记录购入价格呢?回想之前的两道题目,一个是最小值,一个是prices[i-1]
,放在这里都不适用,原因是手续费的存在,使得买卖的状态变得更复杂了,现在我们来分析一下买卖的更新
假如我们在第i天买入了股票,则这一天的收益相当于
-price[i]
假如我们在第i天卖出了股票,则这一天的收益相当于
prices[i] - 购入价格 - fee
,不考虑购入价格,则变为prices[i] - fee
dp[i]的含义仍然是,第i天的时候,带有手续费的多次买卖能达到的最大的收益
由于上面的买卖状态分开,我们需要用二维dp来存放对应的状态
dp[i][0]
来表示第i天时,手里没有股票的收益,dp[i][1]
来表示第i天时,手里有股票的收益,那么有
d
p
[
i
]
[
0
]
=
m
a
x
(
d
p
[
i
−
1
]
[
0
]
,
d
p
[
i
−
1
]
[
1
]
+
p
r
i
c
e
s
[
i
]
−
f
e
e
)
dp[i][0] = max(dp[i-1][0] , dp[i-1][1] + prices[i] - fee)
dp[i][0]=max(dp[i−1][0],dp[i−1][1]+prices[i]−fee)
d
p
[
i
]
[
1
]
=
m
a
x
(
d
p
[
i
−
1
]
[
1
]
,
d
p
[
i
−
1
]
[
0
]
−
p
r
i
c
e
s
[
i
]
)
dp[i][1] = max(dp[i-1][1] , dp[i-1][0] - prices[i])
dp[i][1]=max(dp[i−1][1],dp[i−1][0]−prices[i])
class Solution:
def maxProfit(self, prices: List[int], fee: int) -> int:
n = len(prices)
dp = [[0, -prices[0]]] + [[0, 0] for _ in range(n - 1)]
for i in range(1, n):
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])
return dp[n - 1][0]
上面的描述太乱了,还是看题解吧
这种2维DP的思想在 376. 摆动序列 也有见到,两个状态互相更新
d
p
[
i
]
[
1
]
=
m
a
x
(
d
p
[
i
−
1
]
[
1
]
,
d
p
[
i
−
1
]
[
0
]
−
p
r
i
c
e
s
[
i
]
)
dp[i][1] = max(dp[i-1][1] , dp[i-1][0] - prices[i])
dp[i][1]=max(dp[i−1][1],dp[i−1][0]−prices[i])
d
p
[
i
]
[
0
]
=
m
a
x
(
d
p
[
i
−
1
]
[
0
]
,
d
p
[
i
−
1
]
[
1
]
+
p
r
i
c
e
s
[
i
]
−
f
e
e
)
dp[i][0] = max(dp[i-1][0] , dp[i-1][1] + prices[i] - fee)
dp[i][0]=max(dp[i−1][0],dp[i−1][1]+prices[i]−fee)
贪心
贪心可以看作是优化后的DP,真的不太好想到,先空着