1. 程式人生 > >dp基礎之博弈型和區間型結合輪流取數字

dp基礎之博弈型和區間型結合輪流取數字

問題:給定一個序列A[0],..,A[N-1],兩個人輪流從序列兩頭取數字(每次只取一個),雙方都用最優策略,使自己的取到的數字和儘量比對手大。
問先手是否必勝?當和一樣大時規定先手勝。(True/False)
例:
A = [1,5,233,7]
輸出:True(,先手取1,無論後手取哪個,先手都能取到233,即先手必勝)

問題分析:
這是一道博弈型dp,其實質上是區間型dp,目標是讓自己拿到的數字和不必對手小
設己方數字和是A,對方是B,目標是A>=B,等價於A-B>=0
也就是說,如果兩人都存著自己與對手的數字和之差,Sa = A-B,Sb = B-A,先手目標是讓Sa最大化,後手目標讓Sb最大化
當一方X面對剩下的數字時,他的目標就是最大化Sx = X-Y,當X取走一個數字m後,對手Y此時也變成先手,Sy = Y-X
對於X來說
        Sx = -Sy+m = m-Sy
        m是當前要取的數字,Sy是對手的數字和

現在X有兩種選擇,取序列A的頭或者尾,因為是最優策略,X選取的m = max{A的頭,A的尾}、

case1:如果X取的是A[0],則Y面對的是A[1],...,A[N-1],則Y的目標是最大化Sy,則X想要最大化Sx = -Sy+A[0] = A[0]-Sy
case2:如果X取走的是A[N-1],則Y面對的是A[0],...,A[N-2],則Y的目標是最大化Sy',則X想要最大化Sx' = -Sy'+A[N-1] = A[N-1]-Sy'

當Y面對X取走一個數字後,此時Y是先手,他的目標同樣是最大化Sy,但此時Y面對的序列是A[1],...,A[N-1]或者是
A[0],...,A[N-2]

子問題:設f[i][j]表示先手面對序列A[i][j]時能得到的與對手的數字差。
    f[i][j] = max{A[i]-f[i+1][j] , A[j]-f[i][j-1]}
            max{case1取頭  ; case2取尾}
只有一個數字A[i]時,己方得到數字A[i],對方沒有數字了,得0,數字差為A[i]
   f[i][i] = A[i] (i = 0,...,N-1)

計算順序:
序列長度1:f[0][0],f[1][1],......,f[N-1][N-1]
序列長度2:f[0][1],f[1][2],...,f[N-2][N-1]
.
.
.
序列長度N-1:f[0][N-2],f[1][N-1]
序列長度N:f[0][N-1]

答案:f[0][N-1]>=0,則返回True否則返回False

時間和空間複雜度都是O(N^2),空間複雜度可以優化到O(N)

程式碼及註釋如下:

 

def take_coins_inturn(A):
    n = len(A)
    if n == 0:
        return True
    f = [[0 for i in range(n)] for j in range(n)]
    #初始化f[i][i]  = A[i]
    #長度1
    for i in range(n):
        f[i][i] = A[i]
        
    #長度2,..n
    for l in range(2,n+1):
        for i in range(0,n-l+1):
            j = i+l-1
            #f[i][j] = max{A[i]-f[i+1][j] , A[j]-f[i][j-1]}
            f[i][j]= max(A[i]-f[i+1][j] , A[j]-f[i][j-1])
        
    #只要f[0][n-1]大於等於0,說明先手必勝,否則先手必輸
    return True if f[0][n-1] >= 0 else False
A = [1,5,233,7]
print(take_coins_inturn(A))
#結果:True

 

優化空間解釋:
本來要計算矩陣f是n*n的,計算順序是從f[i][i]組成的對角線開始,向右上計算過去,和dp基礎之最長迴文子串裡的優化空間解釋圖一樣

每次計算f[i][j]時,和本行的f[i][j-1]、下一行的f[i+1][j]有關(只和f[i][j]左邊一個數和下邊一個數有關),且算完f[i][j]時,

f[i][j-1](f[i][j]左邊的那個數)不會再用到,因此,f[i][j]直接覆蓋f[i][j-1](左邊那個數)。壓縮到一行就是新計算出來的f[i]覆蓋到原來的f[i]

程式碼及註釋如下:

def take_coins_inturn(A):
    n = len(A)
    if n == 0:
        return True
    f = [0 for i in range(n)] 
    #初始化f[i][i]  = A[i]
    #長度1
    for i in range(n):
        f[i] = A[i]
        
    #長度2,..n
    for l in range(2,n+1):
        for i in range(0,n-l+1):
            j = i+l-1
            #f[i][j] = max{A[i]-f[i+1][j] , A[j]-f[i][j-1]}
            f[i]= max(A[i]-f[i+1] , A[j]-f[i])
        
    #只要f[0][n-1]大於等於0,說明先手必勝,否則先手必輸
    return True if f[0] >= 0 else False
A = [1,5,233,7]
print(take_coins_inturn(A))
#結果:True