動態規劃

動態規劃 Dynamic Programminghtml

一種設計的技巧,是解決一類問題的方法
dp遵循固定的思考流程:暴力遞歸 —— 遞歸+記憶化 —— 非遞歸的動態規劃(狀態定義+轉移方程)

 

斐波那契數列python

暴力遞歸,看上去很簡潔正則表達式

def fib(n):
    return n if n <= 1 else fib(n-1) + fib(n-2)

 

畫出遞歸樹分析一下,能夠很容易發現有不少重複計算。重疊子問題算法

遞歸算法的時間複雜度怎麼計算?子問題個數乘以解決一個子問題須要的時間。顯然,斐波那契數列的遞歸解法時間複雜度爲O(2n * 1),暴力遞歸解法基本都會超時。數組

 

如何解決?遞歸 + 記憶化app

仍然使用遞歸,不一樣點在於,若是重疊子問題已經計算過,就不用再算了,至關於對冗餘的遞歸樹進行了剪枝。框架

因爲不存在重疊子問題,時間複雜度爲O(n * 1),降到線性。ide

 1 class Solution:
 2     def Fibonacci(self, n):
 3         # write code here
 4         if n <= 1:
 5             return n
 6         memo = [-1] * (n+1)
 7         memo[0], memo[1]= 0, 1
 8         
 9         def helper(n, memo):
10             if memo[n] >= 0:
11                 return memo[n]
12             memo[n] = helper(n-1, memo) + helper(n-2, memo)
13             return memo[n]
14         
15         return helper(n, memo)
View Code

 

實際上這已經和動態規劃同樣了,只不過這是自頂向下的。而動態規劃是自底向上的。從最小的子問題一步一步向上遞推,O(n)函數

 1 class Solution:
 2     def Fibonacci(self, n):
 3         # write code here
 4         if n <= 1:
 5             return n
 6         dp = [0] * (n+1)
 7         dp[0], dp[1] = 0, 1
 8         for i in range(2, n+1):
 9             dp[i] = dp[i-1] + dp[i-2]
10         return dp[n]
View Code

 

進一步優化空間複雜度,因爲只要最後一個狀態,並且狀態轉移只取決於相鄰的狀態,不須要開一維數組,直接兩個變量滾動更新就好了。優化

1 class Solution:
2     def Fibonacci(self, n):
3         # write code here
4         if n <= 1:
5             return n
6         dp_0, dp_1 = 0, 1
7         for i in range(2, n+1):
8             dp_0, dp_1 = dp_1, dp_0 + dp_1
9         return dp_1
View Code

 

從一道題展開

最長公共子序列 LCS

給定長度爲 m 和 n 的兩個數組 x 和 y,找出最長的公共子序列(可能有多個)

x : ABCBDAB

y : BDCABA

存在3個最長公共子序列,長度爲4,分別爲 BDAB、BCAB、BCBA

 

1. 窮舉,窮舉x中的全部子序列,再檢查y裏面是否是也有同樣的子序列

  假設給定了一個子序列,檢查它是否爲 y 的子序列的複雜度?O(n),按順序把 y 對着給定的子序列日後捋一遍便可

  x 有多少子序列?2m ,每一個元素均可以選或者不選。

  因此窮舉的時間複雜度,O(n2m),指數級

 

