letcode贪心算法练习+6个股票问题总结

Date : 2019-08-21

 

1.  宝石与石头

给定字符串J 代表石头中宝石的类型,和字符串 S代表你拥有的石头。 S 中每个字符代表了一种你拥有的石头的类型,你想知道你拥有的石头中有多少是宝石。

J 中的字母不重复,J 和 S中的所有字符都是字母。字母区分大小写,因此"a"和"A"是不同类型的石头。

示例 1:

输入: J = "aA", S = "aAAbbbb"
输出: 3


示例 2:

输入: J = "z", S = "ZZ"
输出: 0

思路:只需要找出S中元素在J中出现过的元素的个数。

class Solution:
    def numJewelsInStones(self, J: str, S: str) -> int:
        res = [x for x in S if x in J]  # S中在J中出现的元素;同理可以延伸S中不在J中出现的元素
        return len(res)
        

贪心算法原理

贪心算法就是做出一系列选择来使原问题达到最优解。在每一个决策点,都是做出当前看来的最优选择,比如在活动选择问题里面,我们总是在一个问题的基础上选择结束时间最早的活动,之后再在剩下活动的基础上选出结束时间最早的活动,以此类推,直到没有活动可以进行选择。但是遗憾的是这种算法并不是总能得到最优解,并且是否能得到最优解还取决于对于贪心策略的选择。

一般来说,设计贪心算法涉及到下面几个步骤


  1.确定问题的最优子结构 
  2.基于问题的最优子结构设计一个递归算法 
  3.证明我们做出的贪心选择,只剩下一个子问题 
  4.证明贪心选择总是安全的 
  5.设计一个递归算法实现贪心策略 
  6.将贪心算法转化为迭代算法


比如在活动选择问题里面,我们就是确定了互动最优子结构的性质,我们在子问题SjSj里面选出一个基于上次选择ajaj的最早结束活动amam,使得SjSj的最优解是由amam和SmSm的最优解组成的。

更一般的来说,我们可以讲贪心算法的设计步骤简述为下面几部:


  1.将最优化问题简化为这样的形式:最初一个选择以后,只剩下一个子问题需要求解! 
  2.证明在做出贪心选择以后,原问题总是存在最优解,即贪心选择总是安全的! 
  3.证明在做出贪心选择以后,剩下的子问题满足性质:其最优解与做出选择的组合在一起得到原问题的最优解,即最优子结构


贪心算法的两大性质

贪心算法有两个重要的性质:

  
  贪心选择性质
  最优子结构性质

下面我们来详细讨论这两个性质

贪心选择性质

  第一个关键要素就是贪心选择性质:我们可以做出局部最优选择来构造最优解。也就是说,我们在做出选择时,总是以当前的情况为基础做出最优选择的,而不用考虑子问题的解!

这要是和动态规划最大的不同之处,我们知道

   在动态规划中,在每次做出一个选择的时候总是要将所有选择进行比较以后才能确定到底采用哪一种选择,而这种选择的参考依据是以子问题的解为基础的,所以动态规划总是采用自下而下的方法来,先得到子问题的解,再通过子问题的解构造原问题的解。就算是自上而下的算法也是先求出子问题的解,通过递归调用自下而上的返回每一个子问题的最优解
  在贪心算法中,我们总是在原问题的基础上做出一个选择,然后求解剩下的唯一子问题,贪心算法从来都不依赖子问题的解,不过有可能会依赖上一次做出的选择,所以贪心算法是自上而下的。一步一步的选择将原问题一步步消减得更小当然,我们必须证明每一个步骤做出的贪心选择都可以生成全局最优解!我们再活动选择问题里面是这样的做的,首先假定有一个最优解,然后将做出的选择替换进去得到另外一个最优解!

最优子结构

如果一个问题的最优解包含其子问题的最优解,那么就称这个问题具有最优子结构性质!我们知道最优子结构这个性质是动态规划和贪心算法都必须具备的关键性质。

贪心算法vs动态规划

贪心算法和动态规划都有一些共同的性质,比如最优子结构,有些问题我们可以采用动态规划来解决,也可以采用贪心算法来结局,这两者之间有细微的差别!下面我们通过研究一个问题来区分之间的差别! 
下面有两个经典的算法问题:

   0-1背包问题:我们有一堆物品S={a1,a2,...,an}S={a1,a2,...,an},每一个物品aiai都有一个重量wiwi和一个价值vivi.现在有一个背包,这个背包的容量为WW,现在要将这些物品在不超出背包容量的情况下选择性的放入背包,使得背包里面物品的价值最大,物品不能只选取其中一部分,必须选择整个,或者不选!
   分数背包问题:这个问题和上面的问题比较相似,唯一不同的就是该问题里面的物品可以进行分割,即可以只选取一个物品aiai的一部分
  
