動態規劃
動態規劃(英語:Dynamic programming,簡稱 DP)是一種在數學、管理科學、計算機科學、經濟學和生物信息學中使用的,通過把原問題分解爲相對簡單的子問題的方式求解複雜問題的方法。
動態規劃常常適用於有重疊子問題和最優子結構性質的問題,動態規劃方法所耗時間往往遠少於樸素解法。
動態規劃背後的基本思想非常簡單。大致上,若要解一個給定問題,我們需要解其不同部分(即子問題),再根據子問題的解以得出原問題的解。動態規劃往往用於優化遞歸問題,例如斐波那契數列,如果運用遞歸的方式來求解會重複計算很多相同的子問題,利用動態規劃的思想可以減少計算量。
通常許多子問題非常相似,爲此動態規劃法試圖僅僅解決每個子問題一次,具有天然剪枝的功能,從而減少計算量:一旦某個給定子問題的解已經算出,則將其記憶化存儲,以便下次需要同一個子問題解之時直接查表。這種做法在重複子問題的數目關於輸入的規模呈指數增長時特別有用。
解題步驟
一、確定狀態
- 狀態在動態規劃中的作用屬於定海神針
- 簡單地說,解動態規劃的時候需要開一個數組,數組的每個元素f[i]或f[i][j]含義——類似於解數學題中X,Y,Z代表什麼。
- 確定狀態需要兩個意識:最後一步和子問題
二、轉移方程
根據最後一步和子問題得到遞推關係,進而得出轉移方程。
三、初始條件和邊界情況
dp數組的初值和結束邊界很重要,很多新手容易在初值設置環節犯錯,即賦錯值。
四、確定計算順序
即確定循環計算的方式。
下面我們以LeetCode上的簡單動態規劃題的順序開始進行學習
LeetCode53. 最大子序和 && 面試題42
給定一個整數數組 nums
,找到一個具有最大和的連續子數組(子數組最少包含一個元素),返回其最大和。
示例:
輸入: [-2,1,-3,4,-1,2,1,-5,4],
輸出: 6
解釋: 連續子數組 [4,-1,2,1] 的和最大,爲 6。
由於求的是最大子序列和,那麼在增加最後一個目標元素nums[i]之前,我們需要取記錄之前最大序列和的dp[i - 1]加上當前nums[i]的值與當前nums[i]的最大值,即確立狀態轉移方程dp[i] = max(dp[i - 1] + nums[i], nums[i]),然後更新序列和的最大值。
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int n = nums.size();
vector<int> dp(n, nums[0]);
int res = nums[0];
for(int i = 1; i < n; i++){
dp[i] = max(dp[i - 1] + nums[i], nums[i]);
res = max(dp[i], res);
}
return res;
}
};
LeetCode392. 判斷子序列
給定字符串 s 和 t ,判斷 s 是否爲 t 的子序列。
你可以認爲 s 和 t 中僅包含英文小寫字母。字符串 t 可能會很長(長度 ~= 500,000),而 s 是個短字符串(長度 <=100)。
字符串的一個子序列是原始字符串刪除一些(也可以不刪除)字符而不改變剩餘字符相對位置形成的新字符串。(例如,"ace"是"abcde"的一個子序列,而"aec"不是)。
示例 1:
s = "abc", t = "ahbgdc"
返回 true.
示例 2:
s = "axc", t = "ahbgdc"
返回 false.
此題可以不用動態規劃來寫,當字母匹配時,使用index來記錄匹配個數,直到index達到子序列s的長度,則匹配成功。
class Solution {
public:
bool isSubsequence(string s, string t) {
//普通方法
int index = 0;
if(s.empty()) return true;
for(int i = 0; i < t.size(); i++){
if(t[i] == s[index]){
index++;
if(index >= s.size())
return true;
}
}
return false;
}
};
此題若寫成動態規劃,由於是兩個字符串的問題,一般都是二維dp:
狀態:s的前i個元素,t的前j個元素位置
dp數組含義:dp[i][j]代表s的前i個元素是否爲t的前j個元素的子序列,是則true,否則false。然後是狀態轉移方程:
- 當s[i] == t[j]時,這兩個元素就不重要了,取決於dp[i-1][j-1]是什麼,即dp[i][j] = dp[i - 1][j - 1];
- 當s[i] != t[j]時,那麼t[j]就不重要了,取決於dp[i][j-1]是什麼,即dp[i][j] = dp[i][j - 1]
容易想到:
- dp[i][j]=false,i>j 這一條可以直接在二重循環中體現: int j = i
- dp[0][j]=true,j∈[0,n]
class Solution {
public:
bool isSubsequence(string s, string t) {
//動態規劃
//dp[i][j] = dp[i - 1][j - 1];匹配時
//dp[i][j] = dp[i][j - 1] 不匹配,縮短長序列
int m = s.length(), n = t.length();
vector<vector<bool>> dp(m + 1, vector<bool>(n + 1, false));
for(int i = 0; i <= n; i++) {
dp[0][i] = true;
}
for(int i = 1; i <= m; i++){
for(int j = i; j <= n; j++){
if(s[i - 1] == t[j - 1]){ //i-1相當於第i個元素
dp[i][j] = dp[i - 1][j - 1];
}
else{
dp[i][j] = dp[i][j - 1];
}
if(i == m && dp[i][j] == true) return true;//剪枝
}
}
return dp[m][n];
}
};
LeetCode303. 區域和檢索 - 數組不可變
給定一個整數數組 nums,求出數組從索引 i 到 j (i ≤ j) 範圍內元素的總和,包含 i, j 兩點。
示例:
給定 nums = [-2, 0, 3, -5, 2, -1],求和函數爲 sumRange()
sumRange(0, 2) -> 1
sumRange(2, 5) -> -1
sumRange(0, 5) -> -3
- 我們不直接存儲結果,我們存儲從第一個元素到第i個元素的和dp[i] = sum(nums[0]~nums[i - 1]);
- 動態轉移方程:
- dp[i] = dp[i - 1] + nums[i];
- res = dp[j] - dp[i - 1];
- 初始條件
- dp[0] = nums[0]
- i=0時,即從初始位置開始則應return dp[j],當i不爲0時,用0~j區域的和減去0~i區域的和及res = dp[j] - dp[i - 1]。
class NumArray {
public:
NumArray(vector<int>& nums) {
if(nums.size() != 0){
dp.push_back(nums[0]);
for(int i = 1; i < nums.size(); i++)
dp.push_back(dp[i - 1] + nums[i]);
}
}
int sumRange(int i, int j) {
if(dp.size() == 0) return 0;
return i == 0 ? dp[j] : (dp[j] - dp[i - 1] );
}
private:
vector<int> dp;
};