动态规划的主要思想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);
复制代码
动态规划更像是一种思想,相似分治,把复杂问题分解成多个子问题,简单化问题。同时记忆化问题的解,争取每一个问题只求解一次,达到优化效果。所以动态规划适用于含有重叠子问题的问题。解决动态规划问题的关键在于推导出状态转移方程。状态转移方程的推导过程能够参考以下:
本文属我的理解,旨在互相学习,有说的不对的地方,师请纠正。转载请注明原帖