在算法学习过程中,我们经常会遇到一些经典问题,其中之一就是爬楼梯问题。这个问题看似简单,但实际上涉及到多种算法思想和优化技巧。本文将介绍几种不同的解法,包括暴力搜索、记忆化搜索和动态规划。
问题描述
给定一个共有 n
阶的楼梯,你每步可以上1阶或者2阶,请问有多少种方案可以爬到楼顶?
暴力搜索
回溯算法通常并不显式地对问题进行拆解,而是将求解问题看作一系列决策步骤,通过试探和剪枝,搜索所有可能的解。
我们可以尝试从问题分解的角度分析这道题。设爬到第i阶共有dp[i]种方案,那么dp[i]就是原问题,其子问题包括:
dp[i-1],dp[i-2],…dp[1]
由于每轮只能上1阶或2阶,因此当我们站在第i阶楼梯上时,上一轮只可能站在第i-1阶或第i-2阶上。换句话说,
我们只能从第i-1阶或第i-2阶迈向第i阶。
由此便可得出一个重要推论:爬到第i阶的方案数加上爬到第i-1阶的方案数就等于爬到第i-2阶的方案数。公式如下:
dp[i]=dp[i-1]+dp[i-2]
我们可以根据递推公式得到暴力搜索解法。以dp[n]为起始点,递归地将一个较大问题拆解为两个较小问题的和,直至到达最小子问题
dp[1]和dp[2]时返回。其中,最小子问题的解是已知的,即dp[1]=1,dp[2]=2;表示爬到1、2阶分别有1、2种方案。
首先,我们尝试使用回溯算法来解决这个问题。我们可以通过递归地将问题分解为更小的子问题来求解。
int dfs1(int i) {
if (i == 1 || i == 2) {
return i;
}
int count = dfs1(i - 1) + dfs1(i - 2);
return count;
}
暴力搜索形成的递归树。对于问题dp[n]其递归树的深度为n,时间复杂度为O(2^n)。
指数阶属于爆炸式增长,如果我们输入一个比较大的m,则会陷入漫长的等待之中。
指数阶的时间复杂度是“重叠子问题”导致的。
例如dp[9]被分解为dp[8]和dp[7],dp[8]被分解为dp[7]和dp[6],
子问题中包含更小的重叠子问题,子子孙孙无穷尽也。绝大部分计算资源都浪费在这些重叠的子问题上。
暴力搜索递归树:
记忆化搜索
为了提升算法效率,我们希望所有的重叠子问题都只被计算一次。为此,我们声明一个数组 mem 来记录每个子问题的解,并在搜索过程中将重叠子问题剪枝。
当首次计算dp[i]时,我们将其记录至 mem[i] ,以便之后使用。
当再次需要计算dp[i时,我们便可直接从 mem[i] 中获取结果,从而避免重复计算该子问题。
int dfs2(int i, vector<int> &mem) {
if (i == 1 || i == 2) {
return i;
}
if (mem[i] != -1) {
return mem[i];
}
int count = dfs2(i - 1, mem) + dfs2(i - 2, mem);
mem[i] = count;
return count;
}
这种方法的时间复杂度优化至 O(n)
。经过记忆化处理后,所有重叠子问题都只需计算一次,时间复杂度优化至,这是一个巨大的飞跃。
动态规划
记忆化搜索是一种“从顶至底”的方法:我们从原问题(根节点)开始,递归地将较大子问题分解为较小子问题,直至解已知的最小子问题(叶节点)。
之后,通过回溯逐层收集子问题的解,构建出原问题的解。
与之相反,动态规划是一种“从底至顶”的方法:从最小子问题的解开始,迭代地构建更大子问题的解,直至得到原问题的解。
由于动态规划不包含回溯过程,因此只需使用循环迭代实现,无须使用递归。
在以下代码中,我们初始化一个数组 dp 来存储子问题的解,它起到了与记忆化搜索中数组 mem 相同的记录作用:
int climbDP(int n) {
if (n == 1 || n == 2) {
return n;
}
vector<int> dp(n + 1);
dp[1] = 1;
dp[2] = 2;
for(int i = 3; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
空间优化
由于dp[i]只与dp[i-1和dp[i-2]有关,因此我们无须使用一个数组 dp 来存储所有子问题的解,而只需两个变量滚动前进即可
int climbDP2(int n) {
if (n == 1 || n == 2) {
return n;
}
int a = 1, b = 2;
for (int i = 3; i <= n; i++) {
int temp = b;
b = a + b;
a = temp;
}
return b;
}
结语
通过上述几种方法的介绍,我们可以看到算法设计中的递归思想、记忆化优化以及动态规划的策略。这些方法不仅适用于爬楼梯问题,同样可以应用于其他类似问题的解决。
C++学习资源
匠心精作C++从0到1入门编程-学习编程不再难
链接: https://pan.baidu.com/s/1q7NG28V8IKMDGD7CMTn2Lg?pwd=ZYNB 提取码: ZYNB
第二套、侯捷老师全系列八部曲 - 手把手教你进阶系列
链接: https://pan.baidu.com/s/1AYzdguXzbaVZFw1tY6rYJQ?pwd=ZYNB 提取码: ZYNB