动态规划
90% 的字符串问题都可以用动态规划解决,并且90%是采用二维数组。
一、动态规划的三大步骤
动态规划,无非就是利用历史记录,来避免我们的重复计算。而这些历史记录,我们得需要一些变量来保存,一般是用一维数组或者二维数组来保存。
动态规划常常适用于有重叠子问题和最优子结构性质的问题,动态规划方法所耗时间往往远少于朴素解法。
最优子结构指的是,问题的最优解包含子问题的最优解。反过来说就是,我们可以通过子问题的最优解,推导出问题的最优解。
动态规划题三个重要步骤:
1、定义数组元素的含义。
上面说了,我们会用一个数组,来保存历史数组,假设用一维数组 dp[] 吧。这个时候有一个非常非常重要的点,就是规定你这个数组元素dp[i]的含义。一般来说dp[i]直接与所求答案关联。
2、找出数组元素直接的关系式(状态转移方程)
动态规划的题,就是把一个规模比较大的问题分成几个规模比较小的问题,然后由小的问题推导出大的问题。
大部分情况下,dp[i] [j] 和 dp[i-1] [j]、dp[i] [j-1]、dp[i-1] [j-1] 肯定存在某种关系。我们可以从最后一步、倒数第二步等方面入手分析。
3、找出初始值
动态规划类似于数学归纳法,我们需要知道初始值,才能不断地推下去。一般来说,如果是一维数组,初始值一般为为dp[0] ,dp[1],dp[2]等;如果是二维数组,一般为dp[0] [0] ,dp[i] [0] (i>=1) ,dp[0] [j] (j>=1)等
在做题的时候,按照以上三个步骤进行刻意练习,达到融会贯通!
二、案例分析
案例一:跳台阶
题目
一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。
答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
示例 1:
输入:n = 2
输出:2
示例 3:
输入:n = 0
输出:1
解题思路:
题目分析:青蛙每次有两种选择,跳一级或两级,当青蛙跳上第n级的前一步,青蛙有两种状态,即在第n-1级或第n-2级,那么当青蛙跳上第n级的跳法 = 第n-1级跳法 + 第 n-2 级跳法。符合最优子结构性质,使用动规。
使用刻意练习进行三大步骤思考:
1、数组元素的含义:dp[i]表示,当青蛙跳上第i级台阶时的跳法数量。我们所求答案为dp[n];
2、状态转移方程:dp[i] = dp[i-1] + dp[i-2] (i>=2)
3、初始值:dp[0] =1 dp[1]=1
代码如下:
var numWays = function (n) {
//特判
if (n <= 1) {
return 1;
}
let dp = new Array(n + 1);
dp[0] = 1;
dp[1] = 1;
for (let i = 2; i <= n; i++) {
dp[i] = (dp[i - 1] + dp[i - 2]) % 1000000007;
}
return dp[n];
}
案例二:路径数
题目
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
示例 1:
输入:m = 3, n = 7
输出:28
解题思路:
对题目进行分析:题目要求机器人到达右下角的路径数,当机器人到达右下角的前一步,只能在右下角上方和右下角左方,这个时候到达右下角的路径数 = 右下角上方路径数 + 右下角左方路径数。
使用刻意练习的思路思考:
1、定义数组元素含义。dp[i] [j] 表示机器人到达(i , j) 坐标时的路径数。我们所求为dp[m-1] [n-1]。
2、状态转移方程:dp[i] [j] = dp[i-1] [j] + dp[i] [j-1];
3、初始值:
- 第一行,机器人只能向右走,此时dp[0] [i] = 1 ( i>=0)
- 第一列,机器人只能向下走,此时dp [i] [0] = 1 (i>=0)
代码如下:
var uniquePaths = function (m, n) {
let dp = new Array(m);
for (let i = 0; i < m; i++) {
dp[i] = new Array(n);
}
//初始化
for (let i = 0; i < m; i++) {
dp[i][0] = 1;
}
for (let i = 0; i < n; i++) {
dp[0][i] = 1;
}
//状态转移
for (let i = 1; i < m; i++) {
for (let j = 1; j < n; j++) {
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
return dp[m - 1][n - 1];
}
案例三:最小路径和
题目
给定一个包含非负整数的 m x n
网格 grid
,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
**说明:**每次只能向下或者向右移动一步。
示例 1:
输入:grid = [[1,3,1],[1,5,1],[4,2,1]]
输出:7
解释:因为路径 1→3→1→1→1 的总和最小。
解题思路:
对题目进行分析:求从左上角到右下角的路径和最小,到达右下角的前一步为右下角上方或右下角左方,则右下角的路径和最小 = Min(右下角上方路径和,右下角左方路径和) + 右下角的值。
使用刻意练习的思路思考:
1、定义数组元素的含义。dp[i] [j] 为机器人到达(i , j )位置的路径最小和。我们所求为dp [m-1] [n-1]。
2、状态转移方程:dp[i] [j] = Math.min(dp[i-1] [j] , dp[i] [j-1]) +grid[i] [j]。(i>=1,j>=1)
3、初始值:
- dp[0] [0] = grid[0] [0]
- 第一行,机器人只能往右走,路径最小和为前一个格子路径最小和 + 当前格子的值。dp[0] [i] = dp[0] [i-1] + grid[0] [i ] (i>=1)
- 第一列,机器人只能往下走。dp[i] [0] = dp[i-1] [0] + grid[i] [0] (i>=1)
代码如下:
var minPathSum = function (grid) {
let m = grid.length
let n = grid[0].length;
let dp = new Array(m);
for (let i = 0; i < m; i++) {
dp[i] = new Array(n);
}
//初始化
dp[0][0] = grid[0][0];
for (let i = 1; i < n; i++) {
dp[0][i] = dp[0][i - 1] + grid[0][i];
}
for (let i = 1; i < m; i++) {
dp[i][0] = dp[i - 1][0] + grid[i][0];
}
//状态转移方程
for (let i = 1; i < m; i++) {
for (let j = 1; j < n; j++) {
dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j]
}
}
return dp[m - 1][n - 1];
}
案例四:编辑距离
题目
给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数。
你可以对一个单词进行如下三种操作:
- 插入一个字符
- 删除一个字符
- 替换一个字符
示例 1:
输入:word1 = "horse", word2 = "ros"
输出:3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')
示例 2:
输入:word1 = "intention", word2 = "execution"
输出:5
解释:
intention -> inention (删除 't')
inention -> enention (将 'i' 替换为 'e')
enention -> exention (将 'n' 替换为 'x')
exention -> exection (将 'n' 替换为 'c')
exection -> execution (插入 'u')
提示:
- 0 <= word1.length, word2.length <= 500
- word1 和 word2 由小写英文字母组成
解题思路:
对题目进行分析:要将word1 转换成 word2 ,每次我们有三种选择 插入、删除、替换,求最少操作次数,整个单词转换的最少操作次数依赖于前面单词转换的最少次数,符合最优子结构,可以使用动态规则。
按照刻意练习的思路进行思考:
1、定义数组元素的含义。dp[i] [j]表示,长度为i的word1 转换成长度为j的word2所需要的的最少操作数。我们所求的答案为dp[len1] [len2] (len1 = word1.length ; len2 = word2.length)。
2、状态转移方程。我们需要对word进行分析,如果word1[i-1] 和 word2[j-1]相等,不用做任何操作;如果不等,我们需要从增删改中进行选择操作数最少的。
注意!!!长度为i ,最后一个字符为word1[i-1]!!!
-
如果word1[i-1] === word2[j-1] ,则dp[i] [j] = dp[i-1] [j-1];
-
如果word1[i-1] !== word2[j-1]
-
选择在i后插入一个word2[j-1],dp[i] [j] = dp[i] [j-1] +1 ;
理解:在i后插入一个word2[j],则需要保证前i个字符与j-1个字符相同。
-
选择删除word1[i] ,dp[i] [j] = dp[i-1] [j]+1;
-
选择替换字符,word1[i-1]变为word2[j-1],dp[i] [j] =dp[i-1] [j-1] +1;
选择操作数最少,即dp[i] [j] = Math.min(dp[i] [j-1] ,dp[i-1] [j] ,dp[i-1] [j-1]) +1;
-
3、初始值:
- dp[0] [0] = 0;
- 其中一个单词为空串时,就是一直进入删除或插入操作
- dp[0] [i] = dp[0] [i-1] +1 ;(i >=1) 不断插入字符
- dp[i] [0] = dp[i-1] [0] +1 ;(i >=1) 不断删除字符
代码如下:
var minDistance = function (word1, word2) {
let len1 = word1.length;
let len2 = word2.length;
let dp = new Array(len1 + 1);
for (let i = 0; i <= len1; i++) {
dp[i] = new Array(len2 + 1);
}
//初始化
dp[0][0] = 0;
for (let i = 1; i <= len1; i++) {
dp[i][0] = dp[i - 1][0] + 1;
}
for (let i = 1; i <= len2; i++) {
dp[0][i] = dp[0][i - 1] + 1;
}
//状态转移
for (let i = 1; i <= len1; i++) {
for (let j = 1; j <= len2; j++) {
if (word1[i - 1] === word2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = Math.min(dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]) + 1;
}
}
}
return dp[len1][len2];
};
console.log(minDistance("horse", "ros"));