动态规划的概念有很多,我就不介绍了,我们直接在解题过程中体会什么是动态规划。
如题,有一座高度是10级台阶的楼梯,从下往上走,每跨一步只能向上1级或者2级台阶。
比如,每次走1级台阶,一共走10步,这是其中一种走法。我们可以简写成 1,1,1,1,1,1,1,1,1,1。
再比如,每次走2级台阶,一共走5步,这是另一种走法。我们可以简写成 2,2,2,2,2。
要求用程序来求出一共有多少种走法。
当然,可以用排列组合的方式求解,但属于暴力枚举,时间复杂度时指数级的,这显然不是我们想要的。
假设只差一步就走到第10级,因为每次只能走 1 级或 2 级台阶,所以这时有两种情况:
- 从 9 级走到 10 级
- 从 8 级走到 10 级
那么就有了一个新的问题,如果知道从 0 级走到 8 级有X种走法,从 0 级走到 9 级有Y种走法,那么从 0 级到 10 级就有 X+Y 种走法。即所以走法根据最后一步而分为列举的两种情况,画出来就如下图:
现在得出一个结论:从 0 级到 10 级的走法 = 从 0 级到 9 级的走法 + 从 0 级到 8 级的走法;把10级台阶的走法写成f(10).
所以if(10) = f(9) + f(8);
按照刚才的思路不难想到 f(9) = f(8) + f(7) , f(8) = f(7) + f(6) …..
当只有一级或两级台阶时,显然只有一种或两种走法。所以f(1) =1,f(2) = 2;
可以归纳成以下公式:
f(1) =1 ;
f(2) = 2;
f(n) = f(n-1) + f(n-2);
问题分析到这里,就可以动手写代码了;
方法一,递归
从归纳的公式可以看出,这显然可以用递归调用来解决,所以
private static int getNumStep(int i) {
if (i < 1) {
return 0;
}
if (i == 1) {
return 1;
}
if (i == 2) {
return 2;
}
return getNumStep(i - 1) + getNumStep(i - 2);
}
看上去挺简单,但是计算一下他的时间复杂度:要算f(n),就要算f(n-1)和f(n-2),要算f(n-1)就要算f(n-2)和f(n-3),以此类推,时间复杂度就为一棵二叉树,高度为n-1,所以为O(2^n).
但是从图中可以看出,有很多都是被重复计算过的,越往下走,重复的越多。
那么,为了避免这种重复,可以用一个Hashmap来记录下算过的值。所以有了第二种方法。
方法二 备忘录算法
private static int getNumStep(int i,HashMap<Integer,Integer> map) {
if (i < 1) {
return 0;
}
if (i == 1) {
return 1;
}
if (i == 2) {
return 2;
}
if (map.containsKey(i)) {
return map.get(i);
}else {
int value = getNumStep(i - 1) + getNumStep(i - 2);
map.put(i, value);
return value;
}
}
集合map就是一个备忘录,当计算f(n)时,先查看map中是否含有这个值,有就直接返回该值,否,就计算出结果,保存在map中,然后返回。
该方法的时间和空间复杂度都是O(n)。但是我们还能不能更简化呢?
方法三 动态规划
前面的方法都是自顶向下递归计算,先算f(10),再算f(9)。那么换一种思路,可不可以自底向上迭代计算呢?
f(1)=1,f(2)=2,这是已经知道的,第一次迭代计算f(3)时,结果为 f(2)+ f(1),所以f(3)只依赖f(1)和f(2);同理第二次迭代计算f(4)时,f(4)只依赖f(3)和f(2)。
所以每次迭代只需要知道前两个状态就行了,所以代码为:
private static int getNumStep(int i) {
if (i < 1) {
return 0;
}
if (i == 1) {
return 1;
}
if (i == 2) {
return 2;
}
int a = 1;
int b = 2;
int value = 0;
for (int j = 3; j <= i; j++) {
value = a + b;
a = b;
b = value;
}
return value;
}
程序从 j=3 开始迭代,一直到 j=n 结束。每一次迭代,都会计算出多一级台阶的走法数量。迭代过程中只需保留两个临时变量a和b,分别代表了上一次和上上次迭代的结果。 为了便于理解,我引入了temp变量。temp代表了当前迭代的结果值。
该方法的时间复杂度和空间复杂度分别为o(n)和o(1)。
总结
回顾解题过程,我们把从0级到10级的问题,分为从8级到10级和从9级到10级两种情况,再可以把从0级到9级分为从7级到9级和从8级到9级两种情况……以此类推,然后当台阶只有1级或两级的情况,走法显然只有1种或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);就是问题的状态转移公式。
当然,走台阶是动态规划中最简单的问题,因为它只有一个维度,要想熟练运用动态规划,还是得更多复杂的题。