一、定義
動態規划(Dynamic Programming,DP)是運籌學的一個分支,是求解[決策過程最優化]的方法。
把多階段過程轉化為一系列單階段問題,利用各階段之間的關系,逐個求解,創立了解決這類過程優化問題的新方法——動態規划
雖然動態規划主要用於求解以時間划分階段的動態過程的優化問題,但是一些與時間無關的靜態規划(如線性規划、非線性規划),
只要人為地引進時間因素,把它視為多階段決策過程,也可以用動態規划方法方便地求解。
在現實生活中,有一類活動的過程,由於它的特殊性,可將過程分成若干個互相聯系的階段,在它的每一階段都需要作出決策,從而使整個過程達到最好的活動效果。因此各個階段決策的選取不能任意確定,它依賴於當前面臨的狀態,又影響以后的發展。當各個階段決策確定后,就組成一個決策序列,因而也就確定了整個過程的一條活動路線.這種把一個問題看作是一個前后關聯具有鏈狀結構的多階段過程就稱為多階段決策過程,這種問題稱為多階段決策問題。在多階段決策問題中,各個階段采取的決策,一般來說是與時間有關的,決策依賴於當前狀態,又隨即引起狀態的轉移,一個決策序列就是在變化的狀態中產生出來的,故有“動態”的含義,稱這種解決多階段決策最優化的過程為動態規划方法
動態規划算法通常用於求解具有某種最優性質的問題。在這類問題中,可能會有許多可行解。每一個解都對應於一個值,我們希望找到具有最優值的解。動態規划算法與分治法類似,其基本思想也是將待求解問題分解成若干個子問題,先求解子問題,然后從這些子問題的解得到原問題的解。與分治法不同的是,適合於用動態規划求解的問題,經分解得到子問題往往不是互相獨立的。若用分治法來解這類問題,則分解得到的子問題數目太多,有些子問題被重復計算了很多次。
二、示例
動態規划(英語:Dynamic programming,簡稱 DP)是一種在數學、管理科學、計算機科學、經濟學和生物信息學中使用的,通過把原問題分解為相對簡單的子問題的方式求解復雜問題的方法。
動態規划常常適用於有重疊子問題和最優子結構性質的問題,並且記錄所有子問題的結果,因此動態規划方法所耗時間往往遠少於朴素解法。
動態規划有自底向上和自頂向下兩種解決問題的方式。自頂向下即記憶化遞歸,自底向上就是遞推。
使用動態規划解決的問題有個明顯的特點,一旦一個子問題的求解得到結果,以后的計算過程就不會修改它,這樣的特點叫做無后效性,求解問題的過程形成了一張有向無環圖。動態規划只解決每個子問題一次,具有天然剪枝的功能,從而減少計算量。
從上面的定義中可以知道使用動態規划的場景特征:
- 求一個問題的最優解
- 大問題可以分解為子問題,子問題還有重疊的更小的子問題
- 整體問題最優解取決於子問題的最優解(狀態轉移方程)
- 從上往下分析問題,從下往上解決問題
- 討論底層的邊界問題
動態規划最重要的有三個概念:
- 最優子結構:是指每個階段的最優狀態可以從之前某個階段的某個或某些狀態直接得到(子問題的最優解能夠決定這個問題的最優解),
- 邊界:指的是問題最小子集的解(初始范圍)
- 狀態轉移方程:是指從一個階段向另一個階段過度的具體形式,描述的是兩個相鄰子問題之間的關系(遞推式)
dp最核心的部分是狀態轉移。所以一般第一步是找狀態,定義一下dp數組,明確dp數組的含義;第二步是初始化dp數組;第三步是狀態轉移。
1、53. 最大子序和
https://leetcode-cn.com/problems/maximum-subarray/
給定一個整數數組 nums ,找到一個具有最大和的連續子數組(子數組最少包含一個元素),返回其最大和。
示例 1:
輸入:nums = [-2,1,-3,4,-1,2,1,-5,4]
輸出:6
解釋:連續子數組 [4,-1,2,1] 的和最大,為 6 。
示例 2:
輸入:nums = [1]
輸出:1
示例 3:
輸入:nums = [0]
輸出:0
示例 4:
輸入:nums = [-1]
輸出:-1
示例 5:
輸入:nums = [-100000]
輸出:-100000
提示:
1 <= nums.length <= 3 * 104
-105 <= nums[i] <= 105
為了保證計算子問題能夠按照順序、不重復地進行,動態規划要求已經求解的子問題不受后續階段的影響。這個條件也被叫做「無后效性」。 換言之,動態規划對狀態空間的遍歷構成一張有向無環圖,遍歷就是該有向無環圖的一個拓撲序。 有向無環圖中的節點對應問題中的「狀態」,圖中的邊則對應狀態之間的「轉移」,轉移的選取就是動態規划中的「決策」。 class Solution: def maxSubArray(self, nums): length = len(nums) # 列表的長度 dp = [0] * length # dp[i] 表示以 nums[i] 結尾 的 連續 子數組的最大和 dp[0] = nums[0] # 初始化,索引為0時,連續的最大值子數組的值是自身 for i in range(1, length): if dp[i - 1] > 0: # 如果前一個 連續的最大值子數組的值 大於 0 dp[i] = dp[i - 1] + nums[i] # 那么無論nums[i]是正數還是負數,加上正數后必定比自身的值大 else: dp[i] = nums[i] # 如果前一個 連續的最大值子數組的值 小於 0,直接使用nums[i] return max(dp) n = [-2, 1, -3, 4, -1, 2, 1, -5, 4] res = Solution().maxSubArray(n) print(res)
2、爬樓梯
https://leetcode-cn.com/problems/climbing-stairs/
假設你正在爬樓梯。需要 n 階你才能到達樓頂。
每次你可以爬 1 或 2 個台階。你有多少種不同的方法可以爬到樓頂呢?
注意:給定 n 是一個正整數。
示例 1:
輸入: 2
輸出: 2
解釋: 有兩種方法可以爬到樓頂。
1. 1 階 + 1 階
2. 2 階
示例 2:
輸入: 3
輸出: 3
解釋: 有三種方法可以爬到樓頂。
1. 1 階 + 1 階 + 1 階
2. 1 階 + 2 階
3. 2 階 + 1 階
""" 分析: 假設要爬到 5 層樓梯: 先爬到第 3 層樓梯,爬到第3層樓梯有多少種方法,那么再爬2個台階到第5層就有多少種方法 先爬到第 4 層樓梯,爬到第4層樓梯有多少種方法,那么再爬1個台階到第5層就有多少種方法 那么爬到第5層的方法就是:爬到第3層和爬到第4層方法的總和 而爬到第4層和爬到第3層兩種方式中,即使中間會有相同的部分,但只要整條路徑存在不相同的部分,那就是不相同的方法。 那么就可以寫出狀態轉移方程: f(n) = f(n-1) + f(n-2) 邊界: f(1) = 1 f(2) = 2 """ class Solution: def climbStairs(self, n: int) -> int: if n < 3: return n dp = [0] * n dp[0] = 1 # 這里索引從0開始,需要注意 dp[1] = 2 for i in range(2, n): # 這里索引從0開始,需要注意 # 因此第3層開始,索引是2,最后一層是n-1 dp[i] = dp[i - 1] + dp[i - 2] return dp[n - 1] """ 思考:如果一次能爬3個階梯呢? 實際也差不多: 狀態轉移方程: f(n) = f(n-1) + f(n-2) + f(n-3) 邊界: f(1) = 1 f(2) = 2 f(2) = 3 """
3、楊輝三角
https://leetcode-cn.com/problems/pascals-triangle/
給定一個非負整數 numRows,生成「楊輝三角」的前 numRows 行。
在「楊輝三角」中,每個數是它左上方和右上方的數的和。

