动态规划算法

150 阅读8分钟

这是我参与8月更文挑战的第10天,活动详情查看:8月更文挑战

大家好,今天我们来聊一聊股票交易问题。

Tips:炒股投资的朋友可以直接走了,这是程序员的笔试面试题,不是真正的去探讨炒股哦!不过这两天港股涨的不错...

​ 前几天群里的小伙伴参加字节面试,遇到了股票交易这么一道题。今天我们就来分析一下。同时也给即将要参加校招的朋友们提供准备,这是字节腾讯等大厂校招时常考的题目。

问题描述:

给定一个数组prices,它的第i个元素 prices[i]表示一支给定股票第i天的价格。你只能选择某一天买入这只股票,并选择在未来的某一个不同的日子卖出该股票。设计一个算法来计算你所能获取的最大利润。返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0

分析问题:

​ 拿到这个问题,我们就需要先思考用什么样的思想去解决。我们来看这个题目,这个问题是求最优解问题。初看题目,我们最容易想到的方法,就是暴力求解,即遍历数组,求出最大值和最小值,相减就是最大利润。不过这里有一个隐含条件,就是股票的卖出时机必须大于股票的买入时机。

def maxProfit(prices):
    #最大利润
    ans = 0
    #股票卖出时机要大于股票买入时机
    for i in range(len(prices)):
        for j in range(i + 1, len(prices)):
            ans = max(ans, prices[j] - prices[i])
    return ans

prices=[7, 1, 5, 3, 6, 4]
maxProfit(prices)

​ 我们可以看出,这种求解的方法,时间复杂度是O(n^2),那有没有时间复杂度更优的方法呢?

​ 首先,我们来分析一下这个问题符合什么“问题模型”呢?我们从0开始走,走到n-1,一共需要走n-1步,也就对应着n-1个阶段。每个阶段都只能往右走,并且每个阶段都会对应一个状态集合,我们把状态定义为maxprofit[i],其中i表示走到哪一步,maxprofit[i]的值表示从开始走到位置i的最大利润是多少。所以这个问题是“多阶段决策最优”问题,对于这类问题,我们首先要考虑能否用动态规划的思想来解决,也就是看是否符合动态规划的特征:重复子问题、无后效性、最优子结构。

  1. 无后效性:我们要想计算maxprofit[i]这个状态,只需要关心maxprofit[i-1]的状态,并不关心maxprofit[i-1]这个状态是如何生成的。而且,我们只能往后移动,不允许后退。所以,前面阶段的状态确定之后,不会被后面阶段的决策所改变。所以符合"无后效性"。

  2. 重复子问题:如果要达到maxprofit[i]这个状态,我们可以有不同的决策序列。比如假设第五天,我们的最大利润是8,即maxprofit[i]的状态为8,我们可以是第一天买入2,第三天卖出10。也可以第二天买入2,第三天卖出10。

  3. 最优子结构:因为maxprofit[i]可以通过maxprofit[i-1]推导出来,即符合“最优子结构”

    maxprofit[i]=max(maxprofit[i-1],prices[i]-minprice)
    
    

    下面我们看代码如何实现:

def maxProfit(prices):
    n=len(prices)
    if n==0:
        return 0
    maxPRofit=[0]*n
    minprice=prices[0]

    for i in range(1,n):
        minprice = min(minprice, prices[i])
        maxPRofit[i]=max(maxPRofit[i-1],prices[i]-minprice)

    return maxPRofit[-1]

prices=[7, 1, 5, 3, 6, 4]
print(maxProfit(prices))

​ 我们可以看出,这种求解的时间复杂度为O(n)。

扩展:

​ 我们再把问题复杂一点,假如我们可以进行多次交易一支股票。

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

​ 这个问题的解题思路和上题类似,也是“多阶段决策最优”问题。也符合动态规划解题模型。不过这里的“状态”会有所区别,我们来看一下。

​ 考虑到“不能同时参与多笔交易”。所以,每天交易结束后手里只可能存在有股票和没有股票两种状态。所以,我们可以用二个数组来表示,其中dp1[i]表示第i天交易后,手里没有股票的最大利润。dp2[i]表示第i天交易完成后,手里有股票的最大利润。