2. 先肯定 LCS 的長度,再看看具體有哪些公共子序列達到了這個長度

  只要考察前綴便可。定義 c[i, j] 表示 x[1...i] 和 y[1...j] 的 LCS 長度,c[m, n] 就爲x 和 y 的 LCS 長度。

    base cases: c[*, 0] = 0 且 c[0, *] = 0。此外,當 x[i] == y[j] 時,c[i, j] = c[i-1, j-1] + 1;不然 c[i, j] = max(c[i, j-1], c[i-1, j])。這裏比較好理解,稍微想一下就清楚了,x[i] 和 y[j] 相等的時候,這個值能夠直接算到 LCS 中,因此加1。不然的話,就看看x[i] 和 y[j] 各自算進去哪一種狀況的 LCS 大。

  上面結論的一點證實:x[i] == y[j] 時,令 z[1...k] = LCS(x[1...i], y[1...j]),顯然 k = c[i, j]。z[k] 就是 x[i] 同時也爲 y[j] ,顯然必定有 z[1...k-1] = LCS(x[1...i-1], y[1...j-1]) ,c[i-1, j-1] = k-1,即 c[i, j] = c[i-1, j-1] + 1 ;x[i] != y[j] 時的證實相似。

 

由此引出動態規劃的第一個特徵,最優子結構,指的是問題的一個最優解包含了子問題的最優解。

If z = LCS(x, y),  then any prefix of z is an LCS(a prefix of x ,  a prefix of y).

 

遞歸實現一下 LCS 計算長度

def LCS(x, y, i, j):
    """ignore base cases"""
    if x[i] == y[j]:
        c[i, j] = LCS(x, y, i-1, j-1) + 1
    else:
        c[i, j] = max(LCS(x, y, i-1, j), LCS(x, y, i, j-1))
    
    return c[i, j]

  

考慮一下這個遞歸樹,最壞狀況下每次都要走取 max 的分支,遞歸樹深度爲 m+n,時間複雜度爲 O(2m+n)。能夠發現有不少重複計算。由此引出動態規劃的第二個特徵,重疊子問題

 

LCS 問題的子問題有 m*n 個,每次算好了就存下來,備忘法

def LSC(x, y, i, j):
    if c[i, j] != None:
        return c[i, j]

    if x[i] == y[j]:
        c[i, j] = LCS(x, y, i-1, j-1) + 1
    else:
        c[i, j] = max(LCS(x, y, i-1, y), LCS(x, y, i, j-1))
    
    return c[i, j]

  

這個計算所須要的時間?O(m*n),由於攤銷以後每一個子問題都只須要執行常數次計算獲得結果

空間?O(m*n),建表

 

自底向上地計算表格  ——動態規劃

 1 def lcs_length(x, y):
 2     if not x or not y:
 3         return 0
 4     m, n = len(x), len(y)
 5     dp = [[0]*(n+1) for _ in range(m+1)]
 6     
 7     for i in range(m+1):
 8         for j in range(n+1):
 9             if i == 0 or j == 0:
10                 dp[i][j] = 0
11             elif x[i-1] == y[j-1]:
12                 dp[i][j] = dp[i-1][j-1] + 1
13             else:
14                 dp[i][j] = max(dp[i-1][j], dp[i][j-1])
15     
16     return dp[-1][-1]
View Code

 

優化空間複雜度,比較簡單的作法就是用滾動數組優化到 O(2*min(m, n)) ,由於每次都須要查看 dp 表的上一行和這一行的左邊。

 1 def lcs_length(x, y):
 2     if not x or not y:
 3         return 0
 4     m, n = len(x), len(y)
 5     if n > m:
 6         m, n, x, y = n, m, y, x
 7         
 8     dp = [[0]*(n+1) for _ in range(2)]
 9     
10     pre, now = 0, 1
11     for i in range(1, m+1):
12         pre, now = now, pre
13         for j in range(1, n+1):
14             if x[i-1] == y[j-1]:
15                 dp[now][j] = dp[pre][j-1] + 1
16             else:
17                 dp[now][j] = max(dp[pre][j], dp[now][j-1])
18                 
19     return dp[now][-1]
View Code

 

在獲得 LCS 長度的同時如何獲得子序列?根據 dp 表回溯,走到每一個位置的時候記錄一下從哪裏來的。整道題的答案就搞定了

 1 def lcs_length(x, y):
 2     if not x or not y:
 3         return 0
 4     m, n = len(x), len(y)
 5     dp = [[0]*(n+1) for _ in range(m+1)]
 6     
 7     # 1:左上、2:上、3:左、4:上或左
 8     states = [[0]*(n+1) for _ in range(m+1)]
 9     
