======================= **基礎知識** =======================
1.遞推基礎知識:
斐波那契(Fibonacii)數列的遞推公式:F(n) = F(n -1) + F(n - 2);
70. 爬樓梯: Fibonacci 的最直接體現;
前置知識: 數學歸納法:
a: 驗證k0 成立; (邊界條件)
b: 證明如果ki 成立,那么Ki+1 也成立;(推導公式)
c: 聯合a & b, 證明k0 -> kn 成立;
2. 遞推問題的步驟:(確定遞推公式時候,不要管前一個結果如何得到)
1. 確定遞推狀態(狀態方程,學習重點): 一個函數符號f(x), 以及函數符號的含義描述; 確定自變量x(影響結果的因素有哪些), 因變量y;
2. 確定遞推公式(ki -> ki+1) ,取決於遞推狀態的定義;(這里ki 是第i組對應的滿足題意的結果,不要糾結與它怎么得到的!)
3. 分析邊界條件;
4. 程序實現: 遞歸 或者 循環
例子:下面例子中介紹的前兩種思路 是更通用的遞推思路;
比較直接的狀態定義:
進一步優化狀態定義:

1. 狀態的定義: f(n, j) : 第一塊塗第0種顏色(這里變成隱變量),最后一塊塗第 j 種顏色個數, 然后乘上顏色個數既可;因為第一塊塗的不同顏色對應可能個數是一樣的; 2. 那么遞推公式: f(n, j) = ∑ f(n -1, k) | k != j; 3.初值條件: f(1, 0) = 1, f(1, 1) = 0, f(1, 2) = 0; 下面都可由遞推公式得到: f(2, 0) = f(1, 1) + f(1, 2) = 0, f(2, 1) = f(1, 0) + f(1, 2) = 1, f(2, 2) = f(1, 1) + f(1, 0) = 1; f(3, 0) = 0(f(n,0)永遠為0), f(3, 1) = f(2, 0) + f(2, 2) = 1, f(3, 2) = f(2, 0) + f(3,1) = 1; 4.最終答案(第一塊顏色有3 種可能):3 * ( f(n - 1, 1) + f(n - 1, 2) );
進一步對於題意進行分析:

與題目本身的聯系比較緊,普世性差點: 1. 狀態定義:f(n) : n 塊環形牆壁時,滿足條件個數; 2. 遞推公式:第一塊 與 第 n - 1 塊的顏色不一樣情況 第一塊 與 第 n - 1塊顏色一樣,等價與 n - 2 的情況; 3. 初值條件: f(2) = 6, f(3) = 6; //注意f(2)=f(3),必須為初值,因為前后顏色都確定了; 4.最終答案: f(n) = f(n - 1) + f(n - 2) * 2;
3. 遞推 與 動態規划 之間的關系:
a. 動態規划是一種求最優化遞推問題,動態規划中涉及到決策過程; 所以動態規划是遞推問題的子問題;
其中 遞推公式 在動態規划中叫做 狀態轉移方程, 具體步驟與上面遞推是類似的;
b. 在狀態轉移方程中,主要下面三個重點:
狀態定義:
決策過程:從所有可能中選擇最優解; //如果不涉及決策,大概率就是貪心算法;
階段依賴:當前階段只依賴上一階段; 對於不同問題中,階段概念是一個很寬泛的定義;
c. 在實際求解問題過程中,有兩種方向:處理問題中,如果從哪里來的條件判斷比較復雜,那就可以考慮到哪里去的方法;
從哪里來:當前狀態 = f(前一個狀態 ) ; 當前這個狀態(被推導) 可以通過其他狀態推導得到;
到哪里去:每更新當前狀態時 ==> 主動更新可以從它推導出狀態的結果; 當前這個狀態(去推導)可以到達哪些狀態;
這兩種方向,本質是一樣的,底層是一個圖的結構, 而遞推(動歸) 求解順序就是狀態依賴圖的一個拓撲序,對有向圖的一維化序列化的結果;
d. 動歸典型題: 體會從哪里來,到哪里去的精髓;
120. 三角形最小路徑和 : 同樣的符號,不同的狀態定義,遞推公式就不一樣;決策過程也明確; 當前階段 與 下一階段 區分與依賴關系也很明確;

