1. 程式人生 > >dp基礎之序列型Stock

dp基礎之序列型Stock

問題:已知後N天 的股票價格為p[0]...p[N-1](N>=2)
要求:可以最多買一股賣一股,
求最大獲利

分析:
    列舉第j天賣(0<j<=N-1)
    儲存j天之前第i天賣的最小值(i<j)
    返回最大的利潤(j-i)

 

程式碼及註釋如下:

def get_proft(p):
    n = len(p)
    #初始化
    #min_j時刻儲存在第j天賣出之前的最小值
    min_j = p[0]
    #por_j[j]表示在天j(不是第j天)賣出時的最大利潤
    pro_j = [0 for i in range(n)]
    #1天賣出時只有0天買入
    pro_j[1] = p[1]-min_j
    for j in range(2,n):
        #如果天j之前發現比之前的最小值還小,則更新最小值,並記錄天賣出時的最大利潤
        if p[j-1] < min_j:
            min_j = p[j-1]
            pro_j[j] = p[j] - min_j
        #否則,天j之前的最小值不更新,同時記錄j天賣出時的最大利潤
        else:
            pro_j[j] = p[j]-min_j
    return max(pro_j)
    

#p[j]表示天j的利潤
p = [3,1,2,19,0,6,16]
print(get_proft(p))
答案:18

時間複雜度為O(N),空間複雜度為O(1),上述程式碼可以只用一個min_pro即可,不用list

 

問題:若將上述問題改為:可以賣一股任意多次,但是任意時刻手中只有一股,求最大利潤。

程式碼及註釋如下:

def get_nproft(p):
    res = 0
    for i in range(1,len(p)):
        #後一天的價格比前一天高,即上升,就買
        if p[i] -p[i-1] > 0:
            res += p[i] -p[i-1]
    return res
p = [2,1,2,0,1]
print(get_nproft(p))
#答案:2

問題:若將原問題改為:可以最多買賣兩次,每次買賣只能一股,且不能在賣光手中股票前買入,但可以在同一天賣完後買入,
求最大利潤。

p = [4,4,6,1,1,4,2,5]

 

分析:
確定狀態:(假設第N-1天時今天,第N-2天就是昨天)
最後一次賣在第j天,列舉最後一次買在第i天(i<j),但是不知道在第i天之前有沒有買賣過


最優策略一定是前N天,第N-1天結束後處於:

階段1:沒有買賣過
階段3:買賣過1次
階段5:買賣過2次

例如:

如圖示意:


如果要求前N天(第N-1天)結束後,在階段5的最大獲利,設為f[N][5]:
情況1:第N-2天就在階段5,利潤對應為f[N-1][5]
情況2:第N-2天還在階段4,即處於第二次持有股票,在第N-1天賣掉,則利潤對應為  f[N-1][4]  +   (p[N-1]-p[N-2])

如果要求前N天(第N-1天)結束後,在階段4的最大獲利,設為f[N][4]:
情況1:第N-2天就在階段4,利潤對應為f[N-1][4] + (p[N-1]-p[N-2])
情況2:第N-2天還在階段3,即第N-1天剛買入,並沒有獲利,
    則利潤還是對應為第N-2天(第N-2天是處於階段3的)的利潤: f[N-1][3]
情況3:第N-2還在階段2,第N-1天賣完立即買入,即第N-1天結束後處於第二次持有股票,沒有階段3,直接進入階段4
    對應利潤為 f[N-1][2] + (p[N-1]-p[N-2]),

子問題:
要求f[N][1]...f[N][5],需要知道f[N-1][1]...f[N-1][5]
狀態:f[i][j]表示前i天(第i天)結束後,在階段j的最大獲利
在階段1,3,5,手中無股票的狀態:
        f[i][j] = max{ f[i-1][j],        f[i-1][j-1] + p[i-1]-p[i-2] }
                    {昨天沒有持股票;   昨天持有股票且今天賣出股票清倉}

