动态规划
动态规划,英文:Dynamic Programming,简称DP,如果某一问题有很多重叠子问题,使用动态规划是最有效的。
所以动态规划中每一个状态一定是由上一个状态推导出来的,这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优的
动态规划的解题步骤
- 确定dp数组(dp table)以及下标的含义
- 确定递推公式
- dp 数组如何初始化
- 确定遍历顺序
- 举例推导 dp 数组
leetCode
1. 斐波那契数列
斐波那契数 (通常用 F(n)
表示)形成的序列称为 斐波那契数列 。该数列由 0
和 1
开始,后面的每一项数字都是前面两项数字的和
示例 1:
- 输入:2
- 输出:1
- 解释:F(2) = F(1) + F(0) = 1 + 0 = 1
示例 2:
- 输入:3
- 输出:2
- 解释:F(3) = F(2) + F(1) = 1 + 1 = 2
示例 3:
- 输入:4
- 输出:3
- 解释:F(4) = F(3) + F(2) = 2 + 1 = 3
动态规划 5 部曲
- 确定 dp 数组以及下标的含义
dp[i]
的定义为:第i
个数的斐波那契数值是dp[i]
- 确定递推公式
状态转移方程 dp[i] = dp[i - 1] + dp[i - 2]; - dp 数组如何初始化
题目中直接给我们了(该数列由
0
和1
开始)
dp[0] = 0;
dp[1] = 1;
-
确定遍历顺序
从递归公式dp[i] = dp[i - 1] + dp[i - 2]
中可以看出,dp[i]是依赖 dp[i - 1] 和 dp[i - 2],那么遍历的顺序一定是从前到后遍历的 -
举例推导dp数组
按照这个递推公式dp[i] = dp[i - 1] + dp[i - 2]
,我们来推导一下,当N为10的时候,dp数组应该是如下的数列:
0 1 1 2 3 5 8 13 21 34 55
代码如下
var fib = function(n) {
let dp = [0, 1]
for(let i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2]
}
return dp[n]
};
2. 爬楼梯
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
示例 1:
-
输入: 2
-
输出: 2
-
解释: 有两种方法可以爬到楼顶。
- 1 阶 + 1 阶
- 2 阶
示例 2:
-
输入: 3
-
输出: 3
-
解释: 有三种方法可以爬到楼顶。
- 1 阶 + 1 阶 + 1 阶
- 1 阶 + 2 阶
- 2 阶 + 1 阶
爬到第一层楼梯有一种方法,爬到二层楼梯有两种方法。
那么第一层楼梯再跨两步就到第三层 ,第二层楼梯再跨一步就到第三层。
所以到第三层楼梯的状态可以由第二层楼梯 和 到第一层楼梯状态推导出来,那么就可以想到动态规划了。
动态规划五部曲
- 确定dp数组以及下标的含义
dp[i]
:爬到第 i
层楼梯,有 dp[i]
种方法
- 确定递推公式
从 dp[i]
的定义可以看出,dp[i]
可以有两个方向推出来。
首先是 dp[i - 1]
,上 i-1
层楼梯,有dp[i - 1]
种方法,那么再一步跳一个台阶不就是 dp[i]
了么。
还有就是 dp[i - 2]
,上 i-2
层楼梯,有 dp[i - 2]
种方法,那么再一步跳两个台阶不就是 dp\[i]
了么。
那么 dp[i]
就是 dp[i - 1]
与 dp[i - 2]
之和!
所以 dp[i] = dp[i - 1] + dp[i - 2]
- dp数组如何初始化
不考虑 dp[0]
如何初始化(站在0层没有意义,而且题目中给定 n 是一个正整数),只初始化 dp[1] = 1,dp[2] = 2
,然后从 i = 3 开始递推,这样才符合 dp[i] 的定义。
-
确定遍历顺序
从递推公式dp[i] = dp[i - 1] + dp[i - 2]
;中可以看出,遍历顺序一定是从前向后遍历的 -
举例推导dp数组
举例当 n为5 的时候,dp table(dp数组)应该是这样的
代码如下
function climbStairs(n) {
/**
dp[i]: i阶楼梯的方法种数
dp[1]: 1;
dp[2]: 2;
...
dp[i]: dp[i - 1] + dp[i - 2];
*/
const dp = [];
dp[1] = 1;
dp[2] = 2;
for (let i = 3; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
};
3. 不同路径
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
示例 1:
- 输入:m = 3, n = 7
- 输出:28
示例 2:
- 输入:m = 2, n = 3
- 输出:3
解释: 从左上角开始,总共有 3 条路径可以到达右下角。
- 向右 -> 向右 -> 向下
- 向右 -> 向下 -> 向右
- 向下 -> 向右 -> 向右
示例 3:
- 输入:m = 7, n = 3
- 输出:28
示例 4:
- 输入:m = 3, n = 3
- 输出:6
动态规划五部曲
-
确定dp数组(dp table)以及下标的含义
dp\[i]\[j]
:表示从(0 ,0)出发,到(i, j)
有dp\[i]\[j]
条不同的路径。 -
确定递推公式
想要求 dp\[i]\[j]
,只能有两个方向来推导出来,即 dp\[i - 1]\[j]
和 dp\[i]\[j - 1]
。
此时再回顾一下 dp\[i - 1]\[j]
表示啥,是从(0, 0)的位置到 (i - 1, j) 有几条路径,dp\[i]\[j - 1]
同理。
那么很自然,dp[i][j] = dp[i - 1][j] + dp[i][j - 1],因为dp[i][j]只有这两个方向过来。
-
dp数组的初始化
如何初始化呢,首先dp\[i]\[0]
一定都是1
,因为从(0, 0)
的位置到(i, 0)
的路径只有一条,那么dp\[0]\[j]
也同理。所以初始化代码为:
for (let i = 0; i < m; i++) dp[i][0] = 1;
for (let j = 0; j < n; j++) dp[0][j] = 1;
-
确定遍历顺序
这里要看一下递推公式dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
,dp[i][j]
都是从其上方和左方推导而来,那么从左到右一层一层遍历就可以了。 -
举例推导dp数组
如图所示:
function uniquePaths(m: number, n: number): number {
/**
dp[i][j]: 到达(i, j)的路径数
dp[0][*]: 1;
dp[*][0]: 1;
...
dp[i][j]: dp[i - 1][j] + dp[i][j - 1];
*/
const dp: number[][] = new Array(m).fill(0).map(_ => []);
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];
};
4.不同路径||
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。
现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
网格中的障碍物和空位置分别用 1 和 0 来表示。
示例 1:
-
输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]
-
输出:2 解释:
-
3x3 网格的正中间有一个障碍物。
-
从左上角到右下角一共有 2 条不同的路径:
- 向右 -> 向右 -> 向下 -> 向下
- 向下 -> 向下 -> 向右 -> 向右
示例 2:
- 输入:obstacleGrid = [[0,1],[0,0]]
- 输出:1
在上文的不同路径中我们已经详细分析了没有障碍的情况,有障碍的话,其实就是标记对应的dp table(dp数组)保持初始值(0)就可以了。
这道题和上一道题很相似,只是多了障碍物这一个条件,根据 dp[i][j] 的定义,dp[i][j] 是表示从(0 ,0)出发,到(i, j) 有dp[i][j]条不同的路径,如果 碰到了障碍物,那么说明这条路走不通,要保持初始状态(0)
所以代码为
if (obstacleGrid[i][j] == 0) { // 当(i, j)没有障碍的时候,再推导dp[i][j]
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
- dp数组如何初始化
同样的,如果遇到了障碍物 ,则停止赋初始值
for (let i = 0; i < m && obstacleGrid[i][0] == 0; i++) dp[i][0] = 1;
for (let j = 0; j < n && obstacleGrid[0][j] == 0; j++) dp[0][j] = 1
代码如下
function uniquePathsWithObstacles(obstacleGrid: number[][]): number {
/**
dp[i][j]: 到达(i, j)的路径数
dp[0][*]: 用u表示第一个障碍物下标,则u之前为1,u之后(含u)为0
dp[*][0]: 同上
...
dp[i][j]: obstacleGrid[i][j] === 1 ? 0 : dp[i-1][j] + dp[i][j-1];
*/
const m: number = obstacleGrid.length;
const n: number = obstacleGrid[0].length;
const dp: number[][] = new Array(m).fill(0).map(_ => new Array(n).fill(0));
for (let i = 0; i < m && obstacleGrid[i][0] === 0; i++) {
dp[i][0] = 1;
}
for (let i = 0; i < n && obstacleGrid[0][i] === 0; i++) {
dp[0][i] = 1;
}
for (let i = 1; i < m; i++) {
for (let j = 1; j < n; j++) {
if (obstacleGrid[i][j] === 1) continue;
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
return dp[m - 1][n - 1];
};
5. 整数拆分
给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。 返回你可以获得的最大乘积。
示例 1:
- 输入: 2
- 输出: 1
- 解释: 2 = 1 + 1, 1 × 1 = 1。
示例 2:
- 输入: 10
- 输出: 36
- 解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。
- 说明: 你可以假设 n 不小于 2 且不大于 58。
动规五部曲,分析如下:
- 确定dp数组(dp table)以及下标的含义
dp[i]:分拆数字i
,可以得到的最大乘积为dp\[i]
。
2. 确定递推公式
一个是j* (i - j)
直接相乘。如果不能分割的话
另一个是dp[i-j]* j
,
那有同学问了,j 怎么就不拆分呢?
j是从1开始遍历,拆分j的情况,在遍历 j 的过程中其实都计算过了。那么从1遍历j,比较 (i - j) * j 和 dp[i - j] * j 取最大的。
递推公式:dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j))
;
也可以这么理解,j * (i - j)
是单纯的把整数拆分为两个数相乘,而 j * dp\[i - j]
是拆分成两个以及两个以上的个数相乘。
如果定义 dp[i - j] \* dp[j]
也是默认将一个数强制拆成4份以及4份以上 了。
所以递推公式:dp[i] = max({dp[i], (i - j) * j, dp[i - j] * j});
那么在取最大值的时候,为什么还要比较dp[i]呢?
因为在递推公式推导的过程中,每次计算dp[i],取最大的而已。
- dp的初始化
不少同学应该疑惑,dp[0] dp[1]应该初始化多少呢?
有的题解里会给出dp[0] = 1,dp[1] = 1的初始化,但解释比较牵强,主要还是因为这么初始化可以把题目过了。
严格从dp[i]的定义来说,dp[0] dp[1] 就不应该初始化,也就是没有意义的数值。
拆分0和拆分1的最大乘积是多少?
这是无解的。
这里我只初始化dp[2] = 1,从dp[i]的定义来说,拆分数字2,得到的最大乘积是1,这个没有任何异议!
- 确定遍历顺序
确定遍历顺序,先来看看递归公式:dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j));
dp[i] 是依靠 dp[i - j]的状态,所以遍历i一定是从前向后遍历,先有dp[i - j]再有dp[i]。\
点击查看 leetCode 题解
var integerBreak = function(n) {
let dp = new Array(n + 1).fill(0)
dp[2] = 1
for(let i = 3; i <= n; i++) {
for(let j = 1; j <= i / 2; j++) {
dp[i] = Math.max(dp[i], dp[i - j] * j, (i - j) * j)
}
}
return dp[n]
};
以上内容来自代码随想录
做一个记录,方便以后查看