10     for i in range(1, m+1):
11         for j in range(1, n+1):
12             if x[i-1] == y[j-1]:
13                 dp[i][j] = dp[i-1][j-1] + 1
14                 states[i][j] = 1
15                 
16             elif dp[i-1][j] > dp[i][j-1]:
17                 dp[i][j] = dp[i-1][j]
18                 states[i][j] = 2
19                 
20             elif dp[i-1][j] < dp[i][j-1]:
21                 dp[i][j] = dp[i][j-1]
22                 states[i][j] = 3
23             else:
24                 dp[i][j] = dp[i][j-1]
25                 states[i][j] = 4
26                 
27     lcsLength = dp[-1][-1]            
28     printAllLCS(states, x, lcsLength, m, n, '')           
29     return lcsLength
30 
31 def  printAllLCS(states, x, lcsLength, i, j, lcs):
32     """states表;只須要一個字符串就夠了;LCS長度;當前位置ij;已搜索軌跡lcs"""
33     if i == 0 or j == 0:
34         if len(lcs) == lcsLength:
35             print(lcs[::-1])  # 從後往前dfs搜索的,這裏逆序輸出
36         return
37     
38     direction = states[i][j]
39     if direction == 1:
40         printAllLCS(states, x, lcsLength, i-1, j-1, lcs+x[i-1])
41     elif direction == 2:
42         # 同一行或者同一列轉移過來的字符沒有變化
43         printAllLCS(states, x, lcsLength, i-1, j, lcs)
44     elif direction == 3:
45         printAllLCS(states, x, lcsLength, i, j-1, lcs)
46     elif direction == 4:
47         # 兩個來源都有可能
48         printAllLCS(states, x, lcsLength, i-1, j, lcs)
49         printAllLCS(states, x, lcsLength, i, j-1, lcs)
View Code

 

 

帶權項目時間計劃

典型的選仍是不選的問題。OPT(i) 表示一共有前 i 個任務的話,最多能掙多少錢,那麼從後往前考慮,就是第i個任務選仍是不選。若是不選,OPT(i) = OPT(i-1);若是選了,就看選了第i個,再往前有幾個能作,好比若是選了8,那麼前面只能從5往前選,用prev(i)來表示這個索引,即prev(8) = 5。因此遞推公式已經列出來了,而prev(i)是能夠先肯定的。

 

寫出遞歸樹能夠發現這是個重疊子問題,用動態規劃求解便可。

 

 

和最大的不連續子數組

給定一個數組,選出和最大的子數組,長度不限,但不能選相鄰元素。例如 [4, 1, 1, 9, 1],知足條件的和最大子數組爲 [4, 9]。定義 OPT(i) 爲到下標爲 i 的數爲止的最大不連續子數組之和。若是選了下標爲 i 的數,那麼前面最多能選到下標 i-2;若是不選則前面能選到 i-1

 1 def maxSubArray(arr):
 2     if not arr:
 3         return 0
 4     
 5     n = len(arr)
 6     if n == 1:
 7         return arr[0]
 8     
 9     dp = [0]*n
10     dp[0], dp[1] = arr[0], max(arr[0], arr[1])
11     
12     for i in range(2, n):
13         dp[i] = max(dp[i-2] + arr[i], dp[i-1])
14         
15     return dp[n-1]
View Code

 

 

和爲給定值的子數組

給定一個正整數數組和一個正整數目標值,判斷可否找到一個子數組,和剛好爲給定的目標值。subset(i, s),i表示當前看第i個數字、s爲目標值。對於每一個當前數字,有選或不選兩種可能,只要有一種知足條件便可。