在階段2,4,手中持有股票:
        f[i][j] = max{ f[i-1][j] + p[i-1] - p[i-2] ,       f[i-1][j-1] , f[i-1][j-2] + p[i-1]-p[i-2] }
{昨天持有股票且今天繼續持有並獲利; 昨天沒有股票且今天剛買入獲利明天算; 昨天持有上一次的股票今天賣出並立即買入} 

邊界條件和初始情況:
剛開始(前0天)處於階段1
    f[0][1] = 0    f[0][2] = f[0][3] = f[0][4] f[0][5] = -sys.maxsize(因為本體要求最大值,故初始為負無窮)
    
    階段1,3,5:
    f[i][j] = max{ f[i-1][j],f[i-1][j-1] + p[i-1]-p[i-2] }
    階段2,4
    f[i][j] = max{ f[i-1][j] + p[i-1] - p[i-2] ,f[i-1][j-1],f[i-1][j-2] + p[i-1]-p[i-2] }

如果 j-1<1 或者 i-2<0 則對應項不計入max,因為是無意義的
因為最多買賣2次,故答案是max{f[N][1],f[N][3],f[N][5]},必須處於清倉狀態下最後一天的最大獲利


計算順序:
f[0][1]...f[0][5]
f[1][1]...f[1][5]
.
.
.
f[N][1]...f[N][5]

時間複雜度是O(n),空間複雜度為O(n),但可以優化到O(1),因為f[i][1]...f[i][5]只依賴於f[i-1][1]...f[i-1][5]

 

程式碼及註釋如下:

def get_2_stock(p):
    n = len(p)
    if n == 0:
        return 0
    #建立一個(n+1)*(5+1)二維列表f[i][j],f[i][j]表示前i天(第i天 )結束後,在階段j的最大獲利
    f = [[0 for i in range(5+1)] for j in range(n+1)]
    #初始化,前0天處於階段1的最大獲利為0
    f[0][1] = 0
    #f[0][2]=f[0][3]=f[0][4]=f[0][5] = -sys.maxsize
    for j in range(2,6):
        f[0][j] = -sys.maxsize
    
    for i in range(1,n+1):
        #前i天處於階段1,3,5
        for j in range(1,6,2):
            #f[i][j] = max{ f[i-1][j],        f[i-1][j-1] + p[i-1]-p[i-2] }
            f[i][j] = f[i-1][j]
            if j > 1 and i > 1 and f[i-1][j-1] != -sys.maxsize:
                f[i][j] = max(f[i-1][j],f[i-1][j-1] + p[i-1]-p[i-2])
            
        #前i天處於階段2,4
        for j in range(2,5,2):
            #f[i][j] = max{ f[i-1][j] + p[i-1] - p[i-2] ,f[i-1][j-1],f[i-1][j-2] + p[i-1]-p[i-2] }
            f[i][j] = f[i-1][j-1]
            if i > 1 and f[i-1][j] != -sys.maxsize:
                f[i][j] = max(f[i][j],f[i-1][j] + p[i-1]-p[i-2])
            if j > 2 and i > 1 and f[i-1][j-2] != -sys.maxsize:
                f[i][j] = max(f[i][j],f[i-1][j-2] + p[i-1]-p[i-2])
    return max(f[n][1],f[n][3],f[n][5])
            
                
p = [4,4,6,1,1,4,2,5]
print(get_2_stock(p))

#結果:6
    

 

問題:若將原問題改為:可以最多買賣K次,每次買賣只能一股,且不能在賣光手中股票前買入,但可以在同一天賣完後買入,
求最大利潤。

 

分析:
如果K很大,K>N/2,則題目可以簡化成任意次買賣,因為兩天一次買賣,要是兩天進行3次買賣則這3次買賣中有一次買賣是沒有意義的。

現在來分析K<=N/2的情況:上問題中詳細講了2次的情況,現在只要把2次推廣到K次即可:

如下圖示意:

