【C++】经典爬楼梯问题的不同解法-C++学习资料

在算法学习过程中,我们经常会遇到一些经典问题,其中之一就是爬楼梯问题。这个问题看似简单,但实际上涉及到多种算法思想和优化技巧。本文将介绍几种不同的解法,包括暴力搜索、记忆化搜索和动态规划。

问题描述

给定一个共有 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],
子问题中包含更小的重叠子问题,子子孙孙无穷尽也。绝大部分计算资源都浪费在这些重叠的子问题上。
暴力搜索递归树:

1
2
1
2
1
2
1
2
1
2
1
2
1
2
1
2
1
1
2
1
1
2
1
2
1
1
1
1
1
1
1
1
1
1
1
1
1
1
2
1
1
1
1
1
1
1
1
1
1
9
8
7
7
6
6
5
5
4
5
3
5
4
4
3
4
3
3
4
2
2
4
3
3
2
3
2
3
2
2
3
1
1
3
2
2
1
2
1
1
1
2
1
1
2
1
1
1
1
1

记忆化搜索

为了提升算法效率,我们希望所有的重叠子问题都只被计算一次。为此,我们声明一个数组 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


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值