出口狀況:s爲0,返回 true;i爲0,只有當 s == arr[0] 才返回true;若是 arr[i] > s,選上必定超,只考慮不選arr[i]的狀況

if s == 0:
    return True
if i == 0:
    return arr[i] == s
if arr[i] > s:
    return subset(i-1, s)

  

遞歸寫一下

 1 def solution(array, s):
 2     if not array:
 3         return False
 4     n = len(array)
 5     
 6     def helper(arr, i, s):
 7         if s == 0:
 8             return True
 9         if i == 0:
10             return arr[i] == s
11         if arr[i] > s:
12             return helper(arr, i-1, s)
13         return helper(arr, i-1, s-arr[i]) or helper(arr, i-1, s)
14     
15     return helper(array, n-1, s)
View Code

 

動態規劃寫一下,顯然是一個二維 dp,遞歸出口就是 dp 的初始化的條件

 1 def solution(array, s):
 2     if not array:
 3         return False
 4     n = len(array)
 5     dp = [[False]*(s+1) for _ in range(n)]
 6     
 7     for i in range(n):
 8         dp[i][0] = True
 9         
10     dp[0][array[0]] = True
11     
12     for i in range(1, n):
13         for t in range(1, s+1):
14             if array[i] > t:
15                 dp[i][t] = dp[i-1][t]
16             else:
17                 dp[i][t] = dp[i-1][t-array[i]] or dp[i-1][t]
18     return dp[-1][-1]
View Code

 

 

零錢兌換

給定不一樣面額的硬幣 coins 和一個總金額 amount。編寫一個函數來計算能夠湊成總金額所需的最少的硬幣個數。若是沒有任何一種硬幣組合能組成總金額,返回 -1。

暴力法,先考慮一下遞歸關係。 總金額爲0時,f(amount) = 0;總金額不爲0時,f(amount) = 1 + min{ f(amount - ci) | i 屬於 [1, k] }。

解釋一下,要求總金額爲amount的最少硬幣個數,先選一個合法可選面值的硬幣,總金額變爲amount - ci,總的最少硬幣數等於子問題的最優解+1。

這裏用到了最優子結構性質,即原問題的解由子問題的最優解構成。要符合最優子結構,子問題之間必須獨立。

 1 import sys
 2 class Solution:
 3     def coinChange(self, coins: List[int], amount: int) -> int:
 4         if amount == 0:
 5             return -1
 6         ans = sys.maxsize
 7         for coin in coins:
 8             if amount < coin:  # 金額不可達
 9                 continue
10             subProblem = self.coinChange(coins, amount-coin)
11             
12             if subProblem == -1:  # 子問題無解
13                 continue
14             ans = min(ans, subProblem + 1)
15         
16         return -1 if ans == sys.maxsize else ans
View Code

 

遞歸+記憶化

 1 import sys
 2 class Solution:
 3     def coinChange(self, coins: List[int], amount: int) -> int:
 4         if not coins:
 5             return -1
 6         memo = [-2] * (amount+1)   # memo[amount]表示湊到金額爲amount的最少硬幣數
 7         
 8         def helper(coins, amount, memo):
 9             if amount == 0:
10                 return 0
11             if memo[amount] != -2:
12                 return memo[amount]
13             
14             ans = sys.maxsize
15             for coin in coins:
16                 if amount < coin:  # 金額不可達
17                     continue
18                 subProblem = helper(coins, amount-coin, memo)
19 
20                 if subProblem == -1:  # 子問題無解
21                     continue
22                 ans = min(ans, subProblem + 1)
23             
24             memo[amount] = -1 if ans == sys.maxsize else ans  # 記錄本輪答案
25             return memo[amount]  
26         
27         return helper(coins, amount, memo)
View Code

 

動態規劃,按上面描述的狀態方程。

 1 import sys
 2 class Solution:
 3     def coinChange(self, coins: List[int], amount: int) -> int:
 4         if not coins:
 5             return 0
 6         dp = [sys.maxsize] * (amount+1)
 7         dp[0] = 0
 8         
 9         for i in range(1, amount+1):