示例 1:
輸入: numRows = 5
輸出: [[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1]]
示例 2:
輸入: numRows = 1
輸出: [[1]]
""" 我的思路: 從第三層開始,每一層除了首尾固定為1之外,其他的值 = 上一層的這個值的索引 + (上一層的這個值的索引-1) 例如: 第四層的:[1,3,3,1],首尾不管 索引為1的位置值 = 第三層的索引為0的值 + 第三層的索引為1的值(1+2) 索引為2的位置值 = 第三層的索引為1的值 + 第三層的索引為2的值(2+1) 得出: 邊界: f(1) = [1] f(2) = [1, 1] 狀態轉移: f(n)[index] = f(n-1)[index - 1] + f(n-1)[index] """ class Solution: def generate(self, numRows: int) -> List[List[int]]: if numRows == 1: return [[1]] elif numRows == 2: return [[1], [1, 1]] # 初始化 dp = [[] for _ in range(numRows)] dp[0] = [1] dp[1] = [1, 1] for i in range(2, numRows): # 初始化長度 dp[i] = [1] * (i + 1) for j in range(len(dp[i])): if j != 0 and j != (len(dp[i]) - 1): # 過濾掉首尾 dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j] return dp
4、楊輝三角 II
https://leetcode-cn.com/problems/pascals-triangle-ii/
給定一個非負索引 rowIndex,返回「楊輝三角」的第 rowIndex 行。
在「楊輝三角」中,每個數是它左上方和右上方的數的和。
示例 1:
輸入: rowIndex = 3
輸出: [1,3,3,1]
示例 2:
輸入: rowIndex = 0
輸出: [1]
示例 3:
輸入: rowIndex = 1
輸出: [1,1]
# 方法1:跟上面一樣,注意索引就好 class Solution: def getRow(self, rowIndex): """這里跟上面對比,注意索引""" if rowIndex == 0: return [1] elif rowIndex == 1: return [1, 1] dp = [[] for _ in range(rowIndex + 1)] dp[0] = [1] dp[1] = [1, 1] for i in range(2, rowIndex + 1): dp[i] = [1] * (i + 1) for j in range(len(dp[i])): if j != 0 and j != (len(dp[i]) - 1): dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j] return dp[rowIndex] class Solution: def getRow(self, rowIndex: int) -> List[int]: """ 只使用一個數組解決問題 當前行第 i 項的計算只與上一行第 i−1 項及第 i 項有關。 因此我們可以倒着計算當前行,這樣計算到第 i 項時,第 i−1 項仍然是上一行的值。 舉例子: 1 1 推出第三階: 1 1 1 先構造默認值,補個1 倒着推(首尾不算),-2的位置: 1 + 1 = 2 得到 1 2 1 1 3 3 1 推出第五階: 1 3 3 1 1 (后面補個1) 倒着推 3 + 1 = 4, 3 + 3 = 6, 1 + 3 = 4. 依次倒着更新推出結果。得出結果: 1 4 6 4 1 """ res = [1] * (rowIndex + 1) # 初始化 for i in range(2, rowIndex + 1): # 從第三行開始 for j in range(i - 1, 0, -1): # 首尾不算,都為1 res[j] = res[j] + res[j - 1] return res
5、斐波那契數
https://leetcode-cn.com/problems/fibonacci-number/
斐波那契數,通常用 F(n) 表示,形成的序列稱為 斐波那契數列 。該數列由 0 和 1 開始,后面的每一項數字都是前面兩項數字的和。也就是:
F(0) = 0,F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1
給你 n ,請計算 F(n) 。
示例 1:
輸入:2
輸出:1
解釋:F(2) = F(1) + F(0) = 1 + 0 = 1
示例 2:
輸入:3
輸出:2
解釋:F(3) = F(2) + F(1) = 1 + 1 = 2
示例 3:
輸入:4
輸出:3
解釋:F(4) = F(3) + F(2) = 2 + 1 = 3
提示:
其實這道題跟爬樓梯的是類似的原理
class Solution: def fib(self, n: int) -> int: if n < 2: return n f0 = 0 f1 = 1 res = 0 for i in range(2, n + 1): res = f0 + f1 f0 = f1 f1 = res return res
6、泰波那契序列
https://leetcode-cn.com/problems/n-th-tribonacci-number/
泰波那契序列 Tn 定義如下:
T0 = 0, T1 = 1, T2 = 1, 且在 n >= 0 的條件下 Tn+3 = Tn + Tn+1 + Tn+2
給你整數 n,請返回第 n 個泰波那契數 Tn 的值。
示例 1:
輸入:n = 4
輸出:4
解釋:
T_3 = 0 + 1 + 1 = 2
T_4 = 1 + 1 + 2 = 4
示例 2:
輸入:n = 25
輸出:1389537
class Solution: def tribonacci(self, n: int) -> int: if n < 2: return n elif n == 2: return 1 t0 = 0 t1 = 1 t2 = 1 res = 0 for i in range(3, n+1): res = t0 + t1 + t2 t0 = t1 t1 = t2 t2 = res return res
7、最長回文子串
https://leetcode-cn.com/problems/longest-palindromic-substring/
回文子串(這里只是說回文子串,並不是最長)的意思是:從左到右讀,和從右到左讀,是一樣的,例如: bab 順着讀反着讀是一樣的。
給你一個字符串 s,找到 s 中最長的回文子串。
示例 1:
輸入:s = "babad"
輸出:"bab"
解釋:"aba" 同樣是符合題意的答案。
示例 2:
輸入:s = "cbbd"
輸出:"bb"
示例 3:
輸入:s = "a"
輸出:"a"
示例 4:
輸入:s = "ac"
輸出:"a"
官方解法
對於一個子串而言,如果它是回文串,並且長度大於 2,那么將它首尾的兩個字母去除之后,它仍然是個回文串。
例如對於字符串“ababa”,如果我們已經知道 “bab” 是回文串,那么 “ababa” 一定是回文串,這是因為它的首尾兩個字母都是“a”。
根據這樣的思路,我們就可以用動態規划的方法解決本題。我們用 P(i,j) 表示字符串 s 的第 i 到 j 個字母組成的串(下文表示成 s[i:j] )是否為回文串:


class Solution: def longestPalindrome(self, s: str) -> str: n = len(s) if n < 2: return s max_len = 1 begin = 0 # dp[i][j] 表示 s[i..j] 是否是回文串 dp = [[False] * n for _ in range(n)] for i in range(n): dp[i][i] = True # 單個字符必然是回文子串 # 遞推開始 # L代表子串的長度 for L in range(2, n + 1): # 枚舉左邊界,左邊界的上限設置可以寬松一些 for i in range(n): # 由 L 和 i 可以確定右邊界,即 j - i + 1 = L 得 j = L + i - 1 # 如果右邊界越界,就可以退出當前循環 if j >= n: break if s[i] != s[j]: dp[i][j] = False else: if j - i < 3: dp[i][j] = True else: dp[i][j] = dp[i + 1][j - 1] # 只要 dp[i][L] == true 成立,就表示子串 s[i..L] 是回文,此時記錄回文長度和起始位置 if dp[i][j] and j - i + 1 > max_len: max_len = j - i + 1 begin = i return s[begin:begin + max_len]
