该系列博客索引目录:数据结构与算法—前端JavaScript学习
动态规划
介绍:
(随便看看,不看也行,看不懂很正常,看几个示例就懂了)
动态规划是一种编程思想,主要用于解决最优解类型的问题。
其思路是为了求解当前的问题的最优解,使用子问题的最优解,然后综合处理,最终得到原问题的最优解。
但是也不是说任何最优解问题都可以使用动态规划,使用dp的问题一般满足下面的两个特征:
(1)最优子结构,就是指问题可以通过子问题最优解得到;体现为找出所有的子问题最优解,然后取其中的最优;
(2)重叠子问题,就是子问题是会重复的。而不是一直产生新的子问题(比如分治类型的问题)。
一般而言,满足上述两个条件的最优解问题都可以会使用DP来解决。
相关概念:
最优子结构、边界、状态转移公式。(先随便看以下,后面会讲)
1.斐波那契数列:
一维的动态规划与斐波那契数列的思想、代码类似,首先介绍斐波那契数列。
斐波那契数,通常用 F(n)
表示,形成的序列称为斐波那契数列。该数列由 0
和 1
开始,后面的每一项数字都是前面两项数字的和。也就是:
F(0) = 0, F(1) = 1
F(N) = F(N - 1) + F(N - 2), 其中 N > 1.
给定 N
,计算 F(N)
。
示例 1:
输入:2
输出:1
解释:F(2) = F(1) + F(0) = 1 + 0 = 1.
var fib = function (n) {
if (n === 0) { return 0 } else if (n === 1) { return 1 }
return fib(n - 1) + fib(n - 2)
}
console.log(fib(2))
2.1一维动态规划例子:
假设你正在爬楼梯。需要 10
阶(不同于leetcode种的n阶,暂考虑为10)你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
image.png
首先考虑排列组合,但是时间复杂度是指数级的。(排列组合知识点可参考我的博客排列组合-js)
那么来介绍动态规划:
动态规划类似于斐波那契,是自后往前考虑的。例如对于最后一步爬到第10阶楼梯有两种方式,从第9阶爬1阶,从第8阶爬2阶。
那么到达第10阶的方法数等于到第9、8阶之和。F(10)=F(9)+F(8)
,F(9)与F(8)就是F(10)的最优子结构
对于第9阶,可以从第8阶、第7阶到第9阶。最优子结构:F(9)=F(8)+F(7)
由最优子结构推导提取出公式:F(n)=F(n-1)+F(n-2)(n>=3)
,这就是状态转移公式
边界:推导出公式至少要有两个数,那么取开始的情况F(2)=2、F(1)=1
。从第0格到第1格1种爬法,第0格到第2格2种
var climbStairs = function (n) {
if (n === 1) { return 1 } else if (n === 2) { return 2 }
return climbStairs(n - 1) + climbStairs(n - 2)
}
console.log(climbStairs(10)) //89
2.2概念巩固
**最优子结构:**从后往前推导,这一次的情况取决于之前的情况。 到第10阶由第9阶爬一格,第8届爬2格。F(10)=F(9)+F(8)
**状态转移公式:**由最优子结构总结出规律,与具体数字无关的普遍规律。爬楼梯问题总结出F(n)=F(n-1)+F(n-2)
**边界:**状态转移公式是由前面的状态推导出后面的状态,采用递归的方式,但是不能无限地向前查找,无限地递归,所以有边界值。边界值即为递归终止条件,以及问题最开始的条件。例如:从第0格到第1格1种爬法,第0格到第2格2种。边界为F(2)=2、F(1)=1
3.不同路径例子:
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。
问总共有多少条不同的路径?
image.png
示例 1:
输入: m = 3, n = 2
输出: 3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。
1. 向右 -> 向右 -> 向下
2. 向右 -> 向下 -> 向右
3. 向下 -> 向右 -> 向右
示例 2:
输入: m = 7, n = 3
输出: 28
使用图示7x3的网格,按照动态规划从后往前推导,对于最后一格(7,3)有两种方式到达(6,3)向右一格,(7,2)向下一格。
最优子结构:F(7,3) =F(6,3)+F(7,2)
状态转移公式:F(m,n)=F(m-1,n)+F(m,n-1)
限制条件:在最左边只能往右走,最上边只能往下走,最下边只能往上走
边界:最左边只有一种走法,最上边也只有一种。F(x,1)=1;F(y,1)=1
var uniquePaths = function (m, n) {
if (m === 1 || n === 1) {
return 1
}
return uniquePaths(m - 1, n) + uniquePaths(m, n - 1)
}
console.log(uniquePaths(7,3)) // 28
4.性能优化:
image.png
F(n)=F(n-1)+F(n-2)`的时间复杂度是`2^n
并且以上颜色相同的都是重复计算的值,越是树的下层,被重复计算的次数越多。F(5)=F(4)+F(3);F(4)=F(3)+F(2)
仅仅这个两个式子中F(4)
与F(3)
就被重复计算了两次。
4.1备忘录算法
将已经计算的值存储在Map当中,下次再计算这个值时直接从Map中获取。这样时间复杂度与空间复杂度都为O(N)
斐波那契:
var fib = function (n, map = new Map()) {
if (n === 0) { return 0 } else if (n === 1) { return 1 }
if (!map.has(n)) {
map.set(n, fib(n - 1, map) + fib(n - 2, map))
}
return map.get(n)
}
不同路径:
var uniquePaths = function (m, n, map = new Map()) {
if (m === 1 || n === 1) {
return 1
}
if (!map.has(`${m},${n}`)) {
map.set(`${m},${n}`, uniquePaths(m - 1, n, map) + uniquePaths(m, n - 1, map))
}
return map.get(`${m},${n}`)
}
4.2从前往后
从前往后计算出F(1)、F(2)、F(3)…直到F(n)
F(n)= F(n-1)+F(n-2)
只用储存两个结果,空间复杂度为O(1)
function fib (n) {
let cache = []
for (let i = 0; i < n; i++) {
if (i === 0) {
cache.push(0)
} else if (i === 1) {
cache.push(1)
} else {
cache.push(cache[0] + cache[1])
cache.shift()
}
}
if (n <= 1) {
return n
}
return cache[0] + cache[1]
}
console.log(fib(3)) // 2
5.经典背包问题:
**题目:**一个小偷半夜进入商店抢劫,发现里面有很多值钱的东西,钻石珠宝等,但是小偷只带了一个能装5KG的袋子,那么小偷能装的最多价值为多少?
5kg袋子
物品价值:6,10,12
物品重量:1,2,3
id为物品id,value为物品价值,weight为物品种类,v/w为物品单位重量的价值。商店物品信息如下表
image.png
此问题用动态规划的思想从前往后推导为:
1.袋子5kg,仅有第1个物品时怎么选收益最大
2.袋子5kg,有第1、2个物品时怎么选
3.袋子5kg,有第1、2、3个物品时怎么选
以上步骤分析为:
1.仅第1个物品时,判断1是否能放下,发现能,所以袋子中装入物品1,总价值6
2.有1、2个物品时,判断是否需要装入2。判断条件为
1.选择装入物品2
select = 0
if(装入2){
//装入2之后还有多少空间,这个空间下的最优解是什么
装2之后剩余空间为3,物品只有1,此时最优解为装1
slect = 6 + 10
}
notSelect = 0
2.不选择装入物品2
notSelect = 1价值
return max(select,noSelect)
在此情况为发现2可以装入,并且装入后剩余空间为3,仍可装1,所以选择装入物品2,总价值为:1价值+2价值
3.有1、2、3个物品时是否需要装入3。上一步情况为装1、2总价值为16。如果装入3那么剩余空间为2,空间为2时的最优解是装入2。装3时共获取价值22,比不装好
在一维的思考中,每次加入一个物品这个物品都有两种选择:
1.不装,那么不装的最优解为之前的这个物品不存在时的最优解,例如步骤3,物品3参与选择时,最优解是空间5
、物品[1,2
]的最优解
2.装,例如步骤3,那么装的最优解就是装入3之后空间2
、物品[1,2]
时的最优解+3的价值。
再比较装与不转的谁的获利多
所以背包问题是一个二维的动态规划问题,不仅仅是物品维度,也是空间维度的。
从左到右从上到下的列举出不同空间,物品量的表,完成到右下角时问题解决。
袋子容积:1kg | 2kg | 3kg | 4kg | 5kg | |
---|---|---|---|---|---|
添加第1个1kg物品 | 6 | 6 | 6 | 6 | 6 |
第2个2kg物品 | 6 | 10 | 16 | 16 | 16 |
第3个3kg物品 | 6 | 10 | 16 | 18 | 22 |
状态转移公式:F(n,space)=Max( F(n-1,space-goods[n].weight)+goods[n].weight, F(n-1,space) )
goods`表示所有物品`[good1,good2,good3]
n
表示现在需要判断的是第几个物品
装第n个物品:F(n-1,space-goods[n].weight)+goods[n].weight
不装第n个物品:F(n-1,space)
5.2代码:
装第n个物品:F(n-1,space-goods[n].weight)+goods[n].weight
下图为求解过程中储存中间值的表格,与之前的表格相对应
储存中间值的表格
由于状态转移公式中涉及到了n-1,物品数又是从1开始的,为了能获取到F(0,space-goods[1].weight)
所以在table中添加一行[0,0,0,0,0,0]
作为边界,表示没有物品时各个空间值对应的解,以供有1个物品时能使用状态转移公式
F(n-1,space-goods[n].weight)
中会出现space
与goods[n].weight
相等的情况,所以添加第1列表示背包空间为0的情况作为边界值
function bag (goods, space) {
/*
goods[0]:{
weight:0,
value:0
}
*/
goods.unshift({ weight: 0, value: 0 }) // 添加边界值,没有物品时
let firstRaw = new Array(space + 1).fill(0)// 填充第一行为0,当没有物品时,各个space情况下获利都为0
let table = [firstRaw]
for (let i = 1; i < goods.length; i++) { // 从第1个物品开始
for (let j = 0; j <= space; j++) { // 背包空间0-space
if (j === 0) {
table.push([])
}
if (goods[i].weight <= j) { // 装得下
// table[i - 1][j - goods[i].weight] + goods[i].value ------------装第i个物品,最优解为: 剩余空间 j - goods[i].weight, 物品i-1 时的储存值+goods[i].value
// table[i - 1][j] 空间j,物品i-1 时储存值
let max = Math.max(table[i - 1][j - goods[i].weight] + goods[i].value, table[i - 1][j])
table[i].push(max)
} else { // 装不下
table[i].push(table[i - 1][j])
}
}
}
console.log(table)
/*
[
[ 0, 0, 0, 0, 0, 0 ],
[ 0, 6, 6, 6, 6, 6 ],
[ 0, 6, 10, 16, 16, 16 ],
[ 0, 6, 10, 16, 18, 22 ]
]
*/
return table[goods.length - 1][space]
}
let goods = [
{
value: 6,
weight: 1
},
{
value: 10,
weight: 2
},
{
value: 12,
weight: 3
}
]
console.log(bag(goods, 5)) // 22
作者:疯狂吸猫
链接:https://www.jianshu.com/p/8ac893abf2ca
来源:简书
0 ],
[ 0, 6, 6, 6, 6, 6 ],
[ 0, 6, 10, 16, 16, 16 ],
[ 0, 6, 10, 16, 18, 22 ]
]
*/
return table[goods.length - 1][space]
}
let goods = [
{
value: 6,
weight: 1
},
{
value: 10,
weight: 2
},
{
value: 12,
weight: 3
}
]
console.log(bag(goods, 5)) // 22
作者:疯狂吸猫
链接:https://www.jianshu.com/p/8ac893abf2ca
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。