f[i][j]表示前i天(第i天)結束後,在階段j的最大獲利
a:j=1,3,...,2K+1,即奇數階段手中無股時:
            f[i][j] = max{ f[i-1][j],        f[i-1][j-1] + p[i-1]-p[i-2] }
                    {昨天沒有持股票;   昨天持有股票且今天賣出股票清倉}
    
b:j=2,4,...,2K,即偶數階段手中有股時:
        f[i][j] = max{ f[i-1][j] + p[i-1] - p[i-2] ,       f[i-1][j-1] , f[i-1][j-2] + p[i-1]-p[i-2] }
{昨天持有股票且今天繼續持有並獲利; 昨天沒有股票且今天剛買入獲利明天算; 昨天持有上一次的股票今天賣出並立即買入} 
    
  
邊界條件和初始情況:
剛開始(前0天)處於階段1
    f[0][1] = 0    f[0][2] = f[0][3] = ... = f[0][2K] = f[0][2K+1] = -sys.maxsize(因為本體要求最大值,故初始為負無窮)

如果 j-1<1 或者 i-2<0 則對應項不計入max,因為是無意義的
因為最多買賣K次,故答案是max{f[N][1],f[N][3],...,f[N][2K+1]},必須處於清倉狀態下最後一天的最大獲利

計算順序:
f[0][1]...f[0][2K+1]
f[1][1]...f[1][2K+1]
.
.
.
f[N][1]...f[N][2K+1]

時間複雜度是O(nk),空間複雜度為O(nk),但可以優化到O(k),因為f[i][1]...f[i][2K+1]只依賴於f[i-1][1]...f[i-1][2K+1]

程式碼及註釋如下:

def get_K_stock(p,K):
    n = len(p)
    if n == 0:
        return 0
    #如果K>n/2,直接轉化成買賣任意次
    if K>n/2:
        res = 0
        for i in range(1,n):
            #後一天的價格比前一天高,即上升,就買
            if p[i] -p[i-1] > 0:
                res += p[i] -p[i-1]
        return res
    
    #建立一個(n+1)*(2*K+1+1)二維列表f[i][j],f[i][j]表示前i天(第i天 )結束後,在階段j的最大獲利
    f = [[0 for i in range((2*K+1)+1)] for j in range(n+1)]
    #初始化,前0天處於階段1的最大獲利為0
    f[0][1] = 0
    #f[0][2]=f[0][3]=...=f[0][2*K]=f[0][2*K+1] = -sys.maxsize
    for j in range(2,(2*K+1)+1):
        f[0][j] = -sys.maxsize
    
    for i in range(1,n+1):
        #前i天處於階段1,3,5
        for j in range(1,(2*K+1)+1,2):
            #f[i][j] = max{ f[i-1][j],        f[i-1][j-1] + p[i-1]-p[i-2] }
            f[i][j] = f[i-1][j]
            if j > 1 and i > 1 and f[i-1][j-1] != -sys.maxsize:
                f[i][j] = max(f[i-1][j],f[i-1][j-1] + p[i-1]-p[i-2])
            
        #前i天處於階段2,4
        for j in range(2,2*K+1,2):
            #f[i][j] = max{ f[i-1][j] + p[i-1] - p[i-2] ,f[i-1][j-1],f[i-1][j-2] + p[i-1]-p[i-2] }
            f[i][j] = f[i-1][j-1]
            if i > 1 and f[i-1][j] != -sys.maxsize:
                f[i][j] = max(f[i][j],f[i-1][j] + p[i-1]-p[i-2])
            if j > 2 and i > 1 and f[i-1][j-2] != -sys.maxsize:
                f[i][j] = max(f[i][j],f[i-1][j-2] + p[i-1]-p[i-2])
    
    #返回f[n][1],f[n][3],...,f[n][2*K+1]的最大值
    return max(f[n][1::2])
            
                
p = [4,4,6,1,1,4,2,5]
K = 2
print(get_K_stock(p,K))

#結果:6