动态规划的三大步骤
动态规划就是利用历史记录,来避免重复计算。而这些历史记录需要一些变量来保存,一般使用一维数组或二维数组来保存。
(1)第一步:定义数组元素的含义
(2)第二步:找出数组元素之间的关系式,当我们计算dp[n]
时,是可以利用dp[n-1]
、dp[n-2]
…dp[1]
来退出dp[n]
的,也就是可以利用历史数据来推出新的元素值,所以需要找出数组元素之间的关系式,例如dp[n] = dp[n-1] + dp[n-2]
这一步也是最难的一步
(3)第三步:找出初始值,虽然知道了数组元素之间的关系式,例如dp[n] = dp[n-1] + dp[n-2]
,但是需要知道初始值,即直接获得dp[2]
和dp[1]
的值,这就是所谓的初始值
有了初始值,有了数组元素之间的关系水,就可以得到dp[n]
的值了,dp[n]
的含义是我们自己定义的,想求什么,就定义它是什么
实例
例1:简单的一维DP
问题描述:一只青蛙可以跳上1级台阶,也可以跳上2级台阶,求该青蛙跳上一个
n
级的台阶总共有多少种跳法
(1)定义数组元素含义
首先定义dp[n]
的含义:跳上一个n
级的台阶总共有dp[n]
种跳法,这样计算出dp[n]
后就可以得到答案
(2)找出数组元素之间的关系式
动态规划就是把一个规模较大的问题,分成几个规模较小的问题,然后由小的问题推导出大的问题。也就是说,dp[n]
的规模是n
,比它规模小的是n-1
、n-2
…,也就是说dp[n]
一定会和dp[n-1]
、dp[n-2]
…存在某种关系的,我们要找出它们之间的关系
这是最核心也最难的一步
对于这道题,青蛙可以选择跳一级,也可以跳两级,所以青蛙到达第n
级的台阶有两种方式:
- 从
n-1
级跳上来 - 从
n-2
级跳上来
所以dp[n] = dp[n-1] + dp[n-2]
就是数组元素之间的关系式
(3)找出初始条件
dp[1]
和dp[0]
都需要直接给出初始值,显然dp[1]
等于1
(因为1级的台阶只有1种跳法),dp[0]
等于0
(0级的台阶自然没有跳法)
(4)初始条件的严谨性
注意,如果按照之间的关系式,dp[2]
的结果是1
,显然这是不对的,dp[2]
的结果应该是2
,所以dp[2]
也应该作为一个初始值存在
也就是说,在寻找初始值的时候,一定要注意不要找漏了,这个只能通过不断做题来积累经验
(5)写代码:这样就可以写出代码:
function fn(n) {
if (n <= 2) {
return n;
}
let dp = [0, 1, 2];
for (let i = 3; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
例2:不同路径
问题描述:一个机器人位于一个
m x n
的网格的左上角,机器人每次只能向下或向右移动一步,机器人试图达到网格的右下角,总共有多少条不同的路径
(1)定义数组元素含义
我们的目的是从左上角到右下角一共有多少条路径,那我们就定义dp[i][j]
的含义是,当机器人从左上角走到[i, j]
这个位置时,一共有dp[i][j]
条路径
那么dp[m-1][n-1]
就是我们要的答案(因为网格相当于一个二维数组,数组下标是从0
开始计算的,所以右下角的位置是[m-1, n-1]
)
(2)找出数组元素之间的关系式
机器人如何才能到达[i, j]
这个位置?由于机器人可以向下走或者向右走,所以有两种方式到达:
- 从
[i - 1, j]
这个位置一步到达 - 从
[i, j - 1]
这个位置一步到达
所以关系是是dp[i][j] = dp[i-1][j] + dp[i][j-1]
(3)找出初始值
在dp[i][j]
中,如果i
或者[j]
有一个为0
,就不能使用关系式了,因为数组的下标会成为负数了。所以初始值计算出所有dp[0][0...n-1]
和dp[0...m-1][0]
的值,这个值都是1
,因为相当于上图中的第一行和第一列:
dp[0][0...n-1]
,第一行,机器人只能是从左侧过来(向右走一步),所以只有1
条路径过来dp[0...m-1][0]
,第一列,机器人只能是从上侧过来(向下走一步),所以只有1
条路径过来
(4)写代码
function uniquePath(m, n) {
if (m <= 0 || n <= 0) {
return 0;
}
const dp = [];
for (let i = 0; i < m; i++) {
dp[i] = [];
for (let j = 0; j < n; j++) {
if (i === 0 || j === 0) {
dp[i][j] =1;
} else {
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
}
return dp[m - 1][n - 1];
}
例3:最小路径和
问题描述:给定一个包含非负整数的 m x n 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
输入:
arr = [
[1,3,1],
[1,5,1],
[4,2,1]
]
输出: 7
解释: 因为路径 1→3→1→1→1 的总和最小。
(1)定义数组元素含义
定义dp[i][j]
含的含义为,当机器人从左上角走到[i][j]
这个位置时,最小的路径和是dp[i][j]
,那么dp[m-1][n-1]
就是我们要的答案
(2)找出数组元素之间的关系式
机器人达到[i, j]
这个位置,有两种选择,向下走或者向右走,所以有两种方式到达
- 从
[i - 1, j]
这个位置一步走达 - 从
[i, j - 1]
这个位置一步走达
这次计算的是路径和最小的,所以需要从上面两种方式中选择一种,是的dp[i][j]
是最小的,所以得到关系式:
dp[i][j] = min(dp[i - 1, j], dp[i, j - 1]) + grid[i][j]; // grid[i][j]表示当前网格的值
(3)找出初始值
- 当
i
和j
都为0
时,dp[i][j]
就是grid[0][0]
当前值 - 当
i
为0
时,dp[0][j]
只能是向右走过来的,所以dp[0][j]
就是dp[i][j - 1] + grid[0][j]
- 同样当
j
为0
时,dp[i][0]
只能是向下走过来的,所以dp[i][0]
就是dp[i - 1][0] + grid[i][0]
(4)写代码
var minPathSum = function (grid) {
const m = grid.length,
n = grid[0].length;
const dp = [];
for (let i = 0; i < m; i++) {
dp[i] = [];
for (let j = 0; j < n; j++) {
if (i === 0 && j === 0) {
dp[0][0] = grid[0][0];
} else if (i === 0) {
dp[0][j] = dp[0][j - 1] + grid[0][j];
} else if (j === 0) {
dp[i][0] = dp[i - 1][0] + grid[i][0];
} else {
dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
}
}
}
return dp[m - 1][n - 1];
};
例4:编辑距离
给定两个单词 word1 和 word2,计算出将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
插入一个字符 删除一个字符 替换一个字符
示例:
输入: word1 = "horse", word2 = "ros"
输出: 3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')
90%的字符串问题都可以使用动态规划解决,都是利用二维数组解决
(1)定义数组元素含义
目的是求将word1
转换word2
使用的最小操作数,那么定义dp[i][j]
的含义为:当字符串word1
长度为i
,字符串word2
长度为j
时,将word1
转换为word2
所用的最小操作次数
我在尝试的时候犯了一个错误,以为最终求的是dp[i - 1][j - 1]
,但是因为i``j
代表的是就是字符串长度,而不是数组下标,所以最终求的就是dp[i][j]
(2)找出数组元素之间的关系式
对word
可以进行替换一个字符、插入一个字符、删除一个字符这三种操作,分这两种情况进行讨论
-
word1[i]
与word2[j]
相等,这时候不需要任何操作,所以有dp[i][j] = dp[i - 1][j - 1]
-
word1[i]
与word2[j]
不相等,这时候需要使用上述三个操作进行处理- 将
word1[i]
替换为word2[j]
,这个时候只需要word1
与word2
都发生了变化,所以有dp[i][j] = dp[i - 1][j - 1] + 1
- 在
word1
后插入一个与word2[j]
相等的字符,相当于word2
发生了变化,所以有dp[i][j] = dp[i - 1][j] + 1
- 把
word1
删除一个字符,相当于word1
发生了变化,所以有dp[i][j] = dp[i - 1][j] + 1
- 应该上面三个当中之一的一种操作,是的
dp[i][j]
的值最小,所以得到关系式dp[i] [j] = min(dp[i-1] [j-1],dp[i] [j-1],dp[[i-1] [j]]) + 1
- 将
(3)找出初始值
当dp[i][j]
当中i
或j
为0
,关系式是不成立的,所以需要计算出所有初始值dp[0][0...n]
和dp[0...n][0]
的值,这种情况下某一个字符串长度为0
,转换为另外一个字符串,只能一直进行插入或者删除操作
(4)写代码
var minDistance = function (word1, word2) {
const length1 = word1.length;
const length2 = word2.length;
// 某一个字符串长度为 0 时的情况
if (length1 * length2 === 0) {
return length1 + length2;
}
const dp = [];
for (let i = 0; i <= length1; i++) {
dp[i] = [];
for (let j = 0; j <= length2; j++) {
if (i === 0 && j === 0) {
dp[0][0] = 0;
} else if (i === 0) {
dp[0][j] = dp[0][j - 1] + 1;
} else if (j === 0) {
dp[i][0] = dp[i - 1][0] + 1;
} else {
if (word1[i - 1] === word2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = Math.min(dp[i - 1][j - 1], dp[i][j - 1], dp[i - 1][j]) + 1;
}
}
}
}
return dp[length1][length2];
};
优化
优化的核心:画图
目标:将${O(m * n)}$
的空间复杂度优化为${O(n)}$
例2不同路径数的优化
这道题目的转移公式是:
dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
当前的空间复杂度是${O(m * n)}$
,实际上在计算dp[i][j]
时,在i
维度上(即按行计算时),只需要保存i - 1
这一行数据即可,而[0, i - 2]
这些数据实际上是没有意义的,也就不需要保存
所以可以将二维数组转换为一维数组,然后通过迭代,不断更新这个一维数组的值即可
var uniquePaths = function (m, n) {
if (m <= 0 || n <= 0) {
return 0;
}
// 初始化第一行的值
let dp = new Array(n).fill(1);
// 从第二行开始遍历
for (let i = 1; i < m; i++) {
// 初始化这一行第一个单元格的值
dp[0] = 1;
// 从第二列开始遍历
for (let j = 1; j < n; j++) {
// 等式左边的`dp[j]`是第`i`行的单元格的值,而等式右边的值则是上一行即`i - 1`行保存的单元格的值
dp[j] = dp[j - 1] + dp[j];
}
}
// 返回最后一个单元格的值
return dp[n - 1];
};
例4编辑距离优化
与上一个相比,需要声明一个临时变量pre
将dp[i - 1][j - 1]
保存起来,同时要注意的就是初始的边界条件,不是1
,而是随着迭代而变化的,另外pre
的边界条件,也需要一个单独的变量temp
来保存(我之前就是因为对pre
的初始值考虑的不正确,怎么都做不出来)
var minDistance = function (word1, word2) {
const m = word1.length,
n = word2.length;
// 如果有一个字符串为空字符串,返回值就是为为空字符串的长度
if (m * m === 0) {
return m + n;
}
const dp = [];
for (let j = 0; j <= n; j++) {
dp[j] = j;
}
for (let i = 1; i <= m; i++) {
// temp 的作用就是给 pre 赋初值
let temp = dp[0];
dp[0] = i;
for (let j = 1; j <= n; j++) {
let pre = temp;
temp = dp[j];
if (word1[i - 1] === word2[j - 1]) {
dp[j] = pre;
} else {
dp[j] = Math.min(dp[j - 1], dp[j], pre) + 1;
}
}
}
return dp[n];
};