虽然上面两个问题比较相似,但是贪心算法可以求解第二个问题而不能求解0-1背包问题,为了求解分数背包问题,我们首先得到每一个物品单位重量的价值vi/wivi/wi,那么我们要设计一个贪心策略来使得装入背包物品的价值最大。我们的第一直觉肯定是要选择单位重量价格最高的喽,让后再选择物品里面第二高的,一次类推直到装满背包为止!
 
  1.  删列造序

给定由 N 个小写字母字符串组成的数组 A,其中每个字符串长度相等。

删除 操作的定义是:选出一组要删掉的列,删去 A 中对应列中的所有字符,形式上,第 n 列为 [A[0][n], A[1][n], ..., A[A.length-1][n]])。

比如,有 A = ["abcdef", "uvwxyz"],

 

要删掉的列为 {0, 2, 3},删除后 A 为["bef", "vyz"], A 的列分别为["b","v"], ["e","y"], ["f","z"]。

 

你需要选出一组要删掉的列 D,对 A 执行删除操作,使 A 中剩余的每一列都是 非降序 排列的,然后请你返回 D.length 的最小可能值。

 

示例 1:

输入:["cba", "daf", "ghi"]
输出:1
解释:
当选择 D = {1},删除后 A 的列为:["c","d","g"] 和 ["a","f","i"],均为非降序排列。
若选择 D = {},那么 A 的列 ["b","a","h"] 就不是非降序排列了。


示例 2:

输入:["a", "b"]
输出:0
解释:D = {}


示例 3:

输入:["zyx", "wvu", "tsr"]
输出:3
解释:D = {0, 1, 2}


 

提示:


    1 <= A.length <= 100
    1 <= A[i].length <= 1000

思路:本题实际上将列表中每个元素的对应位置的元素排成一个新列表,然后计算不是非减序列的个数。

class Solution:
    def minDeletionSize(self, A: List[str]) -> int:
        idx = 0
        for i in range(len(A[0])):
            for j in range(1, len(A)):
                if A[j-1][i] > A[j][i]:
                    idx += 1
                    break
        return idx

或者:

class Solution:
    def minDeletionSize(self, A: List[str]) -> int:
            return sum([list(c) != sorted(c) for c in zip(*A)])

2.  最后一块石头的重量

有一堆石头,每块石头的重量都是正整数。

每一回合,从中选出两块最重的石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:


    如果 x == y,那么两块石头都会被完全粉碎;
    如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。


最后,最多只会剩下一块石头。返回此石头的重量。如果没有石头剩下,就返回 0。

class Solution:
    def lastStoneWeight(self, stones: List[int]) -> int:
        import bisect 
        stones.sort()  # 升序排列石头列表
        while len(stones)>1:
            x = stones.pop() # 删除最后一个元素并取值到x
            y = stones.pop() # 继续删除最后一个元素并取值到y
            if x != y:
                bisect.insort(stones, x-y)  # 如不相等,则进行差值的相应位置放置
        return stones[0] if len(stones)==1 else 0  # 返回元素

3.  股票的时间 

1) 只允许一次交易的最佳买股票时间: 买卖股票的最佳时机 I  (k=1)

【贪婪算法思想?】

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        min_p,max_p = 9999,0
        for i in range(len(prices)):
            min_p = min(min_p, prices[i])    # 最小值的买入值
            max_p = max(max_p, prices[i]-min_p)  # 比较之前的最大收益和今天卖出的收益
        return max_p

2) 可以进行多次交易的最佳买卖股票时间(获取最大收益) 买卖股票的最佳时机 II (k=+inifity)

给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例 1:

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


示例 2:

输入: [1,2,3,4,5]
输出: 4
解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
     注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。
     因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。


示例 3:

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

思路一

:该题同样可以采用DP思想,也可以使用贪婪算法思想:一次遍历股票价格列表,如果今天的价格小于明天的价格,则采取今天买入,明天卖出的手段进行获利。

【无数次的交易无限制,贪婪算法更好一些,动态规划不引入k,与贪婪算法思想有点相似】

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        if len(prices) <= 1:
            return 0
        res = 0
        for i in range(1,len(prices)):
            if prices[i] > prices[i-1]:
                res += prices[i] - prices[i-1]
        return res
        

DP思想:

换一种思路:只要第二天股票上升了,卖了就是赚了。实际上这种想法获得的利润跟上面的一样。基于这种想法,我们就可以写出状态转移方程:

dp[i] = dp[i-1] if prices[i]<=prices[i-1] else dp[i-1]+(prices[i]-prices[i-1])

