動態規劃的主要思想html
動態規劃的三要素:c++
這裏歸納了一下動態規劃的主要思想和要素,讓咱們暫時帶着疑問,先來看幾個經典問題。算法
假設有一個書架,最多容納10本書。如今咱們要把這個書架放滿,條件是,每次只能放1本或者2本。問:放滿十本書總共有多少种放法?數組
下圖爲其中一種方法:每次只放1本。咱們能夠表示爲1,1,1,1,1,1,1,1,1,1 bash
文字來描述問題未免過於囉嗦,爲了更好地分析問題,咱們先對原問題進行建模。學習
咱們須要對問題進行建模,用一個方程來普適化表達這一問題。創建方程就須要肯定變量(狀態),變量的肯定能夠看咱們在問題的每一個階段中作決策時受到什麼因素影響。題目中限制死了每次只能放1或2本,那麼在書架上能放滿書有多少种放法,只跟書架的容量有關係,所以書架的容量可做爲方程中的變量。設F(x)的解爲將容量爲x的書架放滿書的總放法數,則原問題的解爲F(10)。優化
咱們反過來想一下,放滿10本書以前書架的狀態是怎麼樣的?在放滿10本書以前:ui
放滿9本和放滿8本,分別對應了最後一次放1本和2本,包含了放滿10前一階段的全部狀況。所以書架放10本書的總放法數,等同於書架放9本書的總放法數 + 書架放8本書的總放法數。原問題就能夠分解成:F(10) = F(9) + F(8),F(9)和F(8)爲F(10)的最優子結構。推廣到通常狀況可獲得一個方程F(x) = F(x-1) + F(x-2),(x>1),這個方程就是狀態轉移方程。 獲得這個方程後,咱們很容易就能夠獲得這個問題的解了,這不就是典型的遞歸問題嘛。spa
const bookrackRecur = (window.tempF = (bookrack) => {
if (bookrack < 3) {
return bookrack;
}
//根據狀態轉移方程F(x) = F(x-1) + F(x-2)
return window.tempF(bookrack - 1) + window.tempF(bookrack - 2);
})(10);
console.log(bookrackRecur); //89
複製代碼
分解問題的思想讓咱們很容易就知道了解題的思路,可是上面遞歸的解法有一種問題。會重複計算相同問題。3d
下面咱們轉變下思路,嘗試着自底向上迭代求解。下面藉助一張表來講明,F(x)一樣表示爲書架放滿x本書的總放法。
x | 0 | 1 | 2 | 3 | ... |
---|---|---|---|---|---|
F(x) | 0 | 1 | 2 | 3 (F(2)+F(1)) | ... |
該問題具備邊界:
根據狀態轉移方程,咱們知道F(3)是隻依賴F(1)和F(2)的,咱們在程序中保存了前兩個問題的解,就能夠獲得目前問題的解。
const bookrackDP = ((bookrack) => {
//爲了問題更好的對應數組下標,加入問題0, F(0) = 0
//保存前兩個問題的解F(1) = 1, F(2) = 2;
let record = [0, 1, 2];
for (let i = 0; i <= bookrack; i++) {
if (i >= 3) {
//記錄問題的解
record[i] = record[i - 1] + record[i - 2];
}
}
//根據記錄直接獲得問題的解
return record[bookrack];
})(10);
console.log(bookrackDP);
複製代碼
這種算法的時間複雜度爲O(n)。上面的代碼用了數組存儲子問題的解,實際上是能夠再優化一下的。咱們上面分析了,目前問題的解只依賴於前兩個問題,所以用兩個局部變量存儲前兩個問題的解就好了,這裏就再也不展現代碼了。咱們看一個複雜點的問題。
假設有一個運氣爆棚的獵人進入到了彩虹洞中,地精讓這我的從寶庫中任意拿走物品,直到獵人的揹包裝不下爲止。寶庫中每一個物品都有特定的價值和重量,獵人的揹包承重有限。那麼問題來了,獵人怎麼裝物品才能得到最大價值呢?假設獵人揹包最大承重爲20公斤,物品重量及價值以下圖:
仍是先肯定方程的變量(狀態)。思考一下會影響咱們在各階段作決策的因素有什麼(通常問題中能直接找到)?
如今就能夠對問題創建方程了:設F(i, c)的解爲,從i件物品中,挑選一些物品放入剩餘容量爲c(capacity)的揹包中,使的揹包物品價值最大。則原問題的解爲F(5, 20)。思考一下,怎麼將F(5, 20)分解呢?
咱們能夠考慮面對物品5時,選不選的問題。(這個地方着重理解)
根據上面的分析咱們就能夠獲得這個問題的狀態轉移方程了(一樣的咱們發現可能會有重疊子問題):
F(i, c) = max(F(i-1, c-weights[i]) + values[i] , F(i-1, c)), (i > 0)
很容易就能夠獲得遞歸的解法了
let goods = [
{value: 0, weight: 0}, //方便對應下標
{value: 3, weight: 2},
{value: 4, weight: 3},
{value: 8, weight: 5},
{value: 10, weight: 9},
{value: 5, weight: 4}
];
const knapsackRecur = ((goods, capacity) => {
let recurF = (curIdx, curCapacity) => {
if (!curIdx) {
return 0;
}
let weight = goods[curIdx].weight,
value = goods[curIdx].value;
if (weight > curCapacity) {
return recurF(curIdx - 1, curCapacity);
}
let vPick = recurF(curIdx - 1, curCapacity - weight) + value,
vNotPick = recurF(curIdx - 1, curCapacity);
return Math.max(vPick, vNotPick);
};
return recurF(goods.length - 1, capacity);
})(goods, 20);
console.log(knapsackRecur); //26
複製代碼
咱們來看看01揹包的動態規劃解法
跟書架問題同樣,想辦法將子問題的解記憶化。因爲方程裏有兩個狀態(變量),所以咱們須要用二維數組存儲問題的解。咱們先來看看下面這一張表。在線01揹包表
let goods = [
{value: 0, weight: 0}, //方便對應下標
{value: 3, weight: 2},
{value: 4, weight: 3},
{value: 8, weight: 5},
{value: 10, weight: 9},
{value: 5, weight: 4}
];
const knapsackDP = ((goods, capacity) => {
//初始化二維數組,保存子問題的最優解
let record = [];
for (let i = 0; i < goods.length; i++) {
if (!record[i]) {
record[i] = [];
}
let weight = goods[i].weight,
value = goods[i].value;
for (let c = 0; c <= capacity; c++) {
if (!record[i][c]) {
record[i][c] = 0;
}
if (i - 1 < 0) {
continue;
}
//商品i的重量大於揹包容量,沒法把商品i放到揹包裏
if (weight > c) {
record[i][c] = record[i - 1][c];
} else {
let vPick = record[i - 1][c - weight] + value, //選的狀況
vNotPick = record[i - 1][c]; //不選的狀況
record[i][c] = Math.max(vPick, vNotPick); //最優狀況
}
}
}
return record[goods.length - 1][capacity];
})(goods, 20);
console.log(knapsackDP);
複製代碼
動態規劃更像是一種思想,相似分治,把複雜問題分解成多個子問題,簡單化問題。同時記憶化問題的解,爭取每一個問題只求解一次,達到優化效果。所以動態規劃適用於含有重疊子問題的問題。解決動態規劃問題的關鍵在於推導出狀態轉移方程。狀態轉移方程的推導過程能夠參考以下:
本文屬我的理解,旨在互相學習,有說的不對的地方,師請糾正。轉載請註明原帖