动态规划理论基础
动态规划的题目由重叠子问题构成,每一个状态一定是由上一个状态推导出来的。这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优的。
动态规划五步曲
- 确定
dp
数组(dp
table)以及下标的含义 - 确定递推公式
dp
数组如何初始化- 确定遍历顺序
- 举例推导
dp
数组
动态规划里面递推公式十分重要,但是确定dp
数组,初始化,遍历顺序也同样十分重要,一定要严格按照这五步进行,将每一步的思路理清。
做动态规划的题目遇到问题时,最好的方式就是打印出dp
数组,看是否和自己推理一致。
509. 斐波那契数
题目链接:509. 斐波那契数
思路:动态规划五步曲:
-
dp[i]的定义为:第i个数的斐波那契数值是dp[i]
-
递推公式:dp[i] = dp[i - 1] + dp[i - 2]
-
初始化:dp[0] = 0, dp[1] = 1
-
从递推公式可以看出,一定是从前向后遍历的。
-
举例看是否可行,当N为10的时候,dp数组应该是如下的数列:
0 1 1 2 3 5 8 13 21 34 55
如果代码写出来,发现结果不对,就把dp数组打印出来看看与推导的数列是否一致。
class Solution {
public int fib(int n) {
if (n == 0) return 0;
if (n == 1) return 1;
int[] dp = new int[n + 1];
dp[0] = 0;
dp[1] = 1;
for (int i = 2; i <= n; i++){
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
}
可以发现,只需要维护两个数值,不需要记录整个序列。代码如下:
class Solution {
public int fib(int n) { // 动态规划
if (n <= 1) return n;
int dp0 = 0;
int dp1 = 1;
for (int i = 2; i <= n; i++) {
int sum = dp1 + dp0;
dp0 = dp1;
dp1 = sum;
}
return dp1;
}
}
70. 爬楼梯
题目链接:70. 爬楼梯
思路:动态规划五步曲
-
dp[i]: 爬到第i层楼梯,有dp[i]种方法
-
递推公式:dp[i] = dp[i - 1] + dp[i - 2]
从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[i]的时候,一定要时刻想着dp[i]的定义,否则容易跑偏。
-
初始化:dp[1] = 1, dp[2] = 2
题目提示:1 <= n <= 45,所以本题不用考虑dp[0]的初始化!
-
从递推公式可以看出是从前向后遍历。
-
举例看是否可行
class Solution {
public int climbStairs(int n) {
if (n == 1) return 1;
// 1、确定dp数组及下标含义
// dp[i]代表爬到第i层楼梯,有dp[i]种方法
int[] dp = new int[n + 1];
// 2、确定递推函数
// dp[i] = dp[i - 1] + dp[i - 2]
// 3、确定初始化
dp[1] = 1;
dp[2] = 2;
// 4、确定遍历顺序
for (int i = 3; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
}
本题与斐波那契数相同,可以将空间复杂度从O(n)降为O(1)。
746. 使用最小花费爬楼梯
题目链接:746. 使用最小花费爬楼梯
思路:动态规划五步曲
-
dp[i]的定义:到达第i台阶所花费的最少体力为dp[i]。
题目中说 “你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯” 也就是相当于 跳到 下标 0 或者 下标 1 是不花费体力的, 从 下标 0 下标1 开始跳就要花费体力了。
-
递推公式:dp[i] = Math.min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2])
可以有两个途径得到dp[i],一个是dp[i - 1],一个是dp[i - 2]。
dp[i - 1] 跳到 dp[i] 需要花费 dp[i - 1] + cost[i - 1]。
dp[i - 2] 跳到 dp[i] 需要花费 dp[i - 2] + cost[i - 2]。
那么究竟是从dp[i - 1]跳还是从dp[i - 2]跳呢?一定是选最小的!
-
初始化:dp[0] = 0, dp[1] = 0。我们认为第一步无需支付费用,所以到第一个台阶和到第二个台阶都是0。
-
遍历顺序为从前到后
-
举例推导dp数组
拿cost = [1, 100, 1, 1, 1, 100, 1, 1, 100, 1] ,来模拟一下dp数组的状态变化
如果代码写出来有问题,就把dp数组打印出来,看看和如上推导的是否一致。
class Solution {
public int minCostClimbingStairs(int[] cost) {
int len = cost.length;
int[] dp = new int[len + 1];
// 每次最多走两步,前两个台阶无需支付费用
dp[0] = 0;
dp[1] = 0;
// 计算到达每一层台阶的最小费用
for (int i = 2; i <= len; i++) {
dp[i] = Math.min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
}
return dp[len];
}
}
还可以优化空间复杂度,因为dp[i]就是由前两位推出来的,那么也不用dp数组了
class Solution {
public int minCostClimbingStairs(int[] cost) {
int len = cost.length;
// 每次最多走两步,前两个台阶无需支付费用
int dp0 = 0;
int dp1 = 0;
// 计算到达每一层台阶的最小费用
for (int i = 2; i <= len; i++) {
int dp_i = Math.min(dp1 + cost[i - 1], dp0 + cost[i - 2]);
dp0 = dp1;
dp1 = dp_i;
}
return dp1;
}
}