​ dp1[i]表示第i天手里没有股票的最大利润,那么这个状态要么是由dp1[i-1],即前一天手里没有股票的最大利润,转移过来。要么由dp2[i-1]+prices[i],即前一天手里有股票,今天把这个股票卖出。所以dp1[i]的状态转移方程为:

dp1[i]=max(dp1[i-1],dp2[i-1]+prices[i])

​ dp2[i]表示第i天手里有股票的最大利润,那么这个状态要么是由dp2[i-1],即前一天手里有股票的最大利润,转移过来。要么由dp1[i-1]-prices[i],即前一天手里没有股票,今天把这个股票进行买入。所以dp2[i]的状态转移方程为:

dp2[i]=max(dp2[i-1],dp1[i-1]-prices[i])

​ 所以我们代码实现如下所示:

def maxProfit(prices):
    n=len(prices)
    if n==0:
        return 0
    dp1=[0]*n
    dp2=[0]*n
    #手里没有股票
    dp1[0]=0
    #手里有股票
    dp2[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])

    return dp1[n-1]

prices=[7, 1, 5, 3, 6, 4]
print(maxProfit(prices))

​ 我们再来把题目升级一下,即我们限制股票买卖的次数,比如我们只能交易两次。

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

​ 这个题目和上个题相比,状态又发生了变化,这里因为限制了交易次数,所以我们需要把交易次数的状态也保存起来。所以每天交易结束后,有5种状态。

  1. 未进行过任何操作。
  2. 只进行过一次买入操作。
  3. 进行过一次买入和卖出操作。
  4. 完成一笔的交易前提下,进行了第二次买入操作。
  5. 完成两笔交易。

由于第一个状态的利润为0,所以我们不需要记录。我们把剩下的4个状态分别用buy1,sell1,buy2,sell2来表示。下面我们来看一下如何根据前一天的状态,来通过转移方程生成今天的4个状态。

​ 对于buy1而言,我们可以今天不操作,直接通过昨天的buy′ 1转移过来,或者我们从未进行过任何操作前提下,今天买入股票。

​ buy1 =max(buy′ 1,-prices[i])

​ 对于sell1而言,我们今天可以不做任何操作,直接通过昨天的sell′ 1转移过来,或者我们可以在只进行过一次买入交易的前提下,今天把股票卖出。

​ sell1=max(sell′ 1,buy′ 1+prices[i])

​ 对于buy2而言,我们今天可以不做任何操作,直接通过昨天的buy′ 2转移过来,或者,我们在进行过一笔完成的交易条件下,今天再把该股票买入。

​ buy2=max(buy′ 2,sell′ 1-prices[i])

​ 对于sell2而言,我们今天可以不做任何操作,直接通过昨天的sell′ 2 转移过来,或者,我们在buy2的前提下,今天把股票卖出。即

​ sell2=max(sell′ 2,buy′ 2+prices[i])

Tips:我们在计算sell1时,我们可以直接使用buy1而不是buy′ 1进行转移。buy1比buy′ 1多考虑的是在第i天买入股票的情况,而转移到sell1时,考虑的是在第i天卖出股票的情况,这样在同一天买入并且卖出股票的收益为0,不会对结果产生影响。对于buy2和sell2也是同样的考虑,我们可以直接用第i天求出的值来进行转移。

buy1=max(buy1,-prices[i])
sell1=max(sell1,buy1+prices[i]
buy2=max(buy2,sell1-price[i])
sell2=max(sell2,buy2+prices[i])

我们来看代码实现:

def maxProfit(prices):
    n = len(prices)
    #表示以prices[0]买入股票
    buy1=-prices[0]
    #表示在同一天买入并且卖出,即为0
    sell1=0
    #表示同一天买入卖出后再以prices[0]的价格买入
    buy2=-prices[0]
    #表示同一天买入卖出两次,即为0
    sell2 = 0
    for i in range(1, n):
        buy1 = max(buy1, -prices[i])
        sell1 = max(sell1, buy1 + prices[i])
        buy2 = max(buy2, sell1 - prices[i])
        sell2 = max(sell2, buy2 + prices[i])
    return sell2

prices=[7, 1, 5, 3, 6, 4]
print(maxProfit(prices))

最后

​ 对于像“多阶段决策最优”问题来说,我们首先要考虑是否能用动态规划来解决,看是否满足动态规划的三个特征:重复子问题、最优子结构、无后效性。然后写出状态转移方程,那问题基本就解决了。