動態規劃 Dynamic Programminghtml
斐波那契數列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)
實際上這已經和動態規劃同樣了,只不過這是自頂向下的。而動態規劃是自底向上的。從最小的子問題一步一步向上遞推,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]
進一步優化空間複雜度,因爲只要最後一個狀態,並且狀態轉移只取決於相鄰的狀態,不須要開一維數組,直接兩個變量滾動更新就好了。優化
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
從一道題展開
最長公共子序列 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]
優化空間複雜度,比較簡單的作法就是用滾動數組優化到 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]
在獲得 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)
帶權項目時間計劃
典型的選仍是不選的問題。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]
和爲給定值的子數組
給定一個正整數數組和一個正整數目標值,判斷可否找到一個子數組,和剛好爲給定的目標值。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)
動態規劃寫一下,顯然是一個二維 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]
零錢兌換
給定不一樣面額的硬幣 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
遞歸+記憶化
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)
動態規劃,按上面描述的狀態方程。
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]
出發到終點全部可能的路徑問題
只能向右或向下,塗實的點不能走。考慮每一個出發點可能的路徑數,等於右邊一格做爲出發點的路徑數+下邊一格做爲出發點的路徑數。
暴力遞歸,自頂向下
自底向上遞推,若是要到達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
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 # 最後看模式串字符是否是都匹配完了
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:])
而後處理通配符,'.' 能夠匹配任意一個字符,因此判斷能不能匹配的時候有兩種狀況,直接匹配或者用'.'匹配
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:])
再處理'*',星號可讓以前的一個字符出現任意次數,包括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:])
而後加上記憶化
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)
如何判斷是否是重疊子問題:
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]
設計動態規劃的通用技巧:數學概括
最長遞增子序列
給定一個無序的整數數組,找到其中最長上升子序列的長度。
定義 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)
但這道題還有一種 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)
用剛纔提到的二分法來優化
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
博弈問題的思路是在二維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
揹包問題
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 貪⼼
回溯(遞歸) — 重複計算 (沒有最優子結構的話就是須要窮舉全部的可能,並且不存在重複計算的問題)
貪⼼算法 — 永遠局部最優 (但到處局部最優可能最後不是所有最優)
動態規劃 — 記錄局部最優⼦子結構 / 多種記錄值(避免重複計算,只需依賴前一狀態的最優值)