理论基础
动态规划问题的一般形式就是求最值 。比如说让你求最长递增子序列呀,最小编辑距离。求解动态规划的核心问题是穷举。因为要求最值,肯定要把所有可行的答案穷举出来,然后在其中找最值。
穷举所有可行解需要你熟练掌握递归思维,只有列出正确的「状态转移方程」 ,才能正确地穷举。判断算法问题是否具备「最优子结构」 ,是否能够通过子问题的最值得到原问题的最值 。动态规划问题存在「重叠子问题」,如果暴力穷举的话效率会很低,所以需要你使用「备忘录」或者「DP table」来优化穷举过程,避免不必要的计算。
以上提到的重叠子问题、最优子结构、状态转移方程就是动态规划三要素。
明确「状态」-> 明确「选择」 -> 定义 dp 数组/函数的含义。
我认为动态规划就是你能不能根据这道题去抽象出函数方程,这个方程是每一个状态一定是由上一个状态推导出来的,根据这个公式去写代码。
** 509. 斐波那契数 **
题目
斐波那契数 (通常用 F(n) 表示)形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:
F(0) = 0,F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1
给定 n ,请计算 F(n) 。
示例1:
输入:n = 2
输出:1
解释:F(2) = F(1) + F(0) = 1 + 0 = 1
思路
如果是直接递归呢,那么就是照着函数方程去写代码:
java
int fib(int n){
if(n==0||n=1){
return n;
}
return fib(n-1)+fib(n-2);
}
本质就是递归树,就是说想要计算原问题 f(20),我就得先计算出子问题 f(19) 和 f(18),然后要计算 f(19),我就要先算出子问题 f(18) 和 f(17),以此类推。最后遇到 f(1) 或者 f(2) 的时候,结果已知,就能直接返回结果,递归树不再向下生长了。
递归算法的时间复杂度是用子问题个数 * 解决一个问题需要的时间。即为递归树的总结点数2n * 加法的复杂度O(1)=O(2n);
但是存在大量重复计算:比如 f(18) 被计算了两次,而且以 f(18) 为根的这个递归树体量巨大,多算一遍,会耗费大量的时间。更何况还不止 f(18) 这一个节点被重复计算,所以这个算法效率很差。这就是重叠子问题
为了解决这个问题,加入了备忘录,就是每次算出某个子问题的答案后顺便记到「备忘录」里;每次遇到一个子问题别急着计算,先去「备忘录」里查一查,如果发现之前已经解决过这个问题了,直接把答案拿出来用,不要再耗时去计算了。可以看看代码的解释
代码
java
int fib(int n){
//创建一维数组,因为数组的索引从0开始,所以需要n+1个空间
int[] memo = new int [n+1];
//备忘录初始化为-1,是一个未计算标记,子问题的有效结果不会是-1。存储已解决的子问题结果,避免重复计算
Arrays.fill(memo,-1);
//memo是备忘录,n是子问题规模,比如斐波那契中的目标项数、凑零钱中的目标金额)。
return dp(memo,n);
}
//带备忘录进行递归
int dp(int[]memo,int n){
if(n==0||n==1){
return n;
}
//备忘录的作用
if(memo[n] !=-1){
return memo[n];// 已经计算过,不用再计算了
}
//返回结果前,存入备忘录
memo[n] = dp(memo,n-1)+dp(memo,n-2);
return memo[n];
}
70. 爬楼梯
题目
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
示例 1:
输入:n = 2
输出:2
解释:有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶
思路
-
爬到第一层楼梯有一种方法,爬到二层楼梯有两种方法。
-
那么第一层楼梯再跨两步就到第三层 ,第二层楼梯再跨一步就到第三层。
-
所以到第三层楼梯的状态可以由第二层楼梯 和 到第一层楼梯状态推导出来
这样能看出是重叠子问题
-
首先确定递归的数组dp[i]: 爬到第i层楼梯,有dp[i]种方法
-
然后推测上i-1层楼梯,有dp[i - 1]种方法,再跳一个台阶就是dp[i],比如到2层有2种方法,每种再走1步都能到3层 → 贡献 2种
-
上i-2层楼梯,有dp[i -2]种方法,因为到 i-2 层有 dp[i-2] 种方式,再走2步到i层。比如到1层有 1种 方法,再走2步到3层 → 贡献 1种
-
所以:dp[i] = dp[i-1] + dp[i-2]
dp[1] = 1,dp[2] = 2,然后从i = 3开始递推,那就会发现他的公式和斐波那契数列公式一样。
代码
java
class Solution{
int climbStairs(int n) {
int[] memo = new int[n+1];
Arrays.fill(memo, -1);
return dp(memo, n);
}
int dp(int[] memo, int n) {
if (n == 1) return 1; // 到1层:1种方法
if (n == 2) return 2; // 到2层:2种方法
if (memo[n] != -1) return memo[n];
memo[n] = dp(memo, n-1) + dp(memo, n-2); // 到n层 = 到n-1层 + 到n-2层
return memo[n];
}
}
746. 使用最小花费爬楼梯
题目
给你一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。
你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。
请你计算并返回达到楼梯顶部的最低花费。
示例 1:
输入:cost = [10,15,20]
输出:15
解释:你将从下标为 1 的台阶开始。
- 支付 15 ,向上爬两个台阶,到达楼梯顶部。总花费为 15 。
思路
-
dp[i]的定义:到达第i台阶所花费的最少体力为dp[i];
-
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[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
题目提到:"你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯" 也就是说 到达 第 0 个台阶是不花费的,但从 第0 个台阶 往上跳的话,需要花费 cost[0]。
所以初始化 dp[0] = 0,dp[1] = 0;
代码
java
class Solution {
public int minCostClimbingStairs(int[] cost) {
int len = cost.length;
int[] dp = new int[len + 1];
//从下标为0或下标为1的台阶开始,因此支付费用为0
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];
}
}