首先看动态规划的三要素:重叠子问题、最优子结构和状态转移方程。
重叠子问题:存在大量的重复计算
最优子结构:
状态转移方程:当前状态转移成以前的状态
动态规划的解题步骤主要有:
- 确定 dp 数组以及下标的含义
- 状态转移方程、递推公式
- dp数组初始化、遍历顺序
- 写代码验证
直接看实际的算法题
1.LeetCode70. 爬楼梯
假设你正在爬楼梯。需要
n
阶你才能到达楼顶。每次你可以爬
1
或2
个台阶。你有多少种不同的方法可以爬到楼顶呢?示例 1:
输入:n = 2 输出:2 解释:有两种方法可以爬到楼顶。 1. 1 阶 + 1 阶 2. 2 阶
实际上就是斐波那契算法,我们按最后一次爬楼梯的情形:只有爬1个或者2个台阶,如下图:
所以状态转移方程就是 f(n) = f(n-1) + f(n-2)
。
public int climbStairs(int n) {
if(n == 1) {
return 1;
}
int[] dp = new int[n + 1];
dp[0] = 1;
dp[1] = 1;
dp[2] = 2;
for(int i = 3; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
变体:如果可以爬1、2、3、4…m 级台阶,如何求最后的次数?根据上面的图可以得到状态方程:
f(n) = f(n-1) + f(n-2) +...+f(n - m)
所以解题代码为:
public int climbStairs2(int n) {
int[] dp = new int[n+1];
dp[0] = 1;
for(int i = 1; i <= n; i++) {
for(int j = 1; i <= m; j++) {
if(i - j >= 0) {
dp[i] = dp[i - j];
}
}
}
return dp[n];
}
2.LeetCode746. 使用最小花费爬楼梯
给你一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。
只能选择从下标为 0 或下标为 1 的台阶开始爬楼梯,
请你计算并返回达到楼梯顶部的最低花费。
示例 1:
输入:cost = [10,15,8] 输出:15 解释:你将从下标为 1 的台阶开始。 - 支付 15 ,向上爬两个台阶,到达楼梯顶部。 总花费为 15 。
这题的思路和上一题爬楼梯很像,我们还是自顶向下来考虑,
如果最后一次选择记为f(n)
那么需要考虑f(n-1)
和f(n-2)
谁更小,然后再加上当前的花费值。那么状态转移方程为:f(n) = min(f(n-1), f(n-2)) + cost[n]
。因此代码就容易了:
public int minCostClimbingStairs(int[] cost) {
int[] dp = new int[cost.length];
dp[0] = cost[0];
dp[1] = cost[1];
for(int i = 2; i < cost.length; i++) {
dp[i] = Math.min(dp[i - 1], dp[i - 2]) + cost[i];
}
//因为可以一次性爬两层,所以最后再比较倒数一次和倒数第二次的爬楼梯花费
return Math.min(dp[cost.length - 1], dp[cost.length - 2]);
}
3.LeetCode62. 不同路径
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
示例 1:
输入:m = 3, n = 7 输出:28
看题目可以知道,和前两题不一样,需要我们从两个维度去考虑。因此可以选择二维dp数组来解决问题,按照动态规划解题步骤:
- 确定dp数组的含义
dp[m][n]
表示从[0][0]
到[m][n]
的路径条数,数组内的下标表示所处的位置
- 确定状态转移方程
- 我们知道当前位置状态来源于左侧和上侧位置状态因此可以写成:
dp[m][n] = dp[m-1][n] +dp[m][n-1]
- 确定dp数组初始状态和遍历顺序
- 初始状态要注意,和一维dp数组考虑一个初始值不同。我们要考虑二维多个初始值:
- 在
dp[0][i]
和dp[j][0]
这两列上,路径条数都应该为1
- 在
- 遍历顺序,按照从小到大的原则进行
- 初始状态要注意,和一维dp数组考虑一个初始值不同。我们要考虑二维多个初始值:
综合上面的思路,可以写出代码
public int uniquePaths(int m, int n) {
int[][] dp = new int[m][n];
//初始化dp数组
for(int i = 0; i < m; i++) dp[i][0] = 1;
for(int j = 0; j < n; j++) dp[0][j] = 1;
//遍历顺序
for(int i = 1; i < m; i++) {
for(int j = 1; j < n; j++) {
//状态转移方程
dp[i][j] = dp[i][j - 1] + dp[i - 1][j];
}
}
return dp[m - 1][n - 1];
}
4.LeetCode63. 不同路径 II
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish”)。
现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
网格中的障碍物和空位置分别用 1 和 0 来表示。
示例 1:
输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]] 输出:2 解释:3x3 网格的正中间有一个障碍物。 从左上角到右下角一共有 2 条不同的路径: 1. 向右 -> 向右 -> 向下 -> 向下 2. 向下 -> 向下 -> 向右 -> 向右
这题和上一题的区别就是有了障碍,那么我们该如何考虑这个障碍呢?主要分成两个方面:
-
初始化时遇到障碍:
-
顺序遍历时遇到障碍:
也就是题目给出的情况,这种情况将障碍处设置为0 即可
其余的和上一题类似,所以直接给出代码:
public int uniquePathsWithObstacles(int[][] obstacleGrid) {
// 数组的行数
int m = obstacleGrid.length;
// 数组的列数
int n = obstacleGrid[0].length;
int[][] dp = new int[m][n];
//障碍及后面方格统一初始化
for(int i = 0; i < m; i++) {
if(obstacleGrid[i][0] == 1) {
break;
}
dp[i][0] = 1;
}
for(int j = 0; j < n; j++) {
if(obstacleGrid[0][j] == 1) {
break;
}
dp[0][j] = 1;
}
for(int i = 1; i < m; i++) {
for(int j = 1; j < n; j++) {
//遍历过程遇见障碍,直接跳过
if(obstacleGrid[i][j] == 1) {
dp[i][j] = 0;
continue;
}
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
return dp[m - 1][n - 1];
}
5.leetCode343. 整数拆分
给定一个正整数
n
,将其拆分为k
个 正整数 的和(k >= 2
),并使这些整数的乘积最大化。返回 你可以获得的最大乘积 。
示例 1:
输入: n = 2 输出: 1 解释: 2 = 1 + 1, 1 × 1 = 1。
我们按照动态规划解题步骤一步步来:
-
1.确定dp数组的含义以及下标:
- dp数组表示的是最大乘积,下标表示整数n
-
2.确定状态转移方程:
-
考虑到一个整数至少要分成2个及以上的整数,所以:
- 分成2个可以表达成
i * j
- 分成2个以上可以表达成
j * dp[i - j]
- 分成2个可以表达成
-
所以转移方程应该为:
dp[i] = max(j * dp[i - j], j * i)
-
但是这里我们会发现得不到符合题意的值:
因为每次取的都是最后一个,比如
dp[10] = max(9 * dp[1], 9 * 1)
,所以每次需要与前面的 dp[i] 比较,得到最后的最大值。 -
因此转移方程应为:
dp[i] = max(dp[i],max(j * dp[i - j], j * (i - j)))
-
-
-
3.确定初始值,遍历顺序
dp[2] = 1,dp[1] = 1,dp[0] = 1
(dp[0] 和 dp[1] 实际上不符合题意,虽然赋值1 不影响结果)- i从3到n 遍历,j 从 1 到 i - 2 遍历
-
4.根据上面三个步骤写代码:
public int integerBreak(int n) {
int[] dp = new int[n + 1];
//初始化赋值
dp[2] = 1;
for(int i = 3; i <= n; i++) {
//从下标为2开始遍历
for(int j = 1; j < i - 1; j++) {
dp[i] = Math.max(dp[i], Math.max(j * (i - j), j * dp[i - j]));
}
}
return dp[n];
}
6.LeetCode96. 不同的二叉搜索树
给你一个整数
n
,求恰由n
个节点组成且节点值从1
到n
互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。示例 1:
输入:n = 3 输出:5
这题确实不好解决,二叉搜索树的种树这个变量不好确定。需要以左右子树为基础进行考虑,下面引用代码随想录的思路:
dp[3],就是 元素1为头结点搜索树的数量 + 元素2为头结点搜索树的数量 + 元素3为头结点搜索树的数量
元素1为头结点搜索树的数量 = 右子树有2个元素的搜索树数量 * 左子树有0个元素的搜索树数量
元素2为头结点搜索树的数量 = 右子树有1个元素的搜索树数量 * 左子树有1个元素的搜索树数量
元素3为头结点搜索树的数量 = 右子树有0个元素的搜索树数量 * 左子树有2个元素的搜索树数量
有2个元素的搜索树数量就是dp[2]。
有1个元素的搜索树数量就是dp[1]。
有0个元素的搜索树数量就是dp[0]。
所以dp[3] = dp[2] * dp[0] + dp[1] * dp[1] + dp[0] * dp[2]
所以状态转移方程为 dp[i] += dp[j] * dp[i - j - 1];
初始值dp[0] = 1;dp[1] = 1
,按照节点个数进行遍历。直接下出如下代码
public int numTrees(int n) {
int[] dp = new int[n + 1];
//初始化赋值
dp[0] = 1;
dp[1] = 1;
for(int i = 2; i <= n; i++) {
for(int j = 0; j < i; j++) {
dp[i] += dp[j] * dp[i - j - 1];
}
}
return dp[n];
}