动态规划
通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。动态规划常常适用于有重叠子问题和最优子结构性质的问题 。
1.有一座高度是10级台阶的楼梯,从下往上走,每跨一步只能向上1级或者2级台阶。要求用程序来求出一共有多少种走法
我们先来分析下这个问题,假设我们只差最后一步就走到10级台阶,由于每一步台阶只允许往上跨1级或2级,这时候会出现两种情况,第一种情况 是从第9级 走到 第10级,第二种情况是从第8级 走到 第10级。
那么我们再考虑一个问题,如果我们已知0到9级台阶的走法有X种,而0到8级的台阶走法有Y种,那么0-10级的走法有多少种?
10级的台阶的所有走法可以根据最后一步的不同分成两部分,第一部分的最后一步是从9级 到 10级,这部分的走法数量和9级台阶的走法数量是相等的,也是X
故 0 - 10级的走法数量为X+Y种
那么我们可以得出一个结论:从(0-10级台阶的走法数量) = (0 - 9级台阶的走法数量) + (0 - 8级台阶的走法数量)
为了方便表达 我们把10级台阶的走法数量简写成 F(10),此时 F(10)=F(9)+F(8)
以此类推 F(N) = F(N - 1) + F(N - 2) (N>=3)
动态规划中包含三个重要的概念:[最优子结构],[边界],[状态转移方程]
刚才我们分析出 F(10) = F(9) + F(8), 因此 F(9) 和 F(8) 是 F(10)的[最优子结构]
当只有 1级 台阶或 2级 台阶时,我们可以直接得出结果,无需继续简化。我们称 F(1) 和 F(2)是问题的[边界]。如果一个问题没有边界,将永远无法得出有限的结果
F(N) = F(N - 1) + F(N - 2) 是阶段与阶段之间的[状态转移方程]
所以我们大体的思路应该是这样的
public static int getClimbingWays(int n){
if(n == 0){
return 0;
}else if(n == 1){
return 1;
}else if(n == 2){
return 2;
}
return getClimbingWays(n-1)+getClimbingWays(n-2);
}
不难看出这是一颗二叉树,且这颗二叉树的高度 是 N-1,节点个数接近2的N-1次方。所以方法的时间复杂度可以近似的可以看作是 O(2^N)。
现在我们看一下上述的递归图,我们可以看出一个问题,有些相同参数被重复计算了,越往下走,重复的越多
如图,相同的颜色被代表传入相同的参数。
所以我们可以用备忘录算法来做个优化
大体思路:先创建一个哈希表,每次把不同参数的计算结果存入哈希。当遇到相同参数时,再从哈希表中取出来,这样我们就可以不用重复计算了
public static int getClimbingWays(int n,Map<Integer,Integer> map){
if(n == 0){
return 0;
}else if(n == 1){
return 1;
}else if(n == 2){
return 2;
}
if(map.containsKey(n)){
return map.get(n);
}else{
int v = getClimbingWays(n-1,map)+getClimbingWays(n-2,map);
map.put(n, v);
return v;
}
}
现在时间复杂度已经优化的差不多了O(N),我们现在看看能不能把空间复杂度O(N)近一步减小。
我们不妨把思路调转过来 F(10) = F(9) + F(8) , 而 F(1) 为 1, F(2) 为 2, F(3) = F(2) + F(1),F(4)=(F3)+F(2)
public static int getClimbingWays2(int n){
if(n == 0){
return 0;
}else if(n == 1){
return 1;
}else if(n == 2){
return 2;
}
int a = 1;
int b = 2;
int temp = 0;
for(int i=3; i <= n; i++){
temp = a + b;
a = b;
b = temp;
}
return temp;
}
现在的时间复杂度为O(N) , 空间复杂度为 O(1)