10             for j in range(len(coins)):
11                 if i < coins[j]:
12                     continue
13                 dp[i] = min(dp[i], dp[i - coins[j]] + 1)
14             
15         return -1 if dp[amount] == sys.maxsize else dp[amount]
View Code

 

 

出發到終點全部可能的路徑問題

只能向右或向下,塗實的點不能走。考慮每一個出發點可能的路徑數,等於右邊一格做爲出發點的路徑數+下邊一格做爲出發點的路徑數。

暴力遞歸,自頂向下

 

自底向上遞推,若是要到達a[i, j]點,只能從它的上面或者左邊通過:

opt[i, j] = opt[i-1, j] + opt[i, j-1]

# --------------------------------------

if isValid(a[i, j]):
    opt[i, j] = opt[i-1, j] + opt[i, j-1]
else:
    opt[i, j]  = 0   # 石頭

 

 

正則表達式

給你一個字符串 s 和一個字符規律 p,請你來實現一個支持 '.' 和 '*' 的正則表達式匹配。

'.' 匹配任意單個字符
'*' 匹配零個或多個前面的那一個元素
所謂匹配,是要涵蓋 整個 字符串 s的,而不是部分字符串。

說明:

s 可能爲空,且只包含從 a-z 的小寫字母。
p 可能爲空,且只包含從 a-z 的小寫字母,以及字符 . 和 *。

先看無論通配符,兩個普通字符串進行比較應該怎麼寫。而後再改爲比較通用的框架,再寫成遞歸

1 def isMatch(text, pattern):
2     if len(text) != len(pattern):
3         return False
4     for j in range(len(pattern)):
5         if pattern[j] != text[j]:
6             return False
7     return True
View Code
 1 def isMatch(text, pattern):
 2     m, n = len(text), len(pattern)
 3     i, j = 0, 0   # 雙指針 
 4     while j < n:
 5         if i >= m:  # 若是text的指針越界了但pattern的指針沒有,說明沒有待匹配的字符了但模式串還剩下,不匹配
 6             return False
 7         if pattern[j] != text[i]: 
 8             return False
 9         j += 1
10         i += 1
11     return j == n  # 最後看模式串字符是否是都匹配完了
View Code
1 def isMatch(text, pattern):
2     if len(pattern) == 0:
3         return len(text) == 0
4     first_match = len(text) != 0 and text[0] == pattern[0]
5     return first_match and isMatch(text[1:], pattern[1:])
View Code

 

而後處理通配符,'.' 能夠匹配任意一個字符,因此判斷能不能匹配的時候有兩種狀況,直接匹配或者用'.'匹配

1 def isMatch(text, pattern):
2     if not pattern:
3         return not text
4     first_match = bool(text) and pattern[0] in {text[0], '.'}
5     return first_match and isMatch(text[1:], pattern[1:])
View Code

 

再處理'*',星號可讓以前的一個字符出現任意次數,包括0次。關鍵就是出現幾回呢,交給遞歸好了,當前只可能出現0次或者1次。若是匹配前一個字符0次,那就直接從模式串的p[2:] 再匹配文本串;若是當前匹配一次,那文本串要向後移動一位,後面還須要匹配幾回交給遞歸。

1 def isMatch(text, pattern):
2     if not pattern:
3         return not text
4     first_match = bool(text) and pattern[0] in {text[0], '.'}
5     if len(pattern) >= 2 and pattern[1] == '*':  # '*' 不能放首位,發現'*'
6         return isMatch(text, pattern[2:]) or (first_match and isMatch(text[1:], pattern))
7     # else
8     return first_match and isMatch(text[1:], pattern[1:])
View Code

 

