到达第K级台阶的方案数

2024-08-20

题目:

给你有一个 非负 整数 k 。有一个无限长度的台阶,最低 一层编号为 0 。

Alice 有一个整数 jump ,一开始值为 0 。Alice 从台阶 1 开始,可以使用 任意 次操作,目标是到达第 k 级台阶。假设 Alice 位于台阶 i ,一次 操作 中,Alice 可以:

  • 向下走一级到 i - 1 ,但该操作 不能 连续使用,如果在台阶第 0 级也不能使用。
  • 向上走到台阶 i + 2jump 处,然后 jump 变为 jump + 1

请你返回 Alice 到达台阶 k 处的总方案数。

注意,Alice 可能到达台阶 k 处后,通过一些操作重新回到台阶 k 处,这视为不同的方案。

示例 1:

输入:k = 0

输出:2

解释:

2 种到达台阶 0 的方案为:

  • Alice 从台阶 1 开始。
    • 执行第一种操作,从台阶 1 向下走到台阶 0 。
  • Alice 从台阶 1 开始。
    • 执行第一种操作,从台阶 1 向下走到台阶 0 。
    • 执行第二种操作,向上走 20 级台阶到台阶 1 。
    • 执行第一种操作,从台阶 1 向下走到台阶 0 。

示例 2:

输入:k = 1

输出:4

解释:

4 种到达台阶 1 的方案为:

  • Alice 从台阶 1 开始,已经到达台阶 1 。
  • Alice 从台阶 1 开始。
    • 执行第一种操作,从台阶 1 向下走到台阶 0 。
    • 执行第二种操作,向上走 20 级台阶到台阶 1 。
  • Alice 从台阶 1 开始。
    • 执行第二种操作,向上走 20 级台阶到台阶 2 。
    • 执行第一种操作,向下走 1 级台阶到台阶 1 。
  • Alice 从台阶 1 开始。
    • 执行第一种操作,从台阶 1 向下走到台阶 0 。
    • 执行第二种操作,向上走 20 级台阶到台阶 1 。
    • 执行第一种操作,向下走 1 级台阶到台阶 0 。
    • 执行第二种操作,向上走 21 级台阶到台阶 2 。
    • 执行第一种操作,向下走 1 级台阶到台阶 1 。

提示:

  • 0 <= k <= 109

记忆化搜索

思路:

寻找子问题

在跳台阶的过程中:
	为了判断是否到达终点 k,我们需要知道当前在第几个台阶。
	为了计算操作二能往上跳多少个台阶,我们需要知道已经使用了多少次操作二。
	为了判断能否使用操作一(往下跳),我们需要知道上一次操作是否使用了操作一。
例如现在在第 5 个台阶,且之前使用了 3 次操作二,那么可以:
	使用操作二:向上跳 2^3 = 8 个台阶,到达第 5 + 8 = 13 个台阶。接下来要解决的问题是:在使用了 4 次操作二,且上一次操作没有使用操作一的情况下,从 13 跳到 k 的方案数。
	使用操作一(前提是上一次操作没有使用操作一):向下跳 1 个台阶,到达第 5 − 1 = 4 个台阶。接下来要解决的问题是:在使用了 3 次操作二,且上一次操作使用了操作一的情况下,从 4 跳到 k 的方案数。
这些问题都是和原问题相似的子问题,可以用递归解决。

状态定义

根据上面的讨论,我们需要在递归过程中跟踪以下信息:
	i:当前位于第 i 个台阶。
	j:已经使用了 j 次操作二。
	preDown:一个布尔值,表示上一次操作是否使用了操作一。
因此,定义状态为 dfs(i, j, preDown),表示在使用了 j 次操作二,且上一次操作使用/未使用操作一的情况下,从 i 跳到 k 的方案数。

状态转移方程

枚举当前使用哪个操作:
	使用操作二:接下来要解决的问题是 dfs(i + 2^j, j + 1, false),将其方案数加到返回值中。
	使用操作一(前提是 preDown = false 且 i > 0):接下来要解决的问题是 dfs(i − 1, j, true),将其方案数加到返回值中。
	此外,如果 i=k(到达终点),则找到了一个方案,把返回值加一。

注意:到达第 k 个台阶后,还可以继续操作,重新回到第 k 个台阶。所以 i = k 并不是递归边界。
递归边界:如果 i > k + 1,由于操作一不能连续使用,无法到达 k,返回 0。
递归入口:dfs(1, 0, false),即答案。一开始在第 1 个台阶,没有使用过操作二,也没有使用过操作一。

代码:

class Solution {
public:
    int waysToReachStair(int k) {
        unordered_map<long long, int> memo;
        auto dfs = [&](auto&& dfs, int i, int j, bool pre_down) -> int {
            if (i > k + 1) { // 无法到达终点 k
                return 0;
            }
            // 把状态 (i, j, pre_down) 压缩成一个 long long
            long long mask = (long long) i << 32 | j << 1 | pre_down;
            if (memo.contains(mask)) { // 之前算过了
                return memo[mask];
            }
            int res = i == k;
            res += dfs(dfs, i + (1 << j), j + 1, false); // 操作二
            if (i && !pre_down) {
                res += dfs(dfs, i - 1, j, true); // 操作一
            }
            return memo[mask] = res; // 记忆化
        };
        return dfs(dfs, 1, 0, false);
    }
};

组合数学

思路:

假设使用了 m 次操作一,j 次操作二,那么有 1 + 2^0 + 2^1 + 2^2 + ⋯ + 2^j−1 − m = k 
即 m = 2^j − k 注意上式当 j=0 时也是成立的。
由于操作一不能连续使用,我们需要在这 j 次操作二前后,以及相邻两次操作二的空隙中,插入 m 个操作一,所以方案数等于从 j+1 个物品中选出 m 个物品的方案数,即 C(j + 1, m)
枚举 j,最终答案为∑(j = 0, 29) C(j + 1, m)
其中 0 ≤ m ≤ j + 1。根据题目的数据范围,j 至多枚举到 29。

代码:

int c[31][31];

auto init = [] {
    for (int i = 0; i < 31; i++) {
        c[i][0] = c[i][i] = 1;
        for (int j = 1; j < i; j++) {
            c[i][j] = c[i - 1][j - 1] + c[i - 1][j];
        }
    }
    return 0;
}();

class Solution {
public:
    int waysToReachStair(int k) {
        int ans = 0;
        for (int j = 0; j < 30; j++) {
            int m = (1 << j) - k;
            if (0 <= m && m <= j + 1) {
                ans += c[j + 1][m];
            }
        }
        return ans;
    }
};