leetcode
比如,每次走1级台阶,一共走10步,这是其中一种走法。我们可以简写成 1,1,1,1,1,1,1,1,1,1。
再比如,每次走2级台阶,一共走5步,这是另一种走法。我们可以简写成 2,2,2,2,2。
class Solution {
public:
int climbStairs(int n) {
}
};
小结:
小结:
F(10) = F(9) + F(8)
,因此F(9)
和F(8)
是F(10)
的最优子结构F(1)
或者F(2)
是问题的【边界】。如果一个问题没有边界,将永远无法得到有限的结果F(n) = F(n - 1) + F(n - 2)
是阶段与阶段之间的状态转移方程。这是动态规划的核心,决定了问题的每一个阶段与下一个阶段的关系 public static int climbStairs(int n) {
if (n < 1){
return 0;
}
if (n == 1){
return 1;
}
if (n == 2){
return 2;
}
return climbStairs(n - 1) + climbStairs(n - 2);
}
时间复杂度分析
其实递归的时间复杂度计算并不难,我们先分析出递归方法所走过的路径:
时间效率是指数级别的,太低了。
回顾下刚刚的递归图,我们可以发现有些相同的参数被重复计算了(如下图所示,相同的颜色代表了方法被传入相同的参数)
如果想要避免重复计算,我们可以用创建一个哈希表,每次把不同的参数的计算结果存入哈希。当遇到相同参数时,再从哈希表中取出,就不用重复计算了。这个算法有个专有名词,备忘录算法
class Solution {
// 递归时由大量的重复运算,我们可以用一个map把原来算出来的值存起来以便以后使用
static Map<Integer, Integer> maps = new HashMap<Integer, Integer>();
public int climbStairs(int n) {
if (n < 1){
return 0;
}
if (n == 1){
return 1;
}
if (n == 2){
return 2;
}
if (maps.containsKey(n)){
return maps.get(n);
}else{
int value = climbStairs(n - 1) + climbStairs(n - 2);
maps.put(n, value);
return value;
}
}
}
在以上代码中,集合map是一个备忘录。当每次需要计算F(N)的时候,会首先从map中寻找匹配元素。如果map中存在,就直接返回结果,如果map中不存在,就计算出结果,存入备忘录中。
时间复杂度和空间复杂度分析
思考:
动态规划五部曲:
1)确定dp数组以及下标的含义
dp[i]
:爬到第i层楼梯,有dp[i]
种方法2)确定递推公式
dp[i] = dp[i - 1] + dp[i - 2]
3)dp数组如何初始化
dp[0] = 1
4)确定遍历顺序
5)举例推导dp数组
以上五部分析完之后,C++代码如下:
// 版本一
class Solution {
public:
int climbStairs(int n) {
if (n <= 1) return n; // 因为下面直接对dp[2]操作了,防止空指针
vector<int> dp(n + 1);
dp[1] = 1;
dp[2] = 2;
for (int i = 3; i <= n; i++) { // 注意i是从3开始的
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
};
当然依然也可以,优化一下空间复杂度,代码如下:
// 版本二
class Solution {
public:
int climbStairs(int n) {
if (n <= 1) return n;
int dp[3];
dp[1] = 1;
dp[2] = 2;
for (int i = 3; i <= n; i++) {
int sum = dp[1] + dp[2];
dp[1] = dp[2];
dp[2] = sum;
}
return dp[2];
}
};
时间复杂度: O ( n ) O(n) O(n)
空间复杂度: O ( 1 ) O(1) O(1)
动态规划本质上是一个填表过程
这道爬楼梯的题目仅仅是动态规划中最简单的问题,因为它只有一个变化维度。还有很多问题远比这要复杂得多
定义一个签名,如下,表示为:从位置curr走到位置n一共有几种方法
int process(int curr, int n)
假设我们站在楼梯curr上
class Solution {
int process(int curr, int n){
if(curr == n){
return 1;
}
if(curr == n - 1){
return process(curr + 1, n);
}
return process(curr + 1, n) + process(curr + 2, n);
}
public:
int climbStairs(int n) {
if(n < 0){
return -1;
}
return process(0, n);
}
};
(1)第一步:准备一张表。
int process(int curr, int n)
0~n
,所以数组长度为n+1
。也就是: std::vector<int> dp(n + 1)
(2)确定返回值
return process(0, n);
(3)填表
(3.1)先初始化表,也就是看base case
if(curr == n){
return 1;
}
(3.2) 再分析普通情况,这个时候要分析清楚依赖关系
if(curr == n - 1){
return process(curr + 1, n);
}
return process(curr + 1, n) + process(curr + 2, n);
for (int curr = n - 1; curr >= 0; --curr) {
}
(4)综上
class Solution {
int process(int curr, int n){
if(curr == n){
return 1;
}
if(curr == n - 1){
return process(curr + 1, n);
}
return process(curr + 1, n) + process(curr + 2, n);
}
public:
int climbStairs(int n) {
if(n < 0){
return -1;
}
std::vector<int> dp(n + 1);
dp[n] = 1;
for (int curr = n - 1; curr >= 0; --curr) {
if(curr == n - 1){
dp[curr] = dp[curr + 1];
}else{
dp[curr] = dp[curr + 1] + dp[curr + 2];
}
}
return dp[0];
}
};
题目 | 思路 |
---|---|
leetcode:70. 爬楼梯 Climbing Stairs | 动态规划 |
leetcode:746. 使用最小花费爬楼梯 Min Cost Climbing Stairs | 动态规划 |
leetcode:509. 斐波那契数列 Fibonacci Number | 动态规划 |
leetcode:91. 解码方法Decode Ways | |
leetcode:639. * 可以匹配多个字符时,有多少解码方法 II Decode Ways II |
|
leetcode:842. 将数组拆分成斐波那契序列,返回一个成功的组合 Split Array into Fibonacci Sequence | 回溯 |
leetcode:306. 字符串是否能切割成斐波那契序列 Additive Number | |
leetcode:873. 最长的斐波那契序列长度 Length of Longest Fibonacci Subsequence | |
Coin Change |