而後加上記憶化

 1 def isMatch(text, pattern):
 2     memo = dict()
 3     def dp(i, j):
 4         if (i, j) in memo:
 5             return memo[(i, j)]
 6         if j == len(pattern):
 7             return i == len(text)
 8         first_match = i < len(text) and pattern[j] in {text[i], '.'}
 9         if j <= len(pattern)-2 and pattern[j+1] == '*':
10             ans = dp(i, j+2) or (first_match and dp(i+1, j))
11         else:
12             ans = first_match and dp(i+1, j+1)
13         memo[(i, j)] = ans
14         return ans
15     
16     return dp(0, 0)
View Code

 

 

如何判斷是否是重疊子問題:

  1. 隨便假設一個輸入,畫遞歸樹

  2. 先抽象出遞歸算法的框架,而後判斷原問題是如何到達子問題的,看看不一樣的路徑是否是都到達了同一個問題,若是是的話那就是重疊子問題。例如這題

def dp(i ,j):
    dp(i, j+2) # 1
    dp(i+1, j) # 2
    dp(i+1, j+1) # 3

  

dp(i, j) 如何到達 dp(i+2, j+2)。 dp(i, j) -> #3 -> #3;或者dp(i, j) -> #1 -> #2 -> #2;或者dp(i, j) -> #2 -> #2 -> #1,因此必定存在重疊子問題,必定須要動態規劃技巧來優化。

 1 def isMatch(text, pattern):
 2         dp = [[False] * (len(pattern)+1) for _ in range(len(text)+1)]
 3          
 4         dp[-1][-1] = True  # 空串匹配空串
 5         
 6         for i in range(len(text), -1, -1):
 7             for j in range(len(pattern)-1, -1, -1):
 8                 first_match = i < len(text) and pattern[j] in {text[i], '.'}
 9                 if j <= len(pattern)-2 and pattern[j+1] == '*':
10                     dp[i][j] = dp[i][j+2] or (first_match and dp[i+1][j])
11                 else:
12                     dp[i][j] = first_match and dp[i+1][j+1]
13                      
14         return dp[0][0]
View Code

 

 

設計動態規劃的通用技巧:數學概括

最長遞增子序列

給定一個無序的整數數組,找到其中最長上升子序列的長度。

定義 dp[i] 表示以 nums[i] 這個數結尾的最長遞增子序列的長度。根據這個定義,子序列的最大長度應該是dp數組中的最大值。假設已經知道了 dp[0...i-1] 的結果,如何經過這些已知結果推出 dp[i] 呢,這個就是狀態轉移方程了。顯然要知道 nums[i] 能不能加入到上升子序列中,就要找到前面那些結尾比 nums[i] 小的子序列,而後再把 nums[i] 接上,由於要求最大子序列,因此就接上以前的最大子序列便可。剩下的就是base case,這題 dp 數組初始化爲1,由於子序列最少也要包含本身。

 1 class Solution:
 2     def lengthOfLIS(self, nums: List[int]) -> int:
 3         if not nums:
 4             return 0
 5         n = len(nums)
 6         dp = [1] * n
 7         for i in range(n):
 8             for j in range(i-1, -1, -1):
 9                 if nums[j] < nums[i]:
10                     dp[i] = max(dp[i], dp[j] + 1)
11         return max(dp)
View Code

 

但這道題還有一種 O(NlogN) 的解法,可是不看答案估計很難想得出。把上面方法中內層 j 循環替換成二分。始終維護一個數組 LIS 爲要求的上升子序列,對每個 nums[i],都插入到LIS中(二分法找到第一個比 nums[i] 大的數替換掉,由於這樣儘量多的讓後面符合條件的數進來、縮一下上界。若是 nums[i] 比 LIS 全部都大就直接 append),最後 LIS 的長度即爲所求。 代碼在 Leetcode-動態規劃 http://www.javashuo.com/article/p-kwympcwh-co.html

 

 

從最長上升子序列到信封嵌套

