动态规划(Dynamic Programming,DP)是指在给定的约束条件下求最优值的算法,在解决问题的过程,需要经历多个决策阶段,每个决策阶段都对应着一组状态。
  适用于动态规划解决的问题包含三个特征:
  (1)最优子结构:通过子问题的最优解,可推导出问题的最优解,即后面阶段的状态可以通过前面阶段的状态推导出来。
  (2)无后效性:某阶段状态一旦确定,就不受之后阶段的决策影响。即子问题的解一旦确定,就不再改变。
  (3)子问题重叠:不同的决策序列,到达某个相同的阶段时,可能会产生重复的状态。即有些子问题会被重复计算多次。
  动态规划对每个子问题只计算一次,然后将其计算结果保存到一张表中记忆化存储,以便下次需要同一个子问题解时直接查表,从而获得较高的效率,降低了时间复杂度。
## 一、0-1背包
  在之前《[回溯](https://www.cnblogs.com/strick/p/13384038.html)》一文中也提到了0-1背包问题,而此处会在背包重量限制的前提下,计算装入物品的最大总价值。
  假设背包容量为4斤,吉他(1斤,价值1500)、音响(4斤,价值3000)和笔记本(3斤,价值2000),求背包的最大价值(题目来源于《图解算法 9.1节》)。
  先画状态转移表(如下所示),一般是二维的,在画之前需要回答三个问题:
  (1)单元格中的值是什么?当前背包中的最大总价值。
  (2)如何划分子问题?考虑容量为1、2、3和4的背包,并且将物品依次放入。
  (3)网格的坐标轴是什么?横坐标是背包重量,纵坐标是物品。
  接下来将计算每个单元格的值。
  (1)第一步是将吉他放入背包的四个重量中,而重量1、2和3其实就是在解决各个子问题。
  (2)第二步是依次处理音响,判断是否需要放入,经过比对发现只有最大容量才能放入,更新最大价值为3000。
  (3)第三步是依次处理笔记本,在背包容量为3斤时更新最大价值为2000,而在4斤时,可同时放入吉他和笔记本,更新最大价值为3500。
:-: ![](https://img.kancloud.cn/9e/76/9e761f24f2cfb80672e62818f864d96f_759x419.png =400x)
  根据状态表得出状态转移方程,先计算当前商品价值和剩余空间价值,得到的结果与上一个单元格比对,将较大值填充到当前单元格中。
~~~
dp[i][j] = max(goods[i].value + dp[i-1][j-goods[i].weight], dp[i-1][j])
~~~
  具体的代码实现[如下所示](https://codepen.io/strick/pen/bGEyJQq),本文的代码仅做参考,没有经过严格的测试用例论证。
~~~
function knapsack(goods, w) {
let max = Number.MIN_VALUE,
dp = [new Array(w)],
length = goods.length;
for (let j = 1; j <= w; j++) {
dp[0][j] = goods[0].value;
}
for (let i = 1; i < length; i++) {
dp[i] = [];
for (let j = 1; j <= w; j++) {
let rest = j - goods[i].weight; //计算减去当前商品重量后的背包容量
if (rest > 0) { //套用状态转移方程
dp[i][j] = Math.max(goods[i].value + dp[i - 1][rest], dp[i - 1][j]);
} else {
dp[i][j] = dp[i - 1][j]; //沿用上一个单元格的价值
}
if (max < dp[i][j]) max = dp[i][j]; //计算最大值
}
}
return max;
}
~~~
## 二、子串和子序列
**1)最长公共子串**
  最长公共子串是指在不改变字符相对顺序的情况下提取两段字符串中共有的子串,例如fosh和fish,最长公共子串是sh,长度为2(题目来源于《图解算法 9.3节》)。例题:[300\. 最长上升子序列](https://leetcode-cn.com/problems/longest-increasing-subsequence/)。
  在画状态表之前先回答三个问题:
  (1)单元格中的值是什么?公共子串的长度。
  (2)如何划分子问题?将两段字符串分割成字符,从前往后依次比对。
  (3)网格的坐标轴是什么?横坐标是第一段字符串,纵坐标是第二段字符串。
:-: ![](https://img.kancloud.cn/89/e1/89e1bb80b89a31f8d8f3e120d1004d76_759x532.png =400x)
  根据状态表得出状态转移方程,当两个字符相同时,左上角的单元格加一,否则单元格为0。
~~~
dp[i][j] = 0 | dp[i-1][j-1] + 1
~~~
  具体的代码实现[如下所示](https://codepen.io/strick/pen/jOWooxe)。
~~~
function longestCommonSubstr(str1, str2) {
let len1 = str1.length,
len2 = str2.length,
max = Number.MIN_VALUE,
dp = [];
for (let i = 0; i < len1; i++) {
dp[i] = [];
for (let j = 0; j < len2; j++) {
if (str1[i] != str2[j]) { //两个字符不同
dp[i][j] = 0;
continue;
}
//应对 i-1 或 j-1 小于0的情况
dp[i][j] = i < 1 || j < 1 ? 1 : dp[i - 1][j - 1] + 1;
if (max < dp[i][j]) {
max = dp[i][j];
}
}
}
return max;
}
~~~
**2)最长公共子序列**
  还有一个类似的题目是求最长公共子序列,其实就是提取共有的子序列,例如fosh和fish,最长公共子序列是fsh,例题:[1143\. 最长公共子序列](https://leetcode-cn.com/problems/longest-common-subsequence/)。状态转移表如下所示。
:-: ![](https://img.kancloud.cn/74/f2/74f2fa779d184a0980a792de5c2e5f95_759x532.png =400x)
  根据状态表得出状态转移方程,当两个字符相同时,仍然是左上角的单元格加一,否则比较左和上两个单元格的值,取较大值。
~~~
dp[i][j] = (dp[i-1][j-1] + 1) | max(dp[i-1][j], dp[i][j-1])
~~~
  具体的代码实现[如下所示](https://codepen.io/strick/pen/yLeWdOB)。
~~~
function longestCommonSubsequence(str1, str2) {
let len1 = str1.length,
len2 = str2.length,
max = Number.MIN_VALUE,
dp = [];
for (let i = 0; i < len1; i++) {
dp[i] = [];
for (let j = 0; j < len2; j++) {
if (str1[i] != str2[j]) {
//两个字符不同
dp[i][j] = Math.max(
i < 1 ? 0 : dp[i - 1][j],
j < 1 ? 0 : dp[i][j - 1]
);
max = Math.max(max, dp[i][j]);
continue;
}
//应对 i-1 或 j-1 小于0的情况
dp[i][j] = i < 1 || j < 1 ? 1 : dp[i - 1][j - 1] + 1;
max = Math.max(max, dp[i][j]);
}
}
return max;
}
~~~
**3)最长上升子序列**
  求出一个无序的整数数组中最长上升子序列的长度。例题:[300\. 最长上升子序列](https://leetcode-cn.com/problems/longest-increasing-subsequence/)。
  网上的解法都是用一维状态转移方程,此处仍然采用二维的解法(可观察各个步骤),其中dp\[i\]\[j\]是指以第 i 个元素结尾的前 j 个元素中的最长上升子序列。
  状态表如下所示,每次只计算dp\[i\]\[i\]的值,其余值沿用上一行的结果。
:-: ![](https://img.kancloud.cn/34/3b/343b6510a951c195cc132b28430cabdc_1062x760.png =500x)
  根据状态表得出状态转移方程,当第 i 个元素比第 j 个元素大时,在该值的基础上加一,否则默认赋为一。
~~~
dp[i][i] = 1 | max(dp[i][j]) + 1
~~~
  具体的代码实现[如下所示](https://codepen.io/strick/pen/ExPBYRx)。
~~~
function lengthOfLIS(nums) {
let len = nums.length,
max = 1,
dp = [];
dp[0] = [1]; //初始化dp[0][0]的值
for (let i = 1; i < len; i++) {
dp[i] = [];
dp[i][i] = 1; //初始化dp[i][i]的值
for (let j = 0; j < i; j++) { //让第i个元素与前j个元素逐一比较
dp[i][j] = dp[i-1][j]; //默认沿用上一行的值
if (nums[i] > nums[j]) { //当第i个元素比第j个元素大时,取其中的较大值
dp[i][i] = Math.max(dp[i][i], dp[i][j] + 1);
}
}
max = Math.max(max, dp[i][i]);
}
return max;
}
~~~
## 三、钱币找零
  在《[贪心算法](https://www.cnblogs.com/strick/p/13391105.html)》一节中曾提到过钱币找零的问题,此处会加大难度。
  假设有几种不同面值的纸币 v1,v2,……,vn(单位是元),如果要支付 w 元,那么最少需要多少张纸币,例如有 3 种不同的纸币,1元、2元、5元,要支付 9元,最少需要 3张纸币。例题:[322\. 零钱兑换](https://leetcode-cn.com/problems/coin-change/)。
  在画状态表之前先回答三个问题:
  (1)单元格中的值是什么?最少纸币数。
  (2)如何划分子问题?考虑要支付1、2...9等金额时,各自需要的最少纸币数。
  (3)网格的坐标轴是什么?横坐标是支付金额,纵坐标是可用的纸币。
:-: ![](https://img.kancloud.cn/46/6f/466f7c168d3564c7c3f5022056facbed_1516x504.png =800x)
  根据状态表得出状态转移方程,计算金额减去当前面值的剩余金额所用的最少纸币数,将其加一和上一行的最少纸币数做比较,取小值。
~~~
dp[i][j] = min(dp[i-1][j], dp[i][j - coins[i]] + 1)
~~~
  具体的代码实现[如下所示](https://codepen.io/strick/pen/NWxZgZW),虽然代码比较长,但好多都是在判断边界条件,以及做各类情况的处理,核心其实还是围绕着状态转移方程。
~~~
function coinChange(coins, amount) {
let len = coins.length,
min = Number.MAX_VALUE,
dp = [new Array(amount)];
for (let j = 1; j <= amount; j++) { //初始化第一行
//纸币面值比金额大,或金额无法整除纸币
if(coins[0] > j || (j % coins[0]) > 0) {
//对于无法支付金额的情况,赋值为0
dp[0][j] = 0;
continue;
}
dp[0][j] = j / coins[0]; //得到纸币数量
}
for (let i = 1; i < len; i++) {
dp[i] = [];
for (let j = 1; j <= amount; j++) {
let rest = j - coins[i];
if(rest == 0) { //可用当前纸币支付金额
dp[i][j] = 1;
continue;
}
if(rest < 0) { //当前纸币比支付金额大
dp[i][j] = dp[i-1][j];
continue;
}
if(dp[i-1][j] > 0 && dp[i][rest] > 0) { //都可以支付金额
dp[i][j] = Math.min(dp[i-1][j], dp[i][rest] + 1);
}else if(dp[i-1][j] > 0) { //只有上一行可以支付金额
dp[i][j] = dp[i-1][j];
}else if(dp[i][rest] > 0) { //只能借助剩余金额的纸币数支付
dp[i][j] = dp[i][rest] + 1;
}else { //无法支付
dp[i][j] = 0;
}
}
}
//取金额的最小值
for(let i = 0; i < len; i++) {
if(dp[i][amount] == 0) { //过滤掉无法支付的数据
continue;
}
if(dp[i][amount] > 0) {
min = Math.min(min, dp[i][amount]);
}
}
return min == Number.MAX_VALUE ? -1 : min;
}
~~~
*****
> 原文出处:
[博客园-数据结构和算法躬行记](https://www.cnblogs.com/strick/category/1809992.html)
已建立一个微信前端交流群,如要进群,请先加微信号freedom20180706或扫描下面的二维码,请求中需注明“看云加群”,在通过请求后就会把你拉进来。还搜集整理了一套[面试资料](https://github.com/pwstrick/daily),欢迎阅读。
![](https://box.kancloud.cn/2e1f8ecf9512ecdd2fcaae8250e7d48a_430x430.jpg =200x200)
推荐一款前端监控脚本:[shin-monitor](https://github.com/pwstrick/shin-monitor),不仅能监控前端的错误、通信、打印等行为,还能计算各类性能参数,包括 FMP、LCP、FP 等。
- ES6
- 1、let和const
- 2、扩展运算符和剩余参数
- 3、解构
- 4、模板字面量
- 5、对象字面量的扩展
- 6、Symbol
- 7、代码模块化
- 8、数字
- 9、字符串
- 10、正则表达式
- 11、对象
- 12、数组
- 13、类型化数组
- 14、函数
- 15、箭头函数和尾调用优化
- 16、Set
- 17、Map
- 18、迭代器
- 19、生成器
- 20、类
- 21、类的继承
- 22、Promise
- 23、Promise的静态方法和应用
- 24、代理和反射
- HTML
- 1、SVG
- 2、WebRTC基础实践
- 3、WebRTC视频通话
- 4、Web音视频基础
- CSS进阶
- 1、CSS基础拾遗
- 2、伪类和伪元素
- 3、CSS属性拾遗
- 4、浮动形状
- 5、渐变
- 6、滤镜
- 7、合成
- 8、裁剪和遮罩
- 9、网格布局
- 10、CSS方法论
- 11、管理后台响应式改造
- React
- 1、函数式编程
- 2、JSX
- 3、组件
- 4、生命周期
- 5、React和DOM
- 6、事件
- 7、表单
- 8、样式
- 9、组件通信
- 10、高阶组件
- 11、Redux基础
- 12、Redux中间件
- 13、React Router
- 14、测试框架
- 15、React Hooks
- 16、React源码分析
- 利器
- 1、npm
- 2、Babel
- 3、webpack基础
- 4、webpack进阶
- 5、Git
- 6、Fiddler
- 7、自制脚手架
- 8、VSCode插件研发
- 9、WebView中的页面调试方法
- Vue.js
- 1、数据绑定
- 2、指令
- 3、样式和表单
- 4、组件
- 5、组件通信
- 6、内容分发
- 7、渲染函数和JSX
- 8、Vue Router
- 9、Vuex
- TypeScript
- 1、数据类型
- 2、接口
- 3、类
- 4、泛型
- 5、类型兼容性
- 6、高级类型
- 7、命名空间
- 8、装饰器
- Node.js
- 1、Buffer、流和EventEmitter
- 2、文件系统和网络
- 3、命令行工具
- 4、自建前端监控系统
- 5、定时任务的调试
- 6、自制短链系统
- 7、定时任务的进化史
- 8、通用接口
- 9、微前端实践
- 10、接口日志查询
- 11、E2E测试
- 12、BFF
- 13、MySQL归档
- 14、压力测试
- 15、活动规则引擎
- 16、活动配置化
- 17、UmiJS版本升级
- 18、半吊子的可视化搭建系统
- 19、KOA源码分析(上)
- 20、KOA源码分析(下)
- 21、花10分钟入门Node.js
- 22、Node环境升级日志
- 23、Worker threads
- 24、低代码
- 25、Web自动化测试
- 26、接口拦截和页面回放实验
- 27、接口管理
- 28、Cypress自动化测试实践
- 29、基于Electron的开播助手
- Node.js精进
- 1、模块化
- 2、异步编程
- 3、流
- 4、事件触发器
- 5、HTTP
- 6、文件
- 7、日志
- 8、错误处理
- 9、性能监控(上)
- 10、性能监控(下)
- 11、Socket.IO
- 12、ElasticSearch
- 监控系统
- 1、SDK
- 2、存储和分析
- 3、性能监控
- 4、内存泄漏
- 5、小程序
- 6、较长的白屏时间
- 7、页面奔溃
- 8、shin-monitor源码分析
- 前端性能精进
- 1、优化方法论之测量
- 2、优化方法论之分析
- 3、浏览器之图像
- 4、浏览器之呈现
- 5、浏览器之JavaScript
- 6、网络
- 7、构建
- 前端体验优化
- 1、概述
- 2、基建
- 3、后端
- 4、数据
- 5、后台
- Web优化
- 1、CSS优化
- 2、JavaScript优化
- 3、图像和网络
- 4、用户体验和工具
- 5、网站优化
- 6、优化闭环实践
- 数据结构与算法
- 1、链表
- 2、栈、队列、散列表和位运算
- 3、二叉树
- 4、二分查找
- 5、回溯算法
- 6、贪心算法
- 7、分治算法
- 8、动态规划
- 程序员之路
- 大学
- 2011年
- 2012年
- 2013年
- 2014年
- 项目反思
- 前端基础学习分享
- 2015年
- 再一次项目反思
- 然并卵
- PC网站CSS分享
- 2016年
- 制造自己的榫卯
- PrimusUI
- 2017年
- 工匠精神
- 2018年
- 2019年
- 前端学习之路分享
- 2020年
- 2021年
- 2022年
- 2023年
- 日志
- 2020