1 class Solution { 2 public: 3 int minimumTotal(vector<vector<int>>& triangle) { 4 //從上往下,到哪里去 5 int size = triangle.size(); 6 vector<vector<int>> nums(size, vector<int>(size, INT_MAX)); 7 nums[0][0] = triangle[0][0]; 8 9 for(int i = 0, I = size - 1; i < I; ++i) { 10 for(int j = 0; j <= i; ++j){ 11 nums[i + 1][j] = min(nums[i][j] + triangle[i + 1][j], nums[i + 1][j]); 12 nums[i + 1][j + 1] = min(nums[i][j] + triangle[i + 1][j + 1], nums[i + 1][j + 1]); 13 } 14 } 15 16 int ans = INT_MAX; 17 for(auto &x : nums[size -1]) ans = min(ans, x); 18 return ans; 19 20 21 //rev1: 從下往上, 從哪里來 22 //rev1 int size = triangle.size(); 23 //rev1 vector<vector<int>> nums(2, vector<int>(size, 0)); 24 //rev1 25 //rev1 int ind = (size - 1) % 2, pre_ind = 0; 26 //rev1 for(int i = 0; i < size; ++i) nums[ind][i] = triangle[size - 1][i]; 27 //rev1 28 //rev1 for(int i = size - 2; i >= 0; --i) { 29 //rev1 ind = i % 2; 30 //rev1 pre_ind = !ind; 31 //rev1 for(int j = 0; j <= i; ++j) 32 //rev1 nums[ind][j] = min(nums[pre_ind][j], nums[pre_ind][j + 1]) + triangle[i][j]; 33 //rev1 34 //rev1 } 35 //rev1 36 //rev1 return nums[0][0]; 37 } 38 };
e. 動歸程序實現中優化; 滾動數組 ==>再壓縮,可以順着/倒着刷表; 記憶化數組;
132. 分割回文串 II :記憶化數組

1 class Solution { 2 public: 3 #define MAX_N 2006 4 bool mark[MAX_N][MAX_N]; 5 6 bool isPalindrome(int l, int r, string s) { 7 if(s[l] == s[r]) { 8 if(r - l > 1) mark[l][r] = mark[l + 1][r - 1]; 9 else mark[l][r] = true; 10 } 11 return mark[l][r]; 12 } 13 14 int minCut(string s) { 15 memset(mark, 0, sizeof(mark)); 16 int n = s.size(); 17 vector<int>dp(n + 1, n); //i 個字符可組成的最少回文個數 18 dp[0] = 0; 19 20 for(int i = 0, pre = 0; i < n; ++i) { 21 dp[i + 1] = dp[i] + 1; 22 for(int j = pre; j <= i; ++j) { 23 if(isPalindrome(j, i, s) && dp[j] + 1 < dp[i + 1]) { 24 dp[i + 1] = dp[j] + 1; 25 pre = j; 26 } 27 pre = max(0, pre - 1); 28 } 29 30 } 31 return dp[s.size()] - 1; 32 } 33 };
======================= **代碼演示** =======================
1. 動態規划: 746. 使用最小花費爬樓梯

1 class Solution { 2 public: 3 int minCostClimbingStairs(vector<int>& cost) { 4 // int len = cost.size(); 5 // vector<int> dp(len + 1, 0); 6 // dp[0] = cost[0], dp[1] = cost[1]; 7 // cost.push_back(0); 8 // 9 // for(int i = 2; i <= len; ++i) { 10 // dp[i] = min(dp[i - 2], dp[i - 1]) + cost[i]; 11 // } 12 13 //優化空間使用 14 int dp[3] = {cost[0], cost[1], 0}; 15 cost.push_back(0); 16 for(int i = 2, I = cost.size(); i < I; ++i) { 17 int cur = i % 3, pre1 = (i + 3 - 1) % 3, pre2 = (i + 3 - 2) % 3; 18 dp[cur] = min(dp[pre1], dp[pre2]) + cost[i]; 19 } 20 return dp[(cost.size() - 1) % 3]; 21 } 22 };
2. 二維動態規划: 1143. 最長公共子序列

1 class Solution { 2 public: 3 int longestCommonSubsequence(string text1, string text2) { 4 int len1 = text1.size(), len2 = text2.size(); 5 vector<vector<int>> dp(len1 + 1, vector<int>(len2 + 1, 0)); 6 7 for(int i = 1; i <= len1; i++) { 8 for(int j = 1; j <= len2; j++) { 9 dp[i][j] = max(dp[i - 1][j - 1] + (text1[i - 1] == text2[j - 1]), 10 max(dp[i][j - 1], dp[i - 1][j])); 11 } 12 } 13 return dp[len1][len2]; 14 } 15 }; 16 //狀態方程: dp[i][j] 代表text1 長度為i, text2長度為j 的最長公共子序列長度;i 不一定要等於 j 17 //狀態轉移方程: 18 // dp[i][j] = max(dp[i][j - 1], 19 // dp[i -1][j], 20 // dp[i - 1][j - 1] + (text1[i] == text2[j])); //這一組只有在text1[i] == text2[j] 才有必要 21 //
======================= **經典問題** =======================
1. 相鄰互斥,動態過程最優解;

1 class Solution { 2 public: 3 int minCost(vector<vector<int>>& costs) { 4 vector<vector<int>> dp(2, costs[0]); 5 6 for(int i = 1, I = costs.size(); i < I; ++i) { 7 int cur = i % 2, pre = !cur; 8 dp[cur][0] = min(dp[pre][1], dp[pre][2]) + costs[i][0]; 9 dp[cur][1] = min(dp[pre][0], dp[pre][2]) + costs[i][1]; 10 dp[cur][2] = min(dp[pre][0], dp[pre][1]) + costs[i][2]; 11 } 12 13 int idx = (costs.size() - 1) % 2; 14 return min(dp[idx][0], min(dp[idx][1], dp[idx][2])); 15 } 16 };

1 class Solution { 2 public: 3 int rob(vector<int>& nums) { 4 int len = nums.size(); 5 vector<vector<int>> dp(2, vector<int>(2)); //偷/不偷 對應的最大值 6 dp[0][0] = nums[0]; 7 for(int i = 1; i < len; ++i) { 8 int cur = i % 2, pre = !cur; 9 dp[cur][0] = nums[i] + dp[pre][1]; 10 dp[cur][1] = max(dp[pre][0], dp[pre][1]); 11 } 12 len -= 1; 13 return max(dp[len % 2][0], dp[len % 2][1]); 14 } 15 };

1 class Solution { 2 public: 3 int rob(vector<int>& nums) { 4 int n = nums.size(); 5 if(n == 1) return nums[0]; 6 7 int dp1[2][2] = {nums[0]}, dp2[2][2] = {0}; 8 9 //第一家一定偷 10 dp1[0][0] = dp1[0][1] = dp1[1][0] = dp1[1][1] = nums[0]; 11 for(int i = 2; i < n; ++i) { 12 int idx = i % 2, pre = !idx; 13 dp1[idx][0] = dp1[pre][1] + nums[i]; //偷; 14 dp1[idx][1] = max(dp1[pre][0], dp1[pre][1]); //不偷; 15 } 16 //第一家一定不偷 17 for(int i = 1; i < n; ++i) { 18 int idx = i % 2, pre = !idx; 19 dp2[idx][0] = dp2[pre][1] + nums[i]; //偷 20 dp2[idx][1] = max(dp2[pre][0], dp2[pre][1]); //不偷 21 } 22 23 n--; 24 return max(dp1[n % 2][1], max(dp2[n % 2][0], dp2[n % 2][1])); 25 } 26 }; 27 28 //original 29 // 30 class Solution { 31 public: 32 int rob(vector<int>& nums) { 33 //rev2: 存儲空間優化方案 34 int size = nums.size(); 35 if(size == 1) return nums[0]; 36 vector<vector<int>> rob(2, vector<int>(2, 0)); 37 int ret = 0; 38 //最后一家一定不搶 39 rob[0][0] = 0; 40 rob[0][1] = nums[0]; 41 for(int i = 1, I = size - 1; i < I; ++i) { 42 int ind = i % 2, pre_ind = !ind; 43 rob[ind][0] = max(rob[pre_ind][0], rob[pre_ind][1]); 44 rob[ind][1] = rob[pre_ind][0] + nums[i]; 45 } 46 int last_ind = (size - 2) % 2; 47 ret = max(rob[last_ind][0], rob[last_ind][1]); 48 49 //第一家一定不搶 50 rob[0][0] = rob[0][1] = rob[1][0] = rob[1][1] = 0; 51 for(int i = 1; i < size; ++i) { 52 int ind = i % 2, pre_ind = !ind; 53 rob[ind][0] = max(rob[pre_ind][0], rob[pre_ind][1]); 54 rob[ind][1] = rob[pre_ind][0] + nums[i]; 55 } 56 last_ind = (size - 1) % 2; 57 ret = max(ret, max(rob[last_ind][0], rob[last_ind][1])); 58 59 //rev1: 無任何優化方式 60 // int size = nums.size(); 61 // if(size == 1) return nums[0]; 62 // vector<vector<int>> rob(size, vector<int>(2, 0)); 63 // int ret = 0; 64 //// 第一家隨意,最后一家一定不搶 65 // rob[0][0] = 0; 66 // rob[0][1] = nums[0]; 67 // for(int i = 1, I = size - 1; i < I; ++i) { 68 // rob[i][0] = max(rob[i - 1][0], rob[i -1][1]); 69 // rob[i][1] = rob[i -1][0] + nums[i]; 70 // } 71 // ret = max(rob[size -2][0], rob[size -2][1]); 72 ////第一家不搶, 最后一家隨意 73 // rob[0][1] = 0; 74 // for(int i = 1; i < size; ++i) { 75 // rob[i][0] = rob[i][1] = 0; 76 // rob[i][0] = max(rob[i - 1][0], rob[i - 1][1]); 77 // rob[i][1] = rob[i -1][0] + nums[i]; 78 // } 79 // 80 // ret = max(ret, max(rob[size -1][0], rob[size -1][1])); 81 return ret; 82 } 83 };

1 class Solution { 2 public: 3 int maxProduct(vector<int>& nums) { 4 int ans = INT_MIN, max_val = 1, min_val = 1; 5 6 for(auto &x : nums) { 7 if(x < 0) swap(max_val, min_val); 8 max_val = max(max_val * x, x); 9 min_val = min(min_val * x, x); 10 ans = max(ans, max_val); 11 } 12 return ans; 13 } 14 }; 15 16 //f(n) 以當前位置結尾的最大/最小值
2.最長上升子序列:

1 class Solution { 2 public: 3 int bs_01(vector<int>& arr, int num) { 4 int l = 0, r = arr.size(), mid; 5 while(l < r) { 6 mid = (l + r) >> 1; 7 if(arr[mid] < num) l = mid + 1; 8 else r = mid; 9 } 10 return l; 11 } 12 13 int lengthOfLIS(vector<int>& nums) { 14 //優化解法 15 vector<int> arr; 16 for(auto &x : nums) { 17 if(arr.empty() || arr.back() < x) arr.push_back(x); 18 else arr[bs_01(arr, x)] = x; 19 } 20 return arr.size(); 21 22 //基礎解法dp 23 // vector<int> dp(nums.size(), 0); 24 // map<int,int> m; 25 // int ans = 0; 26 // for(int i = 0, I = nums.size(); i < I; ++i) { 27 // m[nums[i]] = i; 28 // auto it = m.find(nums[i]); 29 // while(it != m.begin()) dp[i] = max(dp[i], dp[(--it)->second]); 30 // dp[i] += 1; 31 // ans = max(ans, dp[i]); 32 // } 33 // return ans; 34 //dp(n) 以n 為結尾的子數組長度; 35 } 36 };

1 class Solution { 2 public: 3 int maxProfit(vector<int>& prices, int fee) { 4 int n = prices.size(); 5 vector<vector<int>> dp(2, vector<int>(2, 0)); //第i 天 持有/不持有 6 dp[0][0] = -prices[0], dp[0][1] = 0; 7 for(int i = 1; i < n; ++i) { 8 int idx = i % 2, pre = !idx; 9 dp[idx][0] = max(dp[pre][0], dp[pre][1] - prices[i]);//持有 10 dp[idx][1] = max(dp[pre][0] + prices[i] - fee, dp[pre][1]);//不持有 11 } 12 13 return dp[!(n % 2)][1]; 14 } 15 };
3. 典型的0/1 背包問題; 資源有限的情況下收益最大化問題; 對於當前值,就兩種可能,要么選,要么不選;
416. 分割等和子集 :可達數組類型, 倒着刷表

1 class Solution { 2 public: 3 4 bool canPartition(vector<int>& nums) { 5 int total = 0; 6 for(auto &x : nums) total += x; 7 if(total % 2) return false; 8 total /= 2; 9 10 //dp[i][j] : 前i個數字,能否湊出j值; 11 //dp[i][j] : dp[i - 1][j] | dp[i - 1][j - nums[i] 12 //優化:當前i狀態 只與前面所有可能到達狀態相關, 13 // 從后往前,保證當前i狀態 由 所有前面的狀態決定 14 vector<bool> dp(total + 1, false); 15 dp[0] = true; 16 17 for(auto &x : nums){ 18 for(int i = total - x; i >= 0; --i) { 19 if(dp[i]) dp[i + x] = true; 20 } 21 if(dp[total]) return true; 22 } 23 return false; 24 } 25 };

1 class Solution { 2 public: 3 int findMaxForm(vector<string>& strs, int m, int n) { 4 //dp[i][m][n] : 第i 個str, 取m 個 0, n 個 1 的最多子集樹; 5 //dp[i][m][n] = max(dp[i - 1][m][n] , dp[i - 1][n - cnt0][n - cnt1]) //選 , 不選對應可能 6 //優化:當前i 結果只與 前面所有可能狀態相關; 注意方向,保證是取 i 前面可能狀態; 7 8 vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0)); //i 個0, j 個1 的有可能取到最多子集數; 9 10 for(int i = 0, I = strs.size(); i < I; ++i) { 11 int idx = 0, cnt0 = 0, cnt1 = 0; 12 while(strs[i][idx]) cnt0 += (strs[i][idx++] == '0'); 13 cnt1 = strs[i].size() - cnt0; 14 15 for(int x = m; x >= cnt0; --x) { 16 for(int y = n; y >= cnt1; --y) { 17 dp[x][y] = max(dp[x][y], dp[x - cnt0][y - cnt1] + 1); 18 } 19 } 20 } 21 return dp[m][n]; 22 } 23 };
494. 目標和 // 到哪里去

1 class Solution { 2 public: 3 int findTargetSumWays(vector<int>& nums, int target) { 4 //Rev2: 滾動數組/可達數組/偏移量 5 int sum = 0; 6 for(auto &x : nums) sum += x; 7 if(target > sum || target < -sum) return 0; 8 //偏移量,滾動數組 9 int buff[2][2 * sum + 5], *f[2] = {buff[0] + sum + 2, buff[1] + sum + 2}; 10 memset(buff, 0, sizeof(buff)); 11 f[1][0] = 1; 12 sum = 0; 13 14 for(int i = 0, I = nums.size(); i < I; ++i) { 15 int cur = i % 2, pre = !cur; 16 memset(buff[cur], 0, sizeof(buff[cur])); 17 for(int j = -sum; j <= sum; ++j) { 18 f[cur][j + nums[i]] += f[pre][j]; 19 f[cur][j - nums[i]] += f[pre][j]; 20 } 21 sum += nums[i]; 22 } 23 24 int idx = !(nums.size() % 2); 25 return f[idx][target]; 26 27 //Rev1: 滾動數組/可達數組 28 // vector<unordered_map<int,int>> dp(2); 29 // //i位置可以組成的和為nums的個數 30 // dp[0][nums[0]] += 1, dp[0][-nums[0]] += 1; 31 // for(int i = 1, I = nums.size(); i < I; ++i) { 32 // int idx = i % 2, pre = !idx; 33 // dp[idx].clear(); 34 // for(auto &x : dp[pre]) { 35 // //我到哪里去 36 // dp[idx][x.first + nums[i]] += x.second; 37 // dp[idx][x.first - nums[i]] += x.second; 38 // } 39 // } 40 // 41 // int idx = !(nums.size() % 2); 42 // return dp[idx][target]; 43 } 44 };
2218. 從棧中取出 K 個硬幣的最大面值和 :典型的分組背包問題;

1 class Solution { 2 public: 3 int maxValueOfCoins(vector<vector<int>>& piles, int k) { 4 //k為背包容量,piles 處理成每堆有那些重量,價值多少的物品,且每堆只能取一件物品 5 //dp[i][j] : 前i 堆中 取容量為j 的最大值; 6 int len = piles.size(), dp[len + 1][k + 1]; 7 memset(dp, 0, sizeof(dp)); 8 vector<vector<int>> val(len,vector<int>(1, 0)); 9 for(int i = 0; i < len; ++i) { 10 for(int j = 0, J = piles[i].size(); j < J; j++) { 11 val[i].push_back(val[i].back() + piles[i][j]); 12 } 13 } 14 15 for(int i = 0; i < len; ++i) { 16 for(int j = 1; j <= k; ++j) { 17 int n_i = i + 1, x = 0; 18 while(x <= j && x < val[i].size()) { 19 dp[n_i][j] = max(dp[n_i][j], dp[i][j - x] + val[i][x]); 20 x++; 21 } 22 } 23 } 24 return dp[len][k]; 25 } 26 };
還有多重背包,完全背包;
4. 湊硬幣類型:
322. 零錢兌換: 正着刷表

1 class Solution { 2 public: 3 int coinChange(vector<int>& coins, int amount) { 4 //優化版本 5 vector<int> dp(amount + 5, -1); //總數為i時候,需要的硬幣總數 6 dp[0] = 0; 7 for(auto &x : coins) { //使用前n 種硬幣可以組成的各個金額總數 8 for(int i = x; i <= amount; ++i) { 9 if(-1 == dp[i - x]) continue; 10 if(-1 == dp[i] || dp[i] > dp[i - x] + 1) dp[i] = dp[i - x] + 1; 11 } 12 } 13 return dp[amount]; 14 15 //初始版本, 而且下面兩個循環之間互換,有些情況下影響很大,例如518題 16 // sort(coins.begin(), coins.end()); 17 // vector<int> nums; 18 // for(auto &x : coins){ //減枝過程,可優化 19 // if(x > amount) break; 20 // nums.push_back(x); 21 // } 22 // 23 // for(int i = 0; i <= amount; i++) { 24 // if(dp[i] == -1) continue; 25 // for(auto &x : nums) { 26 // int val = x + i; 27 // if(val > amount) break; 28 // if(dp[val] == -1) dp[val] = dp[i] + 1; //可優化 29 // else dp[val] = min(dp[val], dp[i] + 1); 30 // } 31 // } 32 // return dp[amount]; 33 } 34 };
518. 零錢兌換 II : 遞推/正向刷表;

1 class Solution { 2 public: 3 int change(int amount, vector<int>& coins) { 4 //dp[i][j] = dp[i - 1][j] + dp[i][j - x], 可優化成下面正向刷表; 5 vector<int> dp(amount + 1, 0); //i 金額對應的硬幣組合個數 6 dp[0] = 1; 7 8 for(auto &x : coins) { //使用前 x種硬幣時候,各個金額可能的組合 9 for(int i = x; i <= amount; ++i) { 10 dp[i] += dp[i - x]; 11 } 12 } 13 return dp[amount]; 14 } 15 };
377. 組合總和 Ⅳ : 這題與322, 518 三題綜合起來看,包含了最佳,不計順序,計算順序的不同要求對應的不同操作;

1 class Solution { 2 public: 3 int combinationSum4(vector<int>& nums, int target) { 4 vector<unsigned int> dp(target + 1, 0); 5 sort(nums.begin(), nums.end()); 6 7 dp[0] = 1; 8 //到哪里去 9 for(int i = 0; i <= target; ++i) { //當前金額可以實現哪些新的金額 10 if(!dp[i]) continue; 11 for(auto &x : nums) { 12 int val = i + x; 13 if(val > target) break; 14 dp[val] += dp[i]; 15 } 16 } 17 return dp[target]; 18 } 19 }; 20 21 //original 22 class Solution { 23 public: 24 int combinationSum4(vector<int>& nums, int target) { 25 vector<unsigned int> cnt(target + 1, 0); 26 cnt[0] = 1; 27 sort(nums.begin(), nums.end()); 28 //從哪里來 29 for(int i = 1; i <= target; ++i) { //當前金額,可以由哪些金額組成 30 for(auto &x : nums) { 31 if(x > i) break; 32 cnt[i] += cnt[i - x]; 33 } 34 } 35 36 return cnt[target]; 37 } 38 };
======================= **應用場景** =======================