俄羅斯套娃信封問題

這道題是最長上升子序列的升維,要先對寬度進行升序排列,而後對寬度相同的按高度降序排序。最後對高度數組進行最長上升子序列的求解

 1 class Solution:
 2     def maxEnvelopes(self, envelopes: List[List[int]]) -> int:
 3         if not envelopes:
 4             return 0
 5         n = len(envelopes)
 6         nums = sorted(envelopes, key=lambda x: [x[0], -x[1]])
 7         dp = [1] * n
 8         
 9         for i in range(n):
10             for j in range(i-1, -1, -1):
11                 if nums[i][1] > nums[j][1]:
12                     dp[i] = max(dp[i], dp[j] + 1)
13         return max(dp)
View Code

  

用剛纔提到的二分法來優化

 1 class Solution:
 2     def maxEnvelopes(self, envelopes: List[List[int]]) -> int:
 3         if not envelopes:
 4             return 0
 5         n = len(envelopes)
 6         nums = sorted(envelopes, key=lambda x: [x[0], -x[1]])
 7         LIS = []
 8         
 9         for i in range(n):
10             if not LIS or nums[i][1] > LIS[-1]:
11                 LIS.append(nums[i][1])
12             else:
13                 index = self.binarySearch(LIS, nums[i][1])
14                 LIS[index] = nums[i][1]
15         return len(LIS)
16     
17     def binarySearch(self, array, target):
18         """返回第一個比target大的元素索引"""
19         if not array:
20             return
21         low, high = 0, len(array) - 1
22          
23         while low <= high:
24             mid = low + (high-low)//2
25             if array[mid] < target:
26                 low = mid + 1
27             elif array[mid] > target:
28                 high = mid - 1
29             else:
30                 return mid
31         return low
View Code

 

 

 

博弈問題的思路是在二維dp的基礎上使用元組分別存儲兩我的的博弈結果。

一堆石頭用數組piles表示,piles[i]表示第i堆有多少個石頭,兩我的拿石頭,一次拿一堆,但只能拿走最左邊或者最右邊。全部石頭被拿完後,誰擁有但石頭多誰獲勝。

假設兩人都很聰明,請你設計一個算法,返回先手和後手的最後得分(石頭總數)之差。好比上面[1, 100, 3],先手能得到 4 分,後手會得到 100 分,你的算法應該返回 -96。

博弈問題的通用框架。

定義dp數組,dp[i][j] = (first, second),dp[i][j].fir 表示對於 piles[i,...,j]這部分,先手能得到的最高分數,dp[i][j].sec 表示後手能得到的最高分數

對於每一個狀態,能夠作的選擇有兩個:選擇最左邊的仍是最右邊的。那麼窮舉狀態:

for 0<=i <n:
    for  i<=j < n:
        for who in {first, second}:
            dp[i][j][who] = max(left, right)

  

可是先手的選擇會對後手有影響。面對piles[i,...,j]先手選了左邊,而後面對piles[i+1,...,j]但對方先選,本身變成後手。或者先手選了右邊,而後面對piles[i,...,j-1]本身後手。

dp[i][j].fir = max(piles[i] + dp[i+1][j].sec, piles[j] + dp[i][j-1].sec)  

若是做爲後手,就要等先手先選擇,若是對方先手選了最左邊,本身先手面對piles[i+1,...,j];

dp[i][j].sec = dp[i+1][j].fir

若是對方先手選了右邊,本身先手面對piles[i,...,j-1]

dp[i][j].sec = dp[i][j-1].fir

 

那麼,base case也容易肯定,當i==j,也就是隻有一堆的時候,先手得分爲piles[i],後手不得分0。可是 base case 在 dp table 中是斜的,並且計算dp[i][j]的時候須要dp[i+1][j] 和dp[i][j-1],因此要斜着遍歷數組。(怎麼實現?按對角線斜線往下,一條一條遍歷)

 1 class Pair:
 2     def __init__(self, fir, sec):
 3         self.fir = fir
 4         self.sec = sec
 5         
 6 def stoneGame(piles):
 7     if not piles:
 8         return 0
 9     n = len(piles)
