问题:10阶层楼梯,每次只能走一阶或两阶,问有多少种走法?
拿到这个题目的第一想法是暴力穷举法,多个For循环,每一步都有两种走法,这种方法的时间复杂度为O(2^n),显然不合理。
然后,思考看是否有更合理的解法。
这个时候我们思考的方向应该是:动态规划(Dynamic Programming)。何为动态规划,即把多阶段过程转化为一系列单阶段问题,利用各阶段之间的关系,逐个求解。
就拿这个题目来说,假设只差最后一步就可以走到第10阶台阶,这时候会出现几种情况?显然是两种,因为每一步只允许走1级或2级台阶。第一种是从9级到10级,第二种是从8级直接到10级。如果我们假设从0级到8级的走法有X种,从0级到9级的走法有Y种,那么0到10级的走法有多少种呢?
相信大家已经知晓答案了,0到10级别的走法为X+Y;也许有小伙伴会质疑X和Y中会有重复的走法,但是我们从整体走法来看,即使前面有重复的路线,却也因为最后一步的不同,而导致整体的是不一样的走法。为了便于表达,我们将10阶的走法简写为F(10),那么
F(10) = F(9)+F(8)
那么F(9)和F(8)如何计算呢?可得出如下的推导
F(1) = 1;
F(2) = 2;
F(n) = F(n-1)+F(n-2);
动态规划当中包含三个重要的概念:最优子结构、边界、状态转移公式
刚刚我们分析出F(10)=F(9)+F(8),因此F(9)和F(8)是F(10)的最优子结构。
当有一阶或两阶时,我们可以直接得出结果F(1) = 1,F(2)=2即为问题的边界。
F(n)=F(n-1)+F(n-2)是阶段与阶段间的转移方程。
我们可以试着用递归来实现:
private static int climbStairWays(int n) {
if(n < 1) {
return 0;
}
if(1 == n)
return 1;
if(2 == n)
return 2;
return climbStairWays(n-1) + climbStairWays(n-2);
}
这样的实现方式是简洁了,我们来看看该方案的时间复杂度,分析递归的方法不难发现,这是一颗二叉树,树的节点个数就是我们递归所需要计算的次数。二叉树的高度为N-1,节点的个数接近2的N-1次方。所以时间复杂度可以近似看做O(2^N).
那是否有办法进行优化一下呢?
可以自行画出二叉树分析一下,会发现,越往下走,重复的计算越多。那这些重复计算的结果我们是否可以先缓存起来,下次遇到的时候直接拿来用呢?
这里我们可以使用备忘录进行记录已经计算过的值和结果,如下
private static int climbStairWays2(int n,HashMap<Integer,Integer> map) {
if(n < 1) {
return 0;
}
if(n == 1){
return 1;
}
if(n == 2) {
return 2;
}
if(map.containsKey(n)) {
return map.get(n);
}else {
int value = climbStairWays2(n-1,map)+climbStairWays2(n-2,map);
map.put(n,value);
return value;
}
}
在以上的代码中,集合map是一个备忘录。每当需要计算F(N)的时候,会首先从map中寻找匹配元素,如果map中存在,就直接返回结果;不存在,就进行计算,将结果存入备忘录中。
我们再来看下时间复杂度和空间复杂度。从F(1)到F(N)一共有N个不同的输入,在Hash表里存了N-2个结果,所以时间复杂度和空间复杂度都是O(N).
这个时候感觉时间复杂度和空间复杂度已经都是O(N)级别了,时间复杂度是不能再小了,那么空间复杂度呢,能否再进一步减小?
我们之前用的都是递归,是一种自顶向下的求解过程,现在不妨进行思维逆转一下,自底往上求解。
通过上面的观察发现,F(1),F(2)是确定的,由这两个数可以直接得出F(3)=F(1)+F(2),由F(2)和F(3)又可以得出F(4)...
即后面的每个数字都是由前两个相邻的数字相加得来,那么每次计算完成之后,我们仅保留前面的两个状态即可完成所有的推导,代码如下
private static int climbStairWays3(int n) {
int count = 0;
int first = 1;
int second = 2;
if(n < 1) {
return 0;
}
if(n == 1) {
return 1;
}
if(n == 2) {
return 2;
}
for(int i = 3;i <= n;i++) {
count = first + second;
first = second;
second = count;
}
return count;
}
程序从i=3开始迭代,一直到i=n结束。每一次迭代,都会计算出多一级台阶的走法数量。迭代过程中只需保留两个临时变量first和second,分别代表上一次和上上次迭代的结果。这个时候的时间复杂度依然是O(N),但是空间复杂度却变为了O(1).