算法:动态规划思路(仅作记录)
以leetcode70题爬楼梯为例:
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
递归
一共两种爬楼梯的方式
如果最后一步要用到方法1,那么我们得先爬到 n−1,要解决的问题缩小成:从 0 爬到 n−1 有多少种不同的方法。
如果最后一步要用到方法2,那么我们得先爬到 n−2,要解决的问题缩小成:从 0 爬到 n−2 有多少种不同的方法。
从 0 爬到 n 的方法数等于这两种方法数之和,即
f(n) = f(n-1) + f(n-2)
int climbStairs(int n) {
//n=1时,不存在n-2,因此不带入f(n)
if(n==1){
return 1;
}
return f(n);
}
int f(int n){
if(n==0 || n==1){
return 1;
}
return f(n-1) + f(n-2);
}
递归+记忆化搜索
递归时有重复大量计算的情况: 如f(5)=f(4)+f(3),f(4)=f(3)+f(2),使用列表保存第一次计算f(n)的值,后续如果需要再计算f(n)时直接返回,不进入后续递归,记录下计算f(4)时f(3)结果,在计算f(5)时直接返回=f(4)+cache[3]。
时间复杂度:O(n),需要在每个节点第一次出现时计算,一共n次。
vector<int> cache;
int climbStairs(int n) {
if(n==1){
return 1;
}
cache.resize(n+1, -1);
return f(n);
}
int f(int n){
if(n==0 || n==1){
return 1;
}
if (cache[n] != -1){
return cache[n];
}
int res = f(n-1) + f(n-2);
cache[n] = res;
return res;
}
递推(自底向上)
在递归时我们为了使f(n)到达我们已知的递归边界f(0)与f(1),将f(n)自顶向下拆分为子问题,又将子问题的结果归上去(有种折返跑的感觉)。
所有我们可以使用递推,直接从边界开始累加到f(n),运行速度显著快于递归。
时间复杂度:O(n)
空间复杂度:O(n),创建了长度为n+1的列表
int climbStairs(int n) {
vector<int> f(n+1, 1);
for(int i=2;i<n+1;i++){
f[i] = f[i-1] + f[i-2];
}
return f[n];
}
递推+空间优化
实际上在递推的过程中我们只使用了,三个变量,f[i], f[i-1]与f[i-2],对于列表f,每次计算时,在f[i-2]之前所保存的值再也不会被使用,实际上它们已经包含在由它们累加而来的f[i-1]和f[i-2]中了,因此可以直接使用这三个变量替换列表。
int climbStairs(int n) {
// vector<int> f(n+1, 1);
int f_1 = 1, f_2 = 1, new_f = 0;
// f_1上一个,即f[i-1],f_2上上一个即f[i-2],它们第一次出现时,为f(1)与f(0)=1
for(int i=2;i<=n;i++){
// f[i] = f[i-1] + f[i-2];
new_f = f_1 + f_2;
f_2 = f_1;
f_1 = new_f;
}
return new_f;
}