貪心、分治、動態規劃的區別
從一道經典題說明:力扣42:連續子數組的最大和
// 貪心
var maxSubArray = function(nums) {
const len = nums.length;
let max = nums[0];
let sum = nums[0];
for(let i = 1; i < len; i++) {
if(sum < 0) {
sum = nums[i];
} else {
sum += nums[i];
}
if(sum > max) max = sum;
}
return max;
};
// 動態規劃
var maxSubArray = function(nums) {
const len = nums.length;
if(len==0) return 0;
let dp = new Array(len);
let max = (dp[0] = nums[0]);
for(let i = 1; i < len; i++) {
dp[i] = Math.max(dp[i-1]+nums[i], nums[i]);
max = Math.max(max, dp[i]);
}
return max;
};
// 在原地進行的動態規劃,用nums[i]表示dp[i]
var maxSubArray = function(nums) {
const len = nums.length;
if(len==0) return 0;
let max = nums[0];
for(let i = 1; i < len; i++) {
if (nums[i - 1] > 0) {
nums[i] += nums[i - 1];
}
max = Math.max(max, dp[i]);
}
return max;
};
一、動態規劃介紹
動態規劃其實是運籌學的一種最優化方法,只不過在計算機問題上應用比較多,比如說讓你求最長遞增子序列呀,最小編輯距離呀等等。
動態規劃 = 窮舉 + 剪枝,一定要記住窮舉,先找出所有的狀態,再找到所有狀態對應的所有選擇,接着對比選出符合條件的選擇,這個過程中用dp table存儲之前的選擇或備忘錄減少遞歸次數。
解題步驟
- 建立dp數組,根據題意可以建立一維、二維甚至三維的數組,dp數組的索引值就是影響每個狀態的值,比如兩個字符串問題,有了字符串的改變纔有了狀態的改變,存儲的值就是題目要求的值;揹包問題中揹包的體積和物體的類別限制了揹包中物體的總價值和能放的總體積。
- 定義base case,dp數組的初始條件,方便後來迭代求解下一個值
- 找狀態轉移方程,其實我覺得這纔是第一步,當讀懂了題目後才能思考如何建立dp數組和base case。所謂的狀態就是對不同的選擇形成的狀態,也就是在這裏窮舉所有的可能性,選出最符合題意的最大或最小值等。
- 遍歷窮舉,一般是幾維的dp數組就有幾層循環,比較所有選擇的值,找出符合題意的,遍歷策略也是非常需要注意的。
動態規劃的時間複雜度
- 動態規劃算法的時間複雜度就是子問題個數 × 函數本身的複雜度。
動態規劃的base case
-
Infitity — JavaScript可表示的最大值,最大值爲1.7976931348623157e+308
-
-Infitity — JavaScript可表示的最小值
優化方法
- 二分查找:通過二分查找減少遍歷次數,如雞蛋掉落問題
參考
關於動態規劃如何理解,爲什麼需要動態規劃,可以看看這個大神的題解:動態規劃套路詳解
二、動態規劃應用
1.兩個字符串問題
非常經典,典型的二維動態規劃,大部分比較困難的字符串問題都可以用這個方法。
(1)最長公共子序列LCS問題
先是列出所有可能的情況,從頂向下遞歸解決。
// 暴力解法,從頂向下
// 理解起來簡單,但時間複雜度高,會超出時間限制
var longestCommonSubsequence = function(text1, text2) {
let dp = function(i, j) {
if(i==-1 || j==-1) return 0;
if(text1[i] == text2[j]) {
console.log(1);
return dp(i-1, j-1) + 1;
} else {
return Math.max(dp(i-1, j), dp(i, j-1));
}
}
return dp(text1.length-1, text2.length-1);
};
用動態規劃把遞歸的從上到下轉換成從下到上,減少時間複雜度。
// 動態規劃解法
// 使用狀態機 dp table 保存之前的所有狀態,推測出新的狀態
var longestCommonSubsequence = function(text1, text2) {
// 用table保存之前的每個狀態
// 後一個狀態取決於前一個狀態
const len1 = text1.length;
const len2 = text2.length;
// 建立 dp table 保存狀態
// 同時給基礎狀態 base case 賦值
let table = Array.from(new Array(len1+1),() => new Array(len2+1).fill(0));
// 循環,尋找狀態
for(let i = 1; i < len1+1; i++) {
for(let j = 1; j < len2+1; j++) {
if(text1[i-1] == text2[j-1]) {
table[i][j] = table[i-1][j-1] + 1;
} else {
table[i][j] = Math.max(table[i][j-1], table[i-1][j]);
}
}
}
return table[len1][len2];
};
(2)編輯距離
暴力解決,從頂向下。
var minDistance = function(word1, word2) {
// 字符串類的動態規劃,窮舉策略都是用兩個指針分別指向兩個字符串,然後遍歷兩個字符串
// 窮舉
const l1 = word1.length;
const l2 = word2.length;
let dp = function(i, j) {
if(i==-1) return j+1;
if(j==-1) return i+1;
if(word1[i] == word2[j]) {
return dp(i-1, j-1);
} else {
return Math.min(dp(i-1, j-1)+1, dp(i-1, j)+1, dp(i, j-1)+1);
}
}
return dp(l1, l2);
};
使用備忘錄記錄已經求過的狀態。
var minDistance = function(word1, word2) {
// 字符串類的動態規劃,窮舉策略都是用兩個指針分別指向兩個字符串,然後遍歷兩個字符串
// 窮舉
const l1 = word1.length;
const l2 = word2.length;
// 加備忘錄
let table = Array.from(new Array(l1+1),() => new Array(l2+1).fill(0));
console.log(table);
let dp = function(i, j) {
// base case
if(i==-1) return j+1;
if(j==-1) return i+1;
// 如果之前dp[i][j]的值之前計算過,就直接取它的值
// 添加備忘錄,減少搜索時間
if(table[i][j] != 0) {
return table[i][j];
}
if(word1[i] == word2[j]) {
table[i][j] = dp(i-1, j-1);
} else {
table[i][j] = Math.min(dp(i-1, j-1)+1, dp(i-1, j)+1, dp(i, j-1)+1);
}
return table[i][j];
}
return dp(l1, l2);
};
動態規劃,從下到上。
var minDistance = function(word1, word2) {
// 字符串類的動態規劃,窮舉策略都是用兩個指針分別指向兩個字符串,然後遍歷兩個字符串
// 窮舉
const l1 = word1.length;
const l2 = word2.length;
// 創建新數組時最好不要調用函數,會增加時間複雜度
let dp = Array.from(new Array(l1+1), () => new Array(l2+1).fill(0));
// base case
for (let i = 1; i <= l1; i++)
dp[i][0] = i;
for (let j = 1; j <= l2; j++)
dp[0][j] = j;
for(let i = 1; i < l1+1; i++) {
for(let j = 1; j < l2+1; j++) {
if(word1[i-1] == word2[j-1]) {
dp[i][j] = dp[i-1][j-1];
} else {
dp[i][j] = Math.min(dp[i-1][j-1]+1, dp[i-1][j]+1, dp[i][j-1]+1);
}
}
}
console.log(dp);
return dp[l1][l2];
};
注意:創建新數組時最好不要調用函數,會增加時間複雜度
2. 揹包問題
揹包問題非常經典,可惜力扣上沒有對應的題目
3. 力扣系列動態規劃問題:股票買賣問題
股票買賣問題總有六道題,難度從簡到難,歸根結底每道題都可以用動態規劃來做,這個大神講解的非常全面細緻:一個通用方法團滅 6 道股票問題