也就是说:如果今天比昨天价更高,则dp[i] = dp[i-1]+高出的价格;反之,则不变

另一种动态规划思想

DP动态规划,第i天只有两种状态,不持有或持有股票,当天不持有股票的状态可能来自昨天卖出或者昨天也不持有,同理,当天持有股票的状态可能来自昨天买入或者昨天也持有中,取最后一天的不持有股票状态就是问题的解。【文末尾有整体的思路】

lass Solution:
    def maxProfit(self, prices: List[int]) -> int:
        if not prices:
            return 0
        n = len(prices)
        dp = [[0]*2 for _ in range(n)]
        # dp[i][0]表示第i天不持有股票, dp[i][1]表示第i天持有股票
        dp[0][0], dp[0][1] = 0, - prices[0]
        for i in range(1, n):
            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])
        return dp[n-1][0]

3) 买卖股票的最佳时机 III  (k=2)

给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。

注意: 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例 1:

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

示例 2:

输入: [1,2,3,4,5]
输出: 4
解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。   
     注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。   
     因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。


示例 3:

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

【3维DP,动态规划思想】

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        if not prices:
            return 0
        n = len(prices)
        dp = [[[0]*2 for _ in range(3)] for _ in range(n)]
        # dp[i][j][0]表示第i天交易了j次时不持有股票, dp[i][j][1]表示第i天交易了j次时持有股票
        # 定义购买股票时交易次数加1
        for i in range(3):
            dp[0][i][0], dp[0][i][1] = 0, -prices[0]
        
        for i in range(1, n):
            for j in range(3):
                if not j:
                    dp[i][j][0] = dp[i-1][j][0]
                else:
                    dp[i][j][0] = max(dp[i-1][j][0], dp[i-1][j][1] + prices[i])
                dp[i][j][1] = max(dp[i-1][j][1], dp[i-1][j-1][0] - prices[i])
        
        return max(dp[n-1][0][0], dp[n-1][1][0], dp[n-1][2][0])

4) 买卖股票的最佳时机 IV  (k=K),不固定的k值

给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。

注意: 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例 1:

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


示例 2:

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

【3维DP,动态规划思想】

class Solution:
    def maxProfit(self, k: int, prices: List[int]) -> int:
        if not prices or not k:
            return 0
        n = len(prices)
        
        # 对于过大的K,用贪心法解决
        if k > n//2:
            return self.greedy(prices)
        
        dp,res = [[[0]*2 for _ in range(k+1)] for _ in range(n)],[]
        # dp[i][j][0]表示第i天交易了j次时不持有股票, dp[i][j][1]表示第i天交易了j次时持有股票
        # 定义购买股票时交易次数加1
        for i in range(k+1):
            dp[0][i][0], dp[0][i][1] = 0, -prices[0]
        
        for i in range(1, n):
            for j in range(k+1):
                if not j:
                    dp[i][j][0] = dp[i-1][j][0]
                else:
                    dp[i][j][0] = max(dp[i-1][j][0], dp[i-1][j][1] + prices[i])
                    dp[i][j][1] = max(dp[i-1][j][1], dp[i-1][j-1][0] - prices[i])
                
        # 所有交易次数最后一天不持有股票的集合的最大值即为问题的解
        for m in range(k+1):
            res.append(dp[n-1][m][0])
        return max(res)

    # 处理K过大导致超时的问题,用贪心法解决:
    def greedy(self,prices):
        res = 0
        for i in range(1,len(prices)):
            if prices[i] > prices[i-1]:
                res += (prices[i] - prices[i-1])
        return res

5) 买卖股票的最佳时机(含手续费)

给定一个整数数组 prices,其中第 i 个元素代表了第 i 天的股票价格 ;非负整数 fee 代表了交易股票的手续费用。

你可以无限次地完成交易,但是你每次交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。

返回获得利润的最大值。

示例 1:

输入: 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.

注意:


    0 < prices.length <= 50000.
    0 < prices[i] < 50000.
    0 <= fee < 50000.

【贪心算法求解】

class Solution:
    def maxProfit(self, prices: List[int], fee: int) -> int:
        n =  len(prices)
        if n <= 1:
            return 0
        res = 0
        minval = prices[0]  # 记录最小值
        for i in range(1,n):
            if prices[i] < minval :
                minval = prices[i]  # 更新方式1
            elif prices[i] > (minval+fee):
                res += (prices[i]-minval-fee)  # 满足结果累计
                minval = prices[i]-fee  # 更新方式2
        return res
        

【动态规划思想求解】

思想解释:

dp1[i]表示第i天手上有股票,dp2[i]表示第i天手上没有股票,递归方程:

  1. dp1[i] = max(dp1[i-1], dp2[i-1] - prices[i]) (第二项表示在第i天买入股票)
  2. dp2[i] = max(dp2[i-1], dp1[i-1] + prices[i] - fee) (第二项表示在第i天将股票卖出,需扣除手续费)
class Solution:
    def maxProfit(self, prices: List[int], fee: int) -> int:
        n = len(prices)
        if n <2:
            return 0
        dp1 = [0 for _ in range(n)] # dp1[i]表示第i天手上有股票时的最大收益
        dp2 = [0 for _ in range(n)] # dp2[i]表示第i天手上没有股票时的最大收益
        dp1[0] = -prices[0]
        for i in range(1,n):
            dp1[i] = max(dp1[i-1], dp2[i-1]-prices[i])
            dp2[i] = max(dp2[i-1], dp1[i-1]+prices[i]-fee) # 规定在卖股票时,要支付相应的手续费
        return dp2[n-1]        

6) 最佳买卖股票时机含冷冻期 

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

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


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


示例:

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

 

【动态规划思想】

sell[i]表示截至第i天,最后一个操作是卖时的最大收益;
buy[i]表示截至第i天,最后一个操作是买时的最大收益;
cool[i]表示截至第i天,最后一个操作是冷冻期时的最大收益;
递推公式:
sell[i] = max(buy[i-1]+prices[i], sell[i-1]) (第一项表示第i天卖出,前一天i-1买入,第二项表示第i天冷冻)
buy[i] = max(cool[i-1]-prices[i], buy[i-1]) (第一项表示第i天买进,前一天i-1冷冻期,第二项表示第i天冷冻)
cool[i] = max(sell[i-1], buy[i-1], cool[i-1])

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        n = len(prices)
        if n == 0:
            return 0
        sell = [0 for _ in range(n)]
        buy = [0 for _ in range(n)]
        cool = [0 for _ in range(n)]
        buy[0] = -prices[0]
        for i in range(1,n):
            sell[i] = max(buy[i-1]+prices[i],sell[i-1])
            buy[i] = max(cool[i-1]-prices[i],buy[i-1])
            cool[i] = max(sell[i-1],buy[i-1],cool[i-1])
        return sell[-1]

4. 反转字符串里的单词 【小红书笔试第一题】

给定一个字符串,逐个翻转字符串中的每个单词。

 

示例 1:

输入: "the sky is blue"
输出: "blue is sky the"


示例 2:

输入: "  hello world!  "
输出: "world! hello"
解释: 输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括。


示例 3:

输入: "a good   example"
输出: "example good a"
解释: 如果两个单词间有多余的空格,将反转后单词间的空格减少到只含一个。

class Solution:
    def reverseWords(self, s: str) -> str:
        return ' '.join(s.split()[::-1])

 

###  利用动态规划思想整体分析上面的6种股票问题:

第一题是只进行一次交易,相当于 k = 1;第二题是不限交易次数,相当于 k = +infinity(正无穷);第三题是只进行 2 次交易,相当于 k = 2;第四题是进行 K次交易,剩下两道也是不限次数,但是加了交易「冷冻期」和「手续费」的额外条件,

一、穷举框架

这里,我们不用递归思想进行穷举,而是利用「状态」进行穷举。我们具体到每一天,看看总共有几种可能的「状态」,再找出每个「状态」对应的「选择」。我们要穷举所有「状态」,穷举的目的是根据对应的「选择」更新状态。

选择: 比如说这个问题,每天都有三种「选择」:买入、卖出、无操作,我们用 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 次交易,最多获得多少利润?

二、状态转移框架

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

                              

通过这个图可以很清楚地看到,每种状态(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,所以今天我就持有股票了。

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和状态转移方程:

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

第一题,k = 1

直接套状态转移方程,根据 base case,可以做一些化简:

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

第二题,k = +infinity

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

第三题,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])

这里 k 取值范围比较小,所以可以不用 for 循环,直接把 k = 1 和 2 的情况手动列举出来也可以:

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

  第四题,k = any integer
有了上一题 k = 2 的铺垫,这题应该和上一题的第一个解法没啥区别。但是出现了一个超内存的错误,原来是传入的 k 值会非常大,dp 数组太大了。现在想想,交易次数 k 最多有多大呢?
一次交易由买入和卖出构成,至少需要两天。所以说有效的限制 k 应该不超过 n/2【超过用贪婪算法】,如果超过,就没有约束作用了,相当于 k = +infinity。这种情况是之前解决过的。

第五题,k = +infinity with cooldown

每次 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 。

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

 
参考资料:

作者:labuladong
链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iv/solution/yi-ge-tong-yong-fang-fa-tuan-mie-6-dao-gu-piao-w-5/

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值