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