爬楼梯问题详解
说到动态规划啊,很多人都觉得是一生之敌。繁琐,耗脑子,变化多,有许多细节,不想看,不想懂。对我们这些不是专门研究算法的人来说,确实不容易把握住动态规划的所有细节。那,常见的题型我们还是可以摊开来分析分析的嘛。正好今天做题做到了爬楼梯的题目,那我们就借此来说道说道。
我们先来看一下题目:
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶或者3个台阶。你有多少种不同的方法可以爬到楼顶呢?
示例 1:
输入:3 输出:4 解释:
- 1+1+1
- 1+2
- 2+1
- 3
示例 2:
输入:4 输出:7 解释:
- 1+1+1+1
- 1+1+2
- 1+2+1
- 2+1+1
- 2+2
- 1+3
- 3+1
老惯例,我们先尝试用暴力破解的方式去解题,无脑递归。
暴力解法
首先思路还是很明确的,我们每一步的时候,都有三个选择,跳一步,还是跳两步,还是跳三步。那我们可以把递归写成这样:
public int countWays(int n) {
if (n == 0)
return 1; // 到顶 if (n == 1)
return 1; // 唯一选择,走一步
if (n == 2)
return 2; // 可以走一步,也可以走两步 // 如果走了一步,那就剩下n-1步
int take1Step = countWays(n - 1);
// 类似地,走了两步就剩下n-2步
int take2Step = countWays(n - 2);
// 走3步就剩下n-3步
int take3Step = countWays(n - 3);
//汇总齐活儿
return take1Step + take2Step + take3Step; }
这时间复杂度到达了O(3^n)
,因为我们在函数里做了三次递归调用。空间复杂度就是很普通的O(n)
。我们用countWay(4)
来分析一下:
画个图我们就可以很轻易地发现,有重叠子问题,CountWays(2)
跟CountWays(1)
被调用了两次。说到这个那我可就会了啊,缓存结果啊!?
自上而下
之前在讲斐波那契数列得时候,我们就说过缓存结果。我们可以用一个数组之类的东西把已经计算过的结果缓存起来:
public int countWays(int n) {
int dp[] = new int[n + 1];
return countWaysRecursive(dp, n);
}
public int countWaysRecursive(int[] dp, int n) {
if (n == 0)
return 1;
if (n == 1)
return 1;
if (n == 2)
return 2;
if (dp[n] == 0) {
int take1Step = countWaysRecursive(dp, n - 1);
int take2Step = countWaysRecursive(dp, n - 2);
int take3Step = countWaysRecursive(dp, n - 3);
dp[n] = take1Step + take2Step + take3Step;
}
return dp[n];
}
现在的一个复杂度是怎么样的?我们的缓存数组缓存所有子问题的计算结果,我们可以肯定的是不会有超过n+1个子问题,
所以时间复杂度就在O(n)
,空间复杂度还是没变,还是O(n)
。
自下而上
我们还可以用自下而上的方式来尝试优化。所谓的自下而上很直观,还是看上面那个树形图,可以这么理解,先计算上面的再计算下面的小子问题称之为自上而下。而直接计算下面的子问题从而引导到计算大问题称之为自下而上。我们来尝试填充这个dp数组,以自下而上的方式。通过上面的代码我们可以看出,每个countWaysRecursive(n)
都是前面三个之和。我们可以利用这一点来填充数组。
public int countWays(int n) {
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] + dp[i-3];
return dp[n];
}
这边我们的代码精简了不少,时间复杂度跟空间复杂度都维持在了O(n)
。
还可以继续优化吗?
上面的这个代码跟斐波那契数列还是很像的,我们可以发现我们其实并不需要用一个数组来保存之前所有的值,我们只是需要前面三个结果来计算当前结果。那基于这个情况,我们可以进一步优化我们的代码:
public int countWays(int n) {
if (n < 2) return 1;
if (n == 2) return 2;
int n1=1, n2=1, n3=2, temp;
for(int i=3; i<=n; i++) {
temp = n1 + n2 + n3;
n1 = n2;
n2 = n3;
n3 = temp;
}
return n3;
}
经过这一步,我们的时间复杂度没变,但是空间复杂度已经降到了O(1)
。nice!
写在最后
哇,这一通分析下来真的很令人脱发。但看到我们就这么把一道动态规划题做出来了还是小有成就感的。动态规划的题目太多了,不过常见的题型也可以分为几类,太极端的题目我们可以不看了。它们也像之前讨论的二分啊,滑动窗口啊一样有对应的套路可循。最近我会继续分享一些动态规划相关的问题出来,感谢大家关注。Happy coding~
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- avro使用schema生成java文件
- zabbix_sender安装和使用
- zabbix_get安装和使用
- rabbitmq搭建集群踩坑记
- Elasticsearch升级踩坑记之升级elasticsearch2.x到elasticsearch5.x
- 变量判断与设置
- 变量的删除与替换
- linux 正则表达式详解
- grafana使用教程之安装worldPing插件
- 同事:把"重试"抽象出来做个工具类吧
- Redis慢查询日志
- LinkedList源码阅读笔记
- RTSP协议网络摄像头接入视频平台EasyNVR
- 在 Hiplot 中使用 Sigflow
- 0805-CDH5中的Parquet迁移至CDP中兼容性验证