19-动态规划----JavaScript数据结构与算法学习

该系列博客索引目录:数据结构与算法—前端JavaScript学习

动态规划

参考:算法分析与设计-贪心&动归

漫画:什么是动态规划?

【数据结构与算法】 DP 动态规划 介绍

介绍:

随便看看,不看也行,看不懂很正常,看几个示例就懂了

动态规划是一种编程思想,主要用于解决最优解类型的问题。

其思路是为了求解当前的问题的最优解,使用子问题的最优解,然后综合处理,最终得到原问题的最优解。

但是也不是说任何最优解问题都可以使用动态规划,使用dp的问题一般满足下面的两个特征:

(1)最优子结构,就是指问题可以通过子问题最优解得到;体现为找出所有的子问题最优解,然后取其中的最优;

(2)重叠子问题,就是子问题是会重复的。而不是一直产生新的子问题(比如分治类型的问题)。

一般而言,满足上述两个条件的最优解问题都可以会使用DP来解决。

相关概念:

最优子结构、边界、状态转移公式。(先随便看以下,后面会讲)

1.斐波那契数列:

一维的动态规划与斐波那契数列的思想、代码类似,首先介绍斐波那契数列。

leetcode-509. 斐波那契数

斐波那契数,通常用 F(n) 表示,形成的序列称为斐波那契数列。该数列由 01 开始,后面的每一项数字都是前面两项数字的和。也就是:

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一维动态规划例子:

leetcode-70. 爬楼梯

假设你正在爬楼梯。需要 10阶(不同于leetcode种的n阶,暂考虑为10)你才能到达楼顶。

每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

img

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.不同路径例子:

leetcode-62. 不同路径

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。

问总共有多少条不同的路径?

img

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.性能优化:

img

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为物品单位重量的价值。商店物品信息如下表

img

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的价值。

再比较装与不转的谁的获利多

所以背包问题是一个二维的动态规划问题,不仅仅是物品维度,也是空间维度的。

从左到右从上到下的列举出不同空间,物品量的表,完成到右下角时问题解决。

袋子容积:1kg2kg3kg4kg5kg
添加第1个1kg物品66666
第2个2kg物品610161616
第3个3kg物品610161822

状态转移公式: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

下图为求解过程中储存中间值的表格,与之前的表格相对应

img

储存中间值的表格

由于状态转移公式中涉及到了n-1,物品数又是从1开始的,为了能获取到F(0,space-goods[1].weight)

所以在table中添加一行[0,0,0,0,0,0]作为边界,表示没有物品时各个空间值对应的解,以供有1个物品时能使用状态转移公式

F(n-1,space-goods[n].weight)中会出现spacegoods[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
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值