[LeetCode] 動態規劃題型總結
寫在前面
動態規劃在網際網路公司的筆試題中經常會使大題的壓軸題,解動態規劃題的關鍵是定義動態規劃變數和寫出狀態轉移方程,本篇部落格主要探討動態規劃題型解法,最後也會介紹動態規劃與其他演算法知識結合的題。
文章目錄
152. 乘積最大子陣列
給你一個整數陣列 nums ,請你找出陣列中乘積最大的連續子陣列(該子陣列中至少包含一個數字),並返回該子陣列所對應的乘積。
解題思路: 此題很容易受連續子陣列最大和值 題的影響,以當前元素結尾序列的最大和值只取決於當前值、當前值+前一序列最大和,但是對於乘法,因為一個數a乘上數b,若數a是最大值,乘上數b後,若數b為正數,那麼依然能保證是最大值,但是若數b為負數,反而會成為最小值,因此,我們會發現求當前位置的最大乘積值,要同時關心前一個位置的最大乘積值和最小乘積值,此題屬於雙動態規劃題,定義動態規劃變數mx[i]
表示以nums[i]
為結尾的最大乘積值,mn[i]
表示以nums[i]
為結尾的最小乘積值,狀態轉移方程如下:
mn[i] = min(nums[i], min(mn[i - 1] * nums[i], mx[i - 1] * nums[i]));
mx[i] = max(nums[i], max(mn[i - 1] * nums[i], mx[i - 1] * nums[i]));
// DP,以nums[i]為結尾的序列最大乘積由nums[i],MIN[i-1]*nums[i],MAX[i-1]*nums[i]
// 決定且取最大值,而最小乘積也有三者決定且取最小值
// T: O(n), space: O(n)
class Solution {
public:
int maxProduct(vector<int>& nums) {
int n = nums.size();
int res = nums[0];
vector<int> mn(n), mx(n);
mn[0] = nums[0];
mx[0] = nums[0];
for (int i = 1; i < n; ++i) {
mn[i] = min(nums[i], min(mn[i - 1] * nums[i], mx[i - 1] * nums[i]));
mx[i] = max(nums[i], max(mn[i - 1] * nums[i], mx[i - 1] * nums[i]));
res = max(res, mx[i]);
}
return res;
}
};
44. 萬用字元匹配
給定一個字串 (s) 和一個字元模式 § ,實現一個支援 ‘?’ 和 ‘*’ 的萬用字元匹配。
'?' 可以匹配任何單個字元。
'*' 可以匹配任意字串(包括空字串)。
兩個字串完全匹配才算匹配成功。
說明:
- s 可能為空,且只包含從 a-z 的小寫字母。
- p 可能為空,且只包含從 a-z 的小寫字母,以及字元 ? 和 *。
解題思路: 此題比較適合用動態規劃解題,因為兩個字串匹配可以轉化為兩個字串字首子串匹配的問題(將問題化成子問題,典型的動態規劃特徵),由於要考慮空串,因此定義dp[i][j]
為s前i個字元和p前j個字元是否匹配,而與狀態(i,j)關聯的有三個狀態,即(i-1,j),(i-1,j-1),(i,j-1),因此,動態轉移方程:
dp[i][j] = dp[i][j-1] || dp[i][j-1] || dp[i][j-1]
注:動態規劃解題的關鍵是,找出與當前狀態關聯的其他狀態(完備且互斥),然後思考與他們的關聯(即狀態轉變)。
// DP, dp[i][j]表示s前i個字元與p前j個字元匹配
// dp[i][j]下面兩項決定:
// 1. p[j-1]='*'時,dp[i-1][j] || dp[i][j-1]
// 2. s[i-1]==p[j-1] || p[j]=='?' || p[j] == '*'時,dp[i-1][j-1]
// time&space: O(n*m)
class Solution {
public:
bool isMatch(string s, string p) {
int n = s.size(), m = p.size();
vector<vector<bool>> dp(n + 1, vector<bool>(m + 1, false));
dp[0][0] = true;
for (int i = 0; i < m; ++i) {
if (p[i] == '*') dp[0][i + 1] = true;
else break;
}
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
if (p[j - 1] == '*') dp[i][j] = dp[i][j] || dp[i - 1][j] || dp[i][j - 1];
if (s[i - 1] == p[j - 1] || p[j - 1] == '?' || p[j - 1] == '*') {
dp[i][j] = dp[i][j] || dp[i - 1][j - 1];
}
}
}
return dp[n][m];
}
};
91. 解碼方法
解題思路: 分析題特徵,若想知道整個字串的解碼個數,是不是可以考慮若知道某個字首解碼個數,然後每次解碼選取的數只能在[1,26]範圍內,因此要麼選一個字元,要麼選兩個字元組合,選一個字元時要求不能為’0’,選兩個字元組合時,要求形成的數在[10,26]範圍內,這種若求一個狀態可求另一個(子)狀態的題最適合用Dynamic Programming解題,狀態轉移方程好寫,dp[i]=dp[i-1]+dp[i-2]
,dp[i]
表示前i個字元組成的字串有多少種解碼,對於長度為0的dp[0]我們選擇初始化它為1,這裡主要是考慮當s[0]和s[1]組合的值恰好在[10,26]範圍內,那這個組合值算1種解法。此題的corner case應該是與0相關的測試點,0在前或者後。
// DP, dp[i]=dp[i-1]+dp[i-2],考慮與'0'相關的corner case
// T&S: O(n)
class Solution {
public:
int numDecodings(string s) {
if (s[0] == '0') return 0;
int n = s.size();
vector<int> dp(n + 1);
dp[0] = 1;
dp[1] = 1;
for (int i = 2; i <= n; ++i) {
if (s[i - 1] != '0') dp[i] = dp[i - 1];
int num = 10 * (s[i - 2] - '0') + s[i - 1] - '0';
if (num >= 10 && num <= 26) dp[i] += dp[i - 2];
}
return dp[n];
}
};
639. 解碼方法 2
解題思路: 與題91不同的是,本題引入了'*'
,可以當做1-9,但是解題的本質與題91是相同的,增添的工作量僅僅是需要更多種情況的分類討論,在DP基礎上寫好此題的關鍵是,假設當前位置是i,分類討論s[i-1]
和s[i]
是否為'*'
的2*2種情況。OK,看下面程式碼。
class Solution {
public:
int numDecodings(string s) {
if (s[0] == '0') return 0;
if (s.size() == 1) {
if (s[0] == '*') return 9;
else return 0;
}
int n = s.size();
long M = 1e9+7;
vector<long> dp(n + 1, 0);
dp[0] = 1;
dp[1] = s[0] == '*' ? 9 : 1;
for (int i = 2; i <= n; ++i) {
if (s[i - 1] == '*') {
dp[i] = (dp[i] + dp[i - 1] * 9) % M;
if (s[i - 2] != '*') {
for (int j = 1; j <= 9; ++j) {
int num = (s[i - 2] - '0') * 10 + j;
if (num >= 10 && num <= 26) dp[i] = (dp[i] + dp[i - 2]) % M;
}
} else {
for (int k = 1; k <= 9; ++k) {
for (int j = 1; j <= 9; ++j) {
int num = k * 10 + j;
if (num >= 10 && num <= 26) dp[i] = (dp[i] + dp[i - 2]) % M;
}
}
}
} else {
if (s[i - 2] == '*') {
if (s[i - 1] != '0') dp[i] = (dp[i] + dp[i - 1]) % M;
for (int j = 1; j <= 9; ++j) {
int num = j * 10 + s[i - 1] - '0';
if (num >= 10 && num <= 26) dp[i] = (dp[i] + dp[i - 2]) % M;
}
} else {
if (s[i - 1] != '0') dp[i] = (dp[i] + dp[i - 1]) % M;
int num = (s[i - 2] - '0') * 10 + s[i - 1] - '0';
if (num >= 10 && num <= 26) dp[i] = (dp[i] + dp[i - 2]) % M;
}
}
}
return dp[n];
}
};
97. 交錯字串
解題思路: 此題起初的想法是,若s1和s2中不存在相同的字串題目就好解了,直接雙指標移動即可,而使此題成為hard的題的關鍵是,若將s3與s1和s2對應位比對時,此時s1和s2對應位字元相同,那該由哪一個匹配s3對應位就不好抉擇了,因為這會影響到後續的比對,想到著,我想出的第一個演算法是,對於當前位選與不選的問題,至少有兩個方法可以提供使用,即回溯演算法和動態規劃的01揹包,於是先選擇用回溯法解,但是回溯演算法若不剪枝的複雜度太高,OJ果然TLE了,然後想到用動態規劃解,分析一下,一個狀態有三個變數決定,即s1s2s3三個對應位置i,j,k決定,而k=i+j,因此可以進一步簡化一下,由i和j決定,因此定義狀態dp[i][j]
為s1前i個字元和s2前j個字元能交錯成長度為(i+j)的s3,而s3的(i+j-1)位置上的字元要麼來自s1要麼來自s2,OK狀態轉移方程有了:
dp[i][j] = (dp[i - 1][j] && s1[i - 1] == s3[i + j - 1]) || (dp[i][j - 1] && s2[j - 1] == s3[i + j - 1]);
完美解決動態規劃問題由三步組成,即定義狀態+狀態轉移+狀態初始化和更新。因此接下來就要做狀態初始化和更新的分析,這一步,我們要保證在推進到狀態(i,j)之前,狀態(i-1,j)和(i-1,j)都被初始化和更新,在這裡,因為i和j都是沿著遞增方向推進的,因此不會出現前置狀態未更新。詳細的分析推薦看這篇部落格,博主總結的一條規律,感覺不錯,引用如下。
只要是遇到字串的子序列或是匹配問題直接就上動態規劃 Dynamic Programming,其他的都不要考慮,什麼遞迴呀的都是浮雲(當然帶記憶陣列的遞迴寫法除外,因為這也可以算是 DP 的一種),千辛萬苦的寫了遞迴結果拿到 OJ 上妥妥 Time Limit Exceeded
class Solution {
public:
bool isInterleave(string s1, string s2, string s3) {
int n = s1.size(), m = s2.size(), r = s3.size();
if (n + m != r) return false;
vector<vector<bool>> dp(n + 1, vector<bool>(m + 1, false));
dp[0][0] = true;
for (int i = 1; i <= n; ++i) {
dp[i][0] = dp[i - 1][0] && s1[i - 1] == s3[i - 1];
}
for (int j = 1; j <= m; ++j) {
dp[0][j] = dp[0][j - 1] && s2[j - 1] == s3[j - 1];
}
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
dp[i][j] = (dp[i - 1][j] && s1[i - 1] == s3[i + j - 1]) ||
(dp[i][j - 1] && s2[j - 1] == s3[i + j - 1]);
}
}
return dp[n][m];
}
};
32. 最長有效括號
解題思路: 此題解起來還是有一定難度的,我沒解出來,參考了這篇部落格思路,但是僅根據部落格思路,雖然程式碼能AC,但是還是不能證明為什麼這個解法是對的(可能是我太笨啦╮(╯▽╰)╭),這裡記錄一下我對這題的分析以證明前面解法的合理性。考察配對的問題,比如括號、運算子等等,最常規的是用棧來解題,這個在《資料結構》書上一般都會舉出這個例子,而真正覺得此題為hard的是字串中間若插入了不能配對的(
或者)
這個字串就不能取,但是用棧解題的基本框架我們可以先搭起來,然後在遇到)
時,處理字串長度,我們用start記錄可能合理的字串起始位置,我們可以分析下面一組序列,因為1-3字串出現3因此不能與後序字串作為一個合理字串計算長度,因此start從下一個位置開始計算合理字串,OK,我們從4開始往後分析,若在遍歷到位置10時,若我們發現棧不為空,即6,我們展示還無法確定11是否能與6匹配(i.e.,字串會不會因為6無法匹配導致不能從start開始合理),因此當前位置我們只能上一個元素,即6,作為計算當前合理字串的起點(注意這裡不能以9作為起點因為會漏掉7和8,這個在LT水池題中出現過),當然,若我們遍歷到11位置,發現出棧後棧為空,那說明從start位置開始,所有元素均完成配對,所以可以肯定從start至當前位置字串是合理的,於是可以用start位置計算,那麼,我們為什麼在出棧後要判斷棧是否空呢?這是因為我們在是否用start做計算,i.e.,是否認定從start開始到當前位置字串合理拿捏不準,因此,以出棧後棧是否為空來做最長合理串的盡力計算(貪婪思想)。
class Solution {
public:
int longestValidParentheses(string s) {
stack<int> st;
int start = 0, res = 0;
for (int i = 0; i < s.size(); ++i) {
if (s[i] == '(') st.push(i);
else {
if (st.empty()) start = i + 1;
else {
st.pop();
if (st.empty()) res = max(res, i - start + 1);
else res = max(res, i - st.top());
}
}
}
return res;
}
};
標籤裡提到動態規劃解題,此題屬於字串子串問題,也應該要想到是否能用動態規劃題。此題我動態規劃除錯了半個多小時,AC後感慨了一會,果然是編寫思維不嚴謹,除錯耗時數萬倍,所以我們在寫程式碼的時候,對所有程式碼的位置編排、條件的並排或巢狀,都要能明確的思路說服自己為什麼這麼寫,只有這樣寫出來的程式碼才能bug-free。OK,看此題動態規劃解題思路,所有的括號關係我們可以歸納為要麼巢狀(i.e.,包含)要麼並排,在判斷括號字串是否有效之前,首先要考察自身是否能合理,然後在儘量利用括號關係往內部(i.e.,巢狀)或者向左邊(i.e.,並排)貪婪計算儘可能長的長度。設dp[i]
表示包含第i-1個字元的字串前i個字元能組成的最長合理括號串的長度,只有結尾是)
的字串dp值才非零,那麼dp[i]
前的dp[i-1]
若非零,則表示dp[i]
前面有長度為dp[i-1]
是合理字串,OK,那我們往前推進dp[i-1]+1
長度即可找到s[i - dp[i-1] - 2]
,看它是否為(
,i.e.,判斷dp[i]
是否配對,若不配對,實際上就不用再考慮巢狀和並排的關係了,直接為0,在配對的情況下,考慮巢狀關係,則可在dp[i-1]
長度基礎上再增加兩個配對元素,i.e., dp[i-1]+2
,再考慮並排關係,再向前推一個元素,若s[i - dp[i-1] - 3]
為)
或者說dp[i - dp[i-1] - 2]
非零,則可將前面並排的合理字串也併入其中。
class Solution {
public:
int longestValidParentheses(string s) {
int n = s.size(), res = 0;
vector<int> dp(n + 1, 0);
for (int i = 2; i <= n; ++i) {
if (s[i - 1] == ')') {
int j = i - dp[i - 1] - 2;
if (j >= 0 && s[j] == '(') {
dp[i] = dp[i - 1] + 2;
dp[i] += dp[j];
}
}
res = max(res, dp[i]);
}
return res;
}
};
87. 擾亂字串
解題思路: 看到字串子串問題,我們優先想到的是動態規劃,但在用動態規劃解題之前先介紹遞迴解法。我們先分析一下題意,題目定義將字串寫成二叉樹,將二叉樹的節點的左右子節點交換,得到的新字串為擾亂字串,題眼即左右子節點交換,而反應到字串上,相鄰兩端子串交換,在轉化一下,給定字串s,將s從中間某點斷開,形成字串s1s2,交換兩半段,得新字串s2s1,為擾亂字串。OK,剖析至此,基本明朗了,假設字串s1和s2,假設斷開段的長度一個是L和LEN-L,當對s1左->右斷開L處,那麼s2既可以左->右斷開L處,也可以右->左斷開L處,然後交叉判斷對應的子串是否能是scrambe,若是,則s1和s2是scramble,當然判斷之間可以先判斷s1和s2中對應的字元及其個數是否相同,最簡單的方法是排序然後比較字串是否相等。
class Solution {
public:
bool isScramble(string s1, string s2) {
if (s1 == s2) return true;
string ts1 = s1, ts2 = s2;
sort(ts1.begin(), ts1.end());
sort(ts2.begin(), ts2.end());
if (ts1 != ts2) return false;
int n = s1.size();
for (int i = 1; i <= n - 1; ++i) {
string s11 = s1.substr(0, i);
string s12 = s1.substr(i);
string s21 = s2.substr(0, i);
string s22 = s2.substr(i);
if ((isScramble(s11, s21) && isScramble(s12, s22)))
return true;
s22 = s2.substr(0, n - i);
s21 = s2.substr(n - i);
if ((isScramble(s11, s21) && isScramble(s12, s22)))
return true;
}
return false;
}
};
動態規劃解題,直覺告訴至少要兩個變數定義一個狀態,即s1位置和s2位置,若簡單的對整個字串切的話,大長度的子串在更新狀態時,無法保證小長度的子串已經更新了,這個在很多DP題中都碰到過,而常規的做法是,以長度作為遍歷,而不是以位置作為遍歷,這樣在更新大長度的狀態時,小長度的狀態肯定就已經更新過了。OK,為了方便更新,需要引入第三個變數即,子串長度,因此狀態定義為dp[i][j][l]
,其中i表示s1中子串起始的位置,j表示s2中子串起始的位置,l表示子串的長度,狀態轉移方程怎麼找呢,想一下題意,自然需要對長度為l做切開,切的方式在遞迴解法中已經講過,因此狀態轉移方程為:
dp[i][j][l] = dp[i][j][l] || (dp[i][j][k] && dp[i + k][j + k][l - k]) || (dp[i][j + l - k][k] && dp[i + k][j][l - k])
class Solution {
public:
bool isScramble(string s1, string s2) {
int n = s1.size();
vector<vector<vector<bool>>> dp(n, vector<vector<bool>>(n, vector<bool>(n + 1, false)));
for (int l = 1; l <= n; ++l) {
for (int i = 0; i + l <= n; ++i) {
for (int j = 0; j + l <= n; ++j) {
if (l == 1) {
dp[i][j][1] = (s1[i] == s2[j]);
} else {
for (int k = 1; k < l; ++k) {
if ((dp[i][j][k] && dp[i + k][j + k][l - k]) || (dp[i][j + l - k][k] && dp[i + k][j][l - k])) {
dp[i][j][l] = true;
break;
}
}
}
}
}
}
return dp[0][0][n];
}
};
85. 最大矩形
解題思路: 本題如果用暴力解法解的話,大致思路是,先確定矩陣的左上叫,然後確定矩陣的長和寬,再check這個矩陣是否全’1’,不難發現此演算法複雜度為 O ( n 4 ) O(n^4) O(n4),估計OJ會TLE。其實若是第一次做此題,還挺難解的,這題實際上是在題84基礎上解題,將此題矩陣逐行應用題84解法,具體的,對矩陣每一行,向上看,把它看成直方圖,求取這個直方圖後,利用題84解法解出最大矩陣,題84解法非常靈活,本題用的是單調棧解法。
class Solution {
public:
int maximalRectangle(vector<vector<char>>& matrix) {
if (matrix.empty() || matrix[0].empty()) return 0;
int n = matrix.size(), m = matrix[0].size();
vector<int> heights(m, 0);
int res = 0;
for (int i = 0; i < n; ++i) {
for (int j = 0; j < m; ++j) {
if (matrix[i][j] == '1') ++heights[j];
else heights[j] = 0;
}
res = max(res, helper(heights));
}
return res;
}
int helper(vector<int> &heights) {
stack<int> st;
int res = 0;
heights.push_back(0);
for (int i = 0; i < heights.size(); ++i) {
if (st.empty() || heights[st.top()] <= heights[i]) st.push(i);
else {
int idx = st.top(); st.pop();
int square = (st.empty() ? i * heights[idx] : (i - st.top() - 1) * heights[idx]);
res = max(res, square);
--i;
}
}
return res;
}
};
72. 編輯距離
解題思路: 轉述一下此題題意,若想知道word1和word2轉換情況,可以先知道它們字首(子串)的轉換情況,OK,這一轉換,題目有歸為字串子串問題,而子串問題我們會像膝跳反射一樣想到動態規劃。OK,因為涉及到兩個子串,確切的說是描述兩個子串的位置,那麼我們需要兩個變數來維護狀態,定義dp[i][j]
為word1前i個字元子串轉換到word2前j個字元子串需要的最少操作次數,若i-1和j-1位置字元不相等,則需要藉助題目提到三個操作以觸發狀態間的轉換,若當前狀態為(i,j)
,那麼前一個狀態為:1)刪除,(i-1,j)
,2)替換,(i-1,j-1)
,3)插入,(i,j-1)
。狀態轉移方程為:
if w1[i-1] == w2[j-1] then dp[i][j] = dp[i - 1][j - 1]
else dp[i][j] = min(dp[i - 1][j] + 1, min(dp[i - 1][j - 1] + 1, dp[i][j - 1] + 1))
class Solution {
public:
int minDistance(string word1, string word2) {
int n = word1.size(), m = word2.size();
vector<vector<int>> dp(n + 1, vector<int>(m + 1, 0));
for (int i = 0; i <= n; ++i) {
dp[i][0] = i;
}
for (int j = 0; j <= m; ++j) {
dp[0][j] = j;
}
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
if (word1[i - 1] == word2[j - 1]) dp[i][j] = dp[i - 1][j - 1];
else {
dp[i][j] = min(dp[i - 1][j] + 1, min(dp[i - 1][j - 1] + 1, dp[i][j - 1] + 1));
}
}
}
return dp[n][m];
}
};
132. 分割回文串 II
解題思路: 這題是題131的進階題,題131要求求出所有的情況,而此題要求求出情況的個數,這實際上是LT題中最常規的搭配了,求出所有的情況對應的是回溯+剪枝解法,而求出情況的個數如果按照前面情形解,如果找不到好的剪枝方法,勢必會TLE,最終還是得迴歸到動態規劃解法,因此總結一個結論,求所有情況用回溯剪枝,而求情況的個數用動態規劃。本題應該是我解動態規劃題畫的最長時間的一題了,著重記一筆,後面得詳細review此題。起初用的是由題131改進的回溯法,但是剪枝如何優化都沒能逃過OJ的TLE,遂放棄,然後選擇動態規劃,進而要選擇如何定義狀態以及找轉移方程,定義dp[i]
為[0,i]
最小分割次數,那麼我們對[0,i]
內每個元素切一次,然後取所有切法的最小分割數做為dp[i]
,狀態轉移方程如下:
dp[i]=MIN(dp[j]+1) && Palindrome(s[j,i]), 0<j<i
求迴文我們常規的做法是雙指標,但是本題要求多次迴文,因此雙指標會在原演算法基礎上多加一層O(n)時間複雜度,導致整個演算法時間複雜度太高,有點極端case無法AC,因此,對迴文判斷要做優化,而多次判斷迴文採取的優化策略用到了題647用動態規劃。本題本來解起來非常簡單,但是由於對時間非常苛刻,對細節的把控就比較難想到了,比如迴文用動態規劃來優化,對於我一貫用雙指標來判斷,實在想不到了,或許多做點題,可能融會貫通,激發出一點思維火花,hhh,因此本質上此題考查了兩個動態規劃,不愧為hard╮(╯▽╰)╭.
class Solution {
public:
int minCut(string s) {
int n = s.size();
vector<int> dp(n);
vector<vector<bool>> p(n, vector<bool>(n));
for (int i = 0; i < n; ++i) {
dp[i] = i;
p[i][i] = true;
if (i >= 1 && s[i] == s[i - 1]) p[i - 1][i] = true;
}
for (int len = 3; len <= n; ++len) {
for (int i = 0; i + len <= n; ++i) {
int j = i + len - 1;
if (s[i] == s[j] && p[i + 1][j - 1]) p[i][j] = true;
}
}
for (int i = 1; i < n; ++i) {
if (p[0][i]) dp[i] = 0;
else {
for (int k = 1; k <= i; ++k) {
if (p[k][i]) {
dp[i] = min(dp[i], dp[k - 1] + 1);
}
}
}
}
return dp[n - 1];
}
};
相關文章
- LeetCode總結,動態規劃問題小結LeetCode動態規劃
- leetcode總結——動態規劃LeetCode動態規劃
- LeetCode動態規劃總結LeetCode動態規劃
- leetcode-動態規劃總結LeetCode動態規劃
- 動態規劃 總結動態規劃
- 德魯週記10--15天從0開始刷動態規劃(leetcode動態規劃題目型別總結)動態規劃LeetCode型別
- 動態規劃分類題目總結動態規劃
- leetcode題解(動態規劃)LeetCode動態規劃
- [leetcode初級演算法]動態規劃總結LeetCode演算法動態規劃
- 一維動態規劃總結動態規劃
- [leetcode] 動態規劃(Ⅰ)LeetCode動態規劃
- LeetCode 動態規劃 House Robber 習題LeetCode動態規劃
- 總結 | 動態規劃十問十答動態規劃
- 演算法之動態規劃總結演算法動態規劃
- 【DP專輯】ACM動態規劃總結ACM動態規劃
- leetcode:動態規劃( hard )LeetCode動態規劃
- LeetCode:動態規劃+貪心題目整理LeetCode動態規劃
- 動態規劃小結動態規劃
- 好題——動態規劃動態規劃
- 動態規劃專題動態規劃
- 動態規劃題單動態規劃
- Leetcode 題解演算法之動態規劃LeetCode演算法動態規劃
- 【LeetCode】Word Break 動態規劃LeetCode動態規劃
- 最小總和問題(動態規劃演算法)動態規劃演算法
- 動態規劃練習題動態規劃
- 動態規劃解題方法動態規劃
- 動態規劃做題思路動態規劃
- 【leetcode】741 摘櫻桃(動態規劃)LeetCode動態規劃
- 【LeetCode】Word Break II 動態規劃LeetCode動態規劃
- [LeetCode解題] -- 動態規劃二 [ 子串、子序列問題 ]LeetCode動態規劃
- 【LeetCode動態規劃#12】詳解買賣股票I~IV,經典dp題型LeetCode動態規劃
- 震驚!動態規劃精華總結-----看完這一篇就夠了(含leetcode練習題)動態規劃LeetCode
- 【動態規劃(一)】動態規劃基礎動態規劃
- Leetcode 題解系列 -- 股票的最大利潤(動態規劃)LeetCode動態規劃
- 整數劃分問題(動態規劃)動態規劃
- 演算法刷題:LeetCode中常見的動態規劃題目演算法LeetCode動態規劃
- Leetcode 編輯距離(動態規劃)LeetCode動態規劃
- [leetcode 1235] [動態規劃]LeetCode動態規劃