leetcode 股票问题汇总

文章目录

  • 题目
    • 121. 买卖股票的最佳时机
      • 暴力
      • 记录极小值
      • O(n)解法
      • DP
    • 122. 买卖股票的最佳时机 II
      • 贪心
      • DP
    • 123. 买卖股票的最佳时机 III
    • 124. 买卖股票的最佳时机 IV
    • 127. 买卖股票的最佳时机含手续费
      • DP
      • 贪心

股票问题有很多很多技巧

题目

  1. 买卖股票的最佳时机
    只能买卖一次

  2. 买卖股票的最佳时机 II
    可以买卖多次,但是买入之前手上不能拥有股票

  3. 买卖股票的最佳时机 III
    最多可以买卖两次

  4. 买卖股票的最佳时机 IV
    给定k,最多可以买卖k次

  5. 最佳买卖股票时机含冷冻期
    卖出股票后第二天无法买入

  6. IPO

  7. 买卖股票的最佳时机含手续费
    可以买卖多次,但是每次都要支付给定的手续费

  8. 股票价格跨度

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

leetcode 股票问题汇总_第1张图片
尽管很慢,但还是通过了,实际上这还是 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[i1],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=ana1,但是这个等式 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=(a2a1)+(a3a2)+...+(anan1)同样成立,即我们通过计算n-1组差值来间接求得 a n − a 1 a_n - a_1 ana1的值
如果考虑这个函数非单调递增,即存在 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 ajai<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[i1]+prices[i]prices[i1])

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[i1],dp[i1]+prices[i]prices[i1]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[i1][0],dp[i1][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[i1][1],dp[i1][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]

上面的描述太乱了,还是看题解吧
leetcode 股票问题汇总_第2张图片
这种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[i1][1],dp[i1][0]prices[i])leetcode 股票问题汇总_第3张图片

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[i1][0],dp[i1][1]+prices[i]fee)
leetcode 股票问题汇总_第4张图片

贪心

贪心可以看作是优化后的DP,真的不太好想到,先空着

你可能感兴趣的:(leetcode)