10     dp = [[Pair(-1, -1) for _ in range(n)] for _ in range(n)]
11     
12     for i in range(n):
13         dp[i][i].fir = piles[i]
14         dp[i][i].fir = 0
15         
16     # 斜着遍歷
17     for l in range(1, n):  # 目前遍歷的是第幾條斜線,第0條初始化了
18         for i in range(n-l):  # dp[i][j]須要dp[i+1][j] 和dp[i][j-1]
19             j = l + i  # j的座標始終比i多l
20             left = piles[i] + dp[i+1][j].sec
21             right = piles[j] + dp[i][j-1].sec
22             
23             if left > right:
24                 dp[i][j].fir = left
25                 dp[i][j].sec = dp[i+1][j].fir
26             else:
27                 dp[i][j].fir = right
28                 dp[i][j].sec = dp[i][j-1].fir
29                 
30     return dp[0][n-1].fir - dp[0][n-1].sec
View Code

 

 

揹包問題

01揹包

N 件物品,容量爲 C 的揹包,第 i 件物品的重量爲 Wi,價值爲Vi。求裝的最大價值

每件物品要麼取要麼不取。dp[i, j] 表示取到前 i 件物品,容量爲 j 的最大價值,

dp[i, j] = max(dp[i-1, j],  dp[i-1, j - Wi] + Vi) i:1~n  j:0~W

 

若是倒着遍歷,能夠用滾動數組把 dp 數組優化到一維,dp[j] = max(dp[j], dp[j-Wi]+Vi)  j:W~0

 

徹底揹包

N 件物品,容量爲 C 的揹包,第 i 件物品的重量爲 Wi,價值爲Vi,每件物品有無數個。求裝的最大價值

每件物品能夠從不取,一直取到揹包滿了爲止。dp[i, j] 表示取到前 i 件物品,容量爲 j 的最大價值,

dp[i, j] = max( dp[i-1, j - k*Wi] + k*Vi  | 0 <= k<= j//Wi  )

 

考慮一下優化,對於 dp[i, j] ,選擇 k 個;等價於 dp[i, j-Wi] 選擇 k-1 個,這是兩個重複計算的狀態。

因此把 k=0 的狀態提出來,dp[i, j] = max{dp[i-1, j],  dp[i-1, j-k*Wi] + k*Vi, 1<= k <= j//Wi}

dp[i-1, j-Wi-k*Wi] + k*Vi + Vi | 0 <= k <= (j-Wi)//Wi ;對全部的 k 取 max,就等價於 dp[i, j - Wi] + Vi

因此 dp[i, j] = max( dp[i-1, j], dp[i, j - Wi] + Vi)

就獲得了 O(CN) 的算法。

 

滾動數組優化,但這裏要注意的是正着遍歷,由於 dp[i, j - Wi] 是當前層的值。dp[j] = max(dp[j], dp[j-Wi]+Vi)  j:0~W

 

硬幣兌換

僅有1分、2分、3分的硬幣,將錢 N 兌換成硬幣有多少種方法。N < 32768

用徹底揹包的思路來思考,dp[i, j] = dp[i-1, j] + dp[i, j-a[i]];進一步優化成 dp[j] = dp[j] + dp[j-a[i]]

 

 

DP vs 回溯 vs 貪⼼

回溯(遞歸) — 重複計算
(沒有最優子結構的話就是須要窮舉全部的可能,並且不存在重複計算的問題)

貪⼼算法 — 永遠局部最優
(但到處局部最優可能最後不是所有最優)

動態規劃 — 記錄局部最優⼦子結構 / 多種記錄值(避免重複計算,只需依賴前一狀態的最優值)

相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息