- 0-1揹包問題(二維dp)
- 0-1揹包升級版(二維dp)
- 完全揹包(費解)如湊領錢(一維、二維dp)
- 子序列問題(重要)
- 最長遞增子序列(一維dp)
- 最長公共子序列(二維dp)
- 最長迴文子序列(二維dp)
- 最短編輯距離(二維dp)
- 最短路徑(機器人走路)(二維dp)
第一步要明確兩點,「狀態」和「選擇」。明確dp
數組的定義
狀態有兩個:「揹包的容量」、「可選擇的物品」
選擇有兩個:「裝進揹包」、「不裝進揹包」
幾種狀態就是幾層for循環,也就是幾維dp
第二步,根據「選擇」,思考狀態轉移的邏輯
第三步,確定初始條件
labuladong的動歸
例題一:0-1揹包升級版
給你一個可裝載重量爲
W
的揹包和N
個物品,每個物品有重量和價值兩個屬性。其中第i
個物品的重量爲wt[i]
,價值爲val[i]
,現在讓你用這個揹包裝物品,最多能裝的價值是多少?
- 定義二維
dp[i][j]
:對於前i種物品,當前揹包重量爲j時,能夠獲得的最大價值爲dp[i][j]
。我們要求的就是dp[n][w]
- 數組元素之間的關係:
- 當
j - wt[i - 1] < 0
時:dp[i][j] = dp[i - 1][j]
。表示當前剩餘的容量裝不下當前的物品,只能繼承上一個裝填的 - 當
j - wt[i - 1] >= 0
時:裝或者不裝。dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - wt[i - 1]] + val[i - 1])
- 當
- 數組元素之間的關係:
dp[i] = dp[i - 1] + dp[i - 2]
- 初始條件:
dp[...][0] = 0;dp[0][...] = 0
。表示物品或者容量爲0時,當前價值爲0
// 經典動態規劃:0-1揹包問題
int baseDP(int w, int n, int weight[], int value[]){
// 定義二維狀態數組
int dp[n + 1][w + 1];
// 初始化邊界
for(int i = 0; i <= n; i++)
dp[i][0] = 0;
for(int i = 0; i <= w; i++)
dp[0][i] = 0;
for(int i = 1; i <= n; i++){
for(int j = 1; j <= w; j++){
if(j - weight[i - 1] < 0)
// 裝不下,直接繼承前一個狀態的
dp[i][j] = dp[i - 1][j];
// dp[i][j] = 擇優(選擇1, 選擇2)
// 揹包裝或者不裝,兩者擇優選擇
else
// 這個地方如果是一維數組的話就是倒着來的
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i - 1]] + value[i - 1]);
}
}
return dp[n][w];
}
例題二:0-1揹包變體
給定一個只包含正整數的非空數組。是否可以將這個數組分割成兩個子集,使得兩個子集的元素和相等。
注意:每個數組中的元素不會超過 100;數組的大小不會超過 200
示例 1:輸入: [1, 5, 11, 5]
輸出: true
解釋: 數組可以分割成 [1, 5, 5] 和 [11].
- **定義
dp[i][j]
:對於前i個物品,當前揹包的容量爲j時,若dp[i][j]
爲true,則說明可以裝滿。我們要求的就是dp[N][sum/2]
** - 數組元素之間的關係:
- 當
j - wt[i - 1] < 0
時:dp[i][j] = dp[i - 1][j]
。表示當前剩餘的容量裝不下當前的物品,只能繼承上一個裝填的 - 當
j - wt[i - 1] >= 0
時:dp[i][j] = dp[i - 1][j] || dp[i - 1][j - wt[i - 1]]
- 當
- 初始條件:初始條件:
dp[...][0] = true;dp[0][...] = false
。表示物品爲0,價值不爲0時,肯定裝不滿
class Solution {
public:
bool ans = false;
// 這種方法超時
void dfs(vector<int> &num, vector<int> a, vector<int> b, int index){
if(index == num.size()){
int sumA = 0, sumB = 0;
for(int i = 0; i < a.size(); i++)
sumA += a[i];
for(int i = 0; i < b.size(); i++)
sumB += b[i];
if(sumA == sumB)
ans = true;
return ;
}
// 放入A揹包
a.push_back(num[index]);
dfs(num, a, b, index + 1);
a.pop_back();
b.push_back(num[index]);
dfs(num, a, b, index + 1);
b.pop_back();
}
bool canPartition(vector<int>& nums) {
// 定義狀態數組
int sum = 0;
for(int i : nums)
sum += i;
if(sum % 2 != 0)
return false;
int n = nums.size();
sum = sum / 2;
bool dp[n + 1][sum + 1];
// 初始化
for(int i = 0; i <= n; i++)
dp[i][0] = true;
for(int i = 0; i <= sum; i++)
dp[0][i] = false;
for(int i = 1; i <= n; i++){
for(int j = 1; j <= sum; j++){
if(j - nums[i - 1] < 0)
dp[i][j] = dp[i - 1][j];
else
// 這個地方如果是一維數組的話就是倒着來的
dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i - 1]];
}
}
return dp[n][sum];
}
};
例題三:完全揹包問題
湊領錢1:給你
k
種面值的硬幣,面值分別爲c1, c2 ... ck
,每種硬幣的數量無限,再給一個總金額amount
,問你最少需要幾枚硬幣湊出這個金額,如果不可能湊出,算法返回 -1 。
- 定義一維數組
dp[i]
:當前總金額爲i時,需要最少dp[i]
個硬幣湊出這個金額。我們要求的就是dp[amount]
- 數組元素之間的關係:
dp[i] = min(dp[i - coin] + 1)
- 初始條件:
dp[0] = 0
。即金額爲0就需要0枚硬幣
int coinChangeDP(vector<int> &coins, int amount){
// 初始化備忘錄
vector<int> dp(amount + 1, amount + 1);
dp[0] = 0;
// 填表
for(int i = 1; i < dp.size(); i++){
// 內層循環,找最小
for(int coin : coins){
if(i - coin < 0)
continue;
dp[i] = min(dp[i], dp[i - coin] + 1);
}
}
return (dp[amount] == amount + 1) ? -1 : dp[amount];
}
湊領錢2:給定不同面額的硬幣和一個總金額,寫出函數來計算可以湊成總金額的硬幣組合數,假設每種面額的硬幣有無限個。
- 定義二維數組
dp[i][j]
:當前總金額爲j時,前i個物品可能有dp[i][j]
種可能湊齊。我們要求的就是dp[n][amount]
- 數組元素之間的關係:
- 當
j-coins[i - 1] < 0
時:dp[i][j] = dp[i - 1][j]
。表示當前容量裝不下當前的硬幣,只能繼承上一個狀態的 - 當
j-coins[i - 1] >= 0
時:dp[i][j] = dp[i - 1][j] + dp[i][j - coins[i - 1]]
- 當
- 初始條件:
dp[i][0] = 1;dp[0][i] = 0;
int change(int amount, vector<int>& coins) {
if(amount == 0 && coins.size() == 0)
return 1;
int n = coins.size();
int dp[n + 1][amount + 1];
// 初始化
for(int i = 0; i <= n; i++)
dp[i][0] = 1;
for(int i = 0; i <= amount; i++)
dp[0][i] = 0;
for(int i = 1; i <= n; i++){
for(int j = 1; j <= amount; j++){
if(j - coins[i - 1] < 0)
dp[i][j] = dp[i - 1][j];
else
// 注意這裏是i不是i-1了,這樣就保證了物品可以選無數次,如果是i-1的話,就是普通揹包,只能選一次
// 這個地方如果用一維數組,那麼就是順着來的
dp[i][j] = dp[i - 1][j] + dp[i][j - coins[i - 1]];
}
}
return dp[n][amount];
}
例題四:最長公共子序列
解決兩個字符串的動態規劃問題,一般都是用兩個指針i,j
分別指向兩個字符串的最後,然後一步步往前走,縮小問題的規模。都是建立一個二維的dp數組。
求兩個字符串的 LCS 長度:
輸入: str1 = "abcde", str2 = "ace" 輸出: 3 解釋: 最長公共子序列是 "ace",它的長度是 3
- 定義二維
dp[i][j]
:表示str1
的(0, i)子序列與str2
的(0, j)子序列的最長公共序列。我們要求的就是dp[m][n]
- 數組元素之間的關係:
- 當
str1[i] = str2[j]
時:dp[i][j] = dp[i - 1][j - 1] + 1
- 當
str1[i] != str2[j]
時:dp[i][j] = max(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1])
- 當
- 初始條件:
dp[...][0] = dp[0][...] = 0
int myMax(int a, int b, int c){
return max(max(a, b), c);
}
int longestComStr(string s1, string s2){
int m = s1.size(), n = s2.size();
int dp[m + 1][n + 1];
// 初始化
for(int i = 0; i <= m; i++)
dp[i][0] = 0;
for(int i = 0; i <= n; i++)
dp[0][i] = 0;
for(int i = 1; i <= m; i++){
for(int j = 1; j <= n; j++){
if(s1[i - 1] == s2[j - 1])
dp[i][j] = dp[i - 1][j - 1] + 1;
else
// 這個地方寫成dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])就行了
dp[i][j] = myMax(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
}
}
return dp[m][n];
}
例題五:求兩個字符串的最小編輯距離
和上一個題一樣,一般來說,處理兩個字符串的動態規劃問題,都是按本文的思路處理,建立 DP table。爲什麼呢,因爲易於找出狀態轉移的關係。這裏的dp(i)(j)數組表示的是 s1[0..i] 和 s2[0..j] 的最小編輯距離。
- 定義二維數組
dp[i][j]
:當字符串s1長度爲i,字符串s2長度爲j時,它們的最短編輯距離是dp[i][j]
- 數組元素之間的關係:
- 當
s1[i - 1] == s2[j - 1]
時:dp[i][j] = dp[i - 1][j - 1]
- 當
s1[i - 1] != s2[j - 1]
時:dp[i][j] = min(dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]) + 1
- 當
- 初始條件:
dp[i][0] = i;dp[0][i] = i
int min(int a, int b, int c){
return min(min(a, b), c);
}
int minDistance(string s1, string s2){
int m = s1.size(), n = s2.size();
int dp[m + 1][n + 1];
// 初始化
for(int i = 1; i <= m; i++)
dp[i][0] = i;
for(int i = 1; i <= n; i++)
dp[0][i] = i;
for(int i = 1; i <= m; i++){
for(int j = 1;j <= n; j++){
if(s1[i - 1] == s2[j - 1])
dp[i][j] = dp[i - 1][j - 1];
else{
dp[i][j] = min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + 1);
}
}
}
// 儲存着整個s1和s2的最小編輯距離
return dp[m][n];
}
總結子序列問題模板
首先注意區分一個問題:
- 子序列:可以不連續的子字符串/子數組
- 子串:必須是連續的子字符串/子數組
遇到子序列問題,首先想到兩種動態規劃思路,然後根據實際問題看看哪種思路容易找到狀態轉移關係。
這類問題都是讓你求一個最長子序列,因爲最短子序列就是一個字符嘛,沒啥可問的。一旦涉及到子序列和最值,那幾乎可以肯定,考察的是動態規劃技巧,時間複雜度一般都是 O(n^2)。
1 第一種思路模板是一維的 dp 數組
最長遞增子序列(注意是序列,可以不連續)
- 定義一維
dp[i]
:數組中以num[i]
結尾的最長遞增序列爲dp[i]
。我們要求的就是所有的dp[i]
中最大的那一個 - 數組元素之間的關係:
dp[i] = max(dp[i], dp[j] + 1)
,其中num[j] < num[i]
- 初始條件:
dp[...] = 1
,保證最短爲1
int lengthOfLIS(int nums[], int n){
vector<int> dp(n, 1);
for(int i = 0; i < n; i++){
for(int j = 0; j < i; j++){
if(nums[j] < nums[i])
dp[i] = max(dp[i], dp[j] + 1);
}
}
int ans = INT_MIN;
for(int i = 0; i < n; i++)
ans = max(ans, dp[i]);
return ans;
}
2 第二種思路模板是二維的 dp 數組
這種思路數組含義又分爲「只涉及一個字符串」和「涉及兩個字符串」兩種情況
2.1 涉及兩個字符串/數組時
- 最長公共子序列
- 最短編輯距離
2.2 涉及一個字符串/數組時
最長迴文子序列(注意,和最長迴文子串不一樣,子序列可以不連續)
- 定義二維
dp[i][j]
數組:在子串s[i..j]
中,最長迴文子序列的長度爲dp[i][j]
。我們要求的就是dp[0][n - 1]
- 數組元素之間的關係
- 當
s[i] = s[j]
時:dp[i][j] = dp[i + 1][j - 1] + 2
- 當
s[i] != s[j]
時:dp[i][j] = max(dp[i + 1][j], dp[i][j - 1])
- 當
- 初始條件:
dp[i][i] = 1
爲了保證每次計算dp[i][j]
,左、下、左下三個方向的位置已經被計算出來,只能斜着遍歷或者反着遍歷,本例選擇反着遍歷:
// 反着遍歷
int longestPalindromeSubseq(string s){
int n = s.size();
int dp[n][n];
// 初始化
memset(dp, 0, sizeof(dp));
for(int i = 0; i < n; i++)
dp[i][i] = 1;
for(int i = n - 1; i >= 0; i--){
for(int j = i + 1; j < n; j++){
if(s[i] == s[j])
dp[i][j] = dp[i + 1][j - 1] + 2;
else
dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
}
}
// 返回整個s的最長迴文子序列長度
return dp[0][n - 1];
}
帥地的動歸
1 一維dp
例題一:青蛙跳臺階
一隻青蛙一次可以跳上1級臺階,也可以跳上2級臺階。求該青蛙跳上一個n級臺階總共有多少種跳法?
- 定義一維
dp[i]
:跳上一個i
級的臺階共有dp[i]
種跳法,我們要求的就是dp[n]
- 數組元素之間的關係:
dp[i] = dp[i - 1] + dp[i - 2]
- 初始條件:
dp[0] = 0;dp[1] = 1;dp[2] = 2
完整代碼
// 跳臺階問題
// dp[n]表示跳上一個n階臺階共有dp[n]種跳法
int f(int n){
if(n <= 2)
return n;
int dp[n + 1];
dp[0] = 0;
dp[1] = 1;
dp[2] = 2;
for(int i = 3; i <= n; i++)
dp[i] = dp[i - 1] + dp[i - 2];
return dp[n];
}
2 二維dp
例題二:機器人走路(不含權值)
⼀個機器⼈位於⼀個 m x n ⽹格的左上⻆ (起始點在下圖中標記爲“Start” )。
機器⼈每次只能向下或者向右移動⼀步。機器⼈試圖達到⽹格的右下⻆(在下圖中標記爲“Finish”)。
問總共有多少條不同的路徑?
- 定義二維
dp[i][j]
:當機器人從左上角走到(i, j)這個位置,共有dp[i][j]
種路徑,我們要求的就是dp[m - 1][n - 1]
- 數組元素之間的關係:
dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
- 初始條件:
dp[...][0] = 1;dp[0][...] = 1
,因爲第一行只能往左走,第一列只能往下走
完整代碼
// 機器人走路(無路徑權值)
// dp[i][j]表示當機器人從左上角走到(i,j)這個位置時,一共有dp[i][j]種路徑
int f(int m, int n){
if(m < 0 || n < 0)
return 0;
int dp[m][n];
for(int i = 0; i < m; i++)
dp[i][0] = 1;
for(int i = 0; i < n; i++)
dp[0][i] = 1;
for(int i = 1; i < m; i++){
for(int j = 1; j < n; j++)
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
return dp[m - 1][n - 1];
}
例題三:機器人走路的最短路徑(含權值)
給定⼀個包含⾮負整數的 m x n ⽹格,請找出⼀條從左上⻆到右下⻆的路徑,使得路徑上的數字總和爲最⼩。
- 定義二維
dp[i][j]
:當機器人從左上角走到(i, j)這個位置的最短路徑,我們要求的就是dp[m - 1][n - 1]
- 數組元素之間的關係:
dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + val[i][j]
- 初始條件:
dp[i][0] = dp[i - 1][0] + val[i][0];dp[0][i] = dp[0][i - 1] + val[0][i]
完整代碼
// 機器人走路,有路徑權值
// dp[i][j]表示機器人從左上角走到(i,j)這個位置的最短路徑值
int f(int val[][], int m, int n){
int dp[m][n];
dp[0][0] = val[0][0];
for(int i = 1; i < m; i++)
dp[i][0] = dp[i - 1][0] + val[i][0];
for(int i = 1; i < n; i++)
dp[0][i] = dp[0][i - 1] + val[0][i - 1];
for(int i = 1; i < m; i++){
for(int j = 1; j < n; j++){
dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + val[i][j];
}
}
return dp[m - 1][n - 1];
}
例題四:最短編輯距離
給定兩個單詞 word1 和 word2,計算出將 word1 轉換成 word2 所使⽤的最少操作數 。
你可以對⼀個單詞進⾏如下三種操作:插⼊⼀個字符 刪除⼀個字符 替換⼀個字符
- 定義二維數組
dp[i][j]
:當字符串s1長度爲i,字符串s2長度爲j時,它們的最短編輯距離是dp[i][j]
- 數組元素之間的關係:
- 當
s1[i - 1] == s2[j - 1]
時:dp[i][j] = dp[i - 1][j - 1]
- 當
s1[i - 1] != s2[j - 1]
時:dp[i][j] = min(dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]) + 1
- 當
- 初始條件:
dp[i][0] = i;dp[0][i] = i
完整代碼
int min(int a, int b, int c){
return min(min(a, b), c);
}
int minDistance(string s1, string s2){
int m = s1.size(), n = s2.size();
int dp[m + 1][n + 1];
// 初始化
for(int i = 1; i <= m; i++)
dp[i][0] = i;
for(int i = 1; i <= n; i++)
dp[0][i] = i;
for(int i = 1; i <= m; i++){
for(int j = 1;j <= n; j++){
if(s1[i - 1] == s2[j - 1])
dp[i][j] = dp[i - 1][j - 1];
else{
dp[i][j] = min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + 1);
}
}
}
// 儲存着整個s1和s2的最小編輯距離
return dp[m][n];
}
王爭老師的動歸
例題一:0-1揹包問題
//weight:物品重量,n:物品個數,w:揹包可承載重量
// 二維dp
public int knapsack(int[] weight, int n, int w) {
boolean[][] states = new boolean[n][w+1]; // 默認值false
states[0][0] = true; // 第一行的數據要特殊處理,可以利用哨兵優化
if (weight[0] <= w) {
states[0][weight[0]] = true;
}
for (int i = 1; i < n; ++i) { // 動態規劃狀態轉移
for (int j = 0; j <= w; ++j) {// 不把第i個物品放入揹包
if (states[i-1][j] == true) states[i][j] = states[i-1][j];
}
for (int j = 0; j <= w-weight[i]; ++j) {//把第i個物品放入揹包
if (states[i-1][j]==true) states[i][j+weight[i]] = true;
}
}
for (int i = w; i >= 0; --i) { // 輸出結果
if (states[n-1][i] == true) return i;
}
return 0;
}
// 一維dp
public static int knapsack2(int[] items, int n, int w) {
boolean[] states = new boolean[w+1]; // 默認值false
states[0] = true; // 第一行的數據要特殊處理,可以利用哨兵優化
if (items[0] <= w) {
states[items[0]] = true;
}
for (int i = 1; i < n; ++i) { // 動態規劃
for (int j = w-items[i]; j >= 0; --j) {//把第i個物品放入揹包
if (states[j]==true) states[j+items[i]] = true;
}
}
for (int i = w; i >= 0; --i) { // 輸出結果
if (states[i] == true) return i;
}
return 0;
}