经典落梯问题

1. 问题背景与基本模型

设想有一座塔,塔内竖立着 ( W ) 根柱子,从上到下经过 ( H ) 层(也就是有 ( H ) 行可以铺设横向桥梁)。在每一层中,在相邻柱子之间可以放置一根横桥,但必须遵守两个限制条件:

  1. 桥梁位置受限
    每根桥只能连接相邻的两根柱子,在同一层最多有 ( W-1 ) 个潜在位置(分别在柱子1–2、2–3,……,( W-1 )–( W ))。

  2. 相邻桥梁冲突
    在同一层内,不能在相邻的位置都放桥梁。也就是说,如果在柱子 i 和 i+1 之间放了桥,那么在 i-1 和 i 之间或 i+1 和 i+2 之间都不得放桥,因为这会导致两个桥共享相同的接点。

总体来说,每一行的桥梁安排就是在 ( W-1 ) 个位置上选择若干个位置,使得“相邻两个位置不能同时选中”。这种选取方案具有鲜明的组合特征。


2. 桥梁如何影响路径

想象参与试炼的人从塔顶某一根柱子出发(通常起点固定为第一根柱子),沿着每一层的桥梁走动直到到底部。每层的横桥安排会对移动方向产生影响,规则通常如下:

  • 向左移动的条件
    如果你当前在第 ( j ) 根柱子,并且在这一行中你左侧(也就是连接柱 ( j-1 ) 与 ( j ) 的位置)有一座桥,那么你会沿着桥横走到左边,即来到柱 ( j-1 )。

  • 向右移动的条件
    如果没有左侧桥但在你右侧(连接柱 ( j ) 与 ( j+1 ) 的位置)有桥,那么你会横移至柱 ( j+1 )。

  • 保持不动
    如果左右两侧都没有桥,你仅向下移动,保持在原来的垂直线上。

这种规则说明了,在每一行,桥梁是以“局部作用”的方式改变你的列位置。整座塔的效果则是各行变化的复合。


3. 单行铺桥的合法方案

在一行中,例如对于 ( W=3 )(即有 3 根柱子),有 ( W-1 = 2 ) 个桥梁潜在位置。用“1”表示放桥,用“0”表示不放桥,则合法的铺桥方案有:

  • “00”:两个位置都不放桥。
  • “10”:在第一位置放桥,第二位置为空。
  • “01”:第一位置没有桥,第二位置放桥。

注意:“11”是不合法的,因为两个桥梁是相邻的,会共享柱子2的接点。

这种选择问题在组合数学中常与“不能选相邻元素”联系起来,其总数可以用类似斐波那契的数列来描述;具体地,如果有 ( n ) 个位置,合法的选择方案数为

C ( n ) = ∑ k = 0 ⌊ n / 2 ⌋ ( n − k + 1 k ) C(n) = \sum_{k=0}^{\lfloor n/2 \rfloor} \binom{n-k+1}{k} C(n)=k=0n/2(knk+1)
但在实际“落梯问题”里,我们往往不直接统计一行所有情况,而是关心某种局部布局对当前柱子的横移影响——也就是它对“向左”、“向右”或“直走”的贡献值。


4. 举例说明每行铺桥对路径的影响

我们以 ( W=3 ) 为例,说明一行桥梁安排对不同柱上位置的影响。

情况分析

设柱子依次编号为 1、2、3,对应可放桥的位置用 “位置1”(连接柱1和2)和 “位置2”(连接柱2和3)。

(1)方案“00”:
  • 不放桥
    对于任何柱子,既没有左侧也没有右侧的横桥,故都只做垂直移动。

    • 当你在柱1、柱2、或柱3时,均保持原位。
(2)方案“10”:
  • 桥放在位置1(即连接柱1和2)
    检查每个柱子的情况:
    • 柱1
      没有左侧(边界不能向左),但右侧有桥(在位置1),因此 从柱1横移到柱2
    • 柱2
      左侧有桥(位置1),因此 从柱2横移到柱1
      (即使右侧没有桥,由于规则首先判断左侧,必定先沿左侧走。)
    • 柱3
      柱3既没有左侧的桥(位置2为空)也没有右侧(超过边界),故 保持在柱3
(3)方案“01”:
  • 桥放在位置2(即连接柱2和3)
    逐一分析:
    • 柱1
      左侧无桥,右侧(连接柱1和2)也没有桥,所以 保持在柱1
    • 柱2
      左侧无桥;检查右侧,有桥(位置2),所以 从柱2横移到柱3
    • 柱3
      左侧有桥(位置2),故 从柱3横移到柱2

5. 复合多行的路径计算

当塔有多层(多行)桥梁时,整体路径是各行“作用”的复合。假设有 2 行,我们用上面的分析即可推断:

  • 起点固定:从塔顶第一根柱子(柱1)开始。

第一行的可能情况

假设第一行选择:

  • 方案“00”:从柱1保持在柱1。
  • 方案“10”:从柱1横移至柱2。
  • 方案“01”:从柱1保持在柱1(因为“01”中从柱1没有移动)。

因此,经过第一行之后,有两种情况:

  • 保持在柱1:共2种情况(“00”与“01”)。
  • 移动到柱2:共1种情况(“10”)。

第二行的可能情况

接下来,针对第一行结束时所在的列,我们再看第二行的选择如何影响:

  • 如果你在柱1
    分析第二行:

    • 采用方案“00”:依然从柱1保持在柱1。
    • 采用方案“10”:从柱1横移到柱2(因为右侧有桥)。
    • 采用方案“01”:仍然保持在柱1(无桥影响)。

    从柱1来看,走“00”或“01”可以保持在柱1,走“10”会离开成为柱2。

  • 如果你在柱2(来源于第一行的“10”):
    分析第二行:

    • 采用方案“00”:在柱2保持在柱2。
    • 采用方案“10”:对于在柱2的情况,左侧有桥(位置1),因此会 横移到柱1
    • 采用方案“01”:对于在柱2的情况,右侧有桥(位置2),因此会 横移到柱3

目标统计

问题中给定的是:从起点在柱1出发,经过所有 ( H ) 行最终要求到达目标柱(例如样例中目标柱 ( K=1 ) 即最后仍在柱1)。

把上面两行情况组合:

  • 如果第一行使你停留在柱1(“00”或“01”,共2种情况),那么第二行只有当选择“00”或“01”时才能保持在柱1。所以,这两大类情况中各有 2 种有效选择(共 2 × 2 = 4)。
  • 如果第一行走了“10”,由此你到达柱2,此时第二行的“10”会使你从柱2移动到柱1(而“00”或“01”不会使你到达柱1);这条路径只有 1 种组合。

合计:4 + 1 = 5 种方法,正好与样例答案吻合。


6. 组合思想与动态规划思想

如果层数较多,手工枚举显然不现实。理想的方法是动态规划。一般可以定义状态 ( dp[i][j] ) 表示经过前 ( i ) 行之后,落在第 ( j ) 根柱子的路径数。每一行对状态的转移要综合这一行所有可能合法的桥梁排列,并且根据桥梁在当前位置左右的情况确定运动方向。核心在于:

  • 对于任一行,如何快速将“所有合法桥梁配置”的影响数(即那些使你从某柱子移到左相邻、右相邻、或不变的位置的办法数)计算出来。

这里的“合法桥梁配置的数”实际上和排列组合有关,常常可以用类似 Fibonacci 数列的关系来求得(因为问题归结于在一维上不相邻选取的问题)。


下面详细说明这种“桥梁排列”与斐波那契数列之间的关系。


7.为什么符合斐波那契递推?

为了解“合法排列”的总数,我们可以采用递归思路:

  • 考虑第一个位置的选择
    假设有 n 个位置,记 f(n) 表示在这 n 个位置上满足条件的排列总数。现在看第一个位置可以如何选择:

    1. 第一个位置不放桥(记为 0)
      如果第一个位置不放桥,那么剩下的 n-1 个位置随意排列,且排列方式数为 f(n-1)。

    2. 第一个位置放桥(记为 1)
      但如果第一个位置放了桥,为了避免和紧邻第二个位置形成连续“11”,第二个位置必然不能放桥(必须为 0)。这时,剩下的 n-2 个位置可以任意安排,排列方式数为 f(n-2)。

因此,我们有递推关系:
f ( n ) = f ( n − 1 ) + f ( n − 2 ) f(n) = f(n-1) + f(n-2) f(n)=f(n1)+f(n2)
这正是斐波那契数列的典型递推公式!


基本情况(边界条件)

  • 当没有位置可以选择时,我们认为有一种“空”排列:

f ( 0 ) = 1. f(0) = 1. f(0)=1.

  • 当只有一个位置时,这个位置可以选择放或不放,通常会得到两个可能。但在一些应用场景中,由于边界条件或额外约束(比如后续状态已固定),可能将 f(1) 设为 1 或 2。
    在很多落梯问题的动态规划模型中,常常通过设置特定的初始条件(例如 f(0)=1, f(1)=1)来配合整体转移公式,使得组合时左右两侧的空段计算能够统一起来。不过从纯组合角度来看,不连续排列的总数应该是 f(1)=2:

    • “0”:不放桥
    • “1”:放桥

    这种细微差别往往是为了配合题目里的左右空段计算而设定的。无论如何,关键在于后续对于剩余位置排列方案的计算,都是遵循 f(n)=f(n-1)+f(n-2) 的递推关系。


举例

假设有 3 个位置(n = 3),来看看所有满足“不能连续放桥”的排列:

  1. 000
  2. 001
  3. 010
  4. 100
  5. 101

可以看到总共就是 5 种排列,而这正好与斐波那契数列在这个规模下的某个值相符。证明过程如下:

  • f(0) = 1
  • f(1) = 2 (如果允许空和放桥两个选项)
  • f(2) = f(1) + f(0) = 2 + 1 = 3
  • f(3) = f(2) + f(1) = 3 + 2 = 5

(注意:在某些具体题务中可能为了配合其他转移计算,将 f(1) 设为 1,从而整体数值会稍有不同,但递推公式不变。)


总结

结论:
当你面对这样的问题时,每个位置都可以看作一个二元决策,但“放桥”时必须保证其后一个位置一定不放桥,这样的选择过程自然满足
f ( n ) = f ( n − 1 ) + f ( n − 2 ) f(n) = f(n-1) + f(n-2) f(n)=f(n1)+f(n2)
的递推关系,也就是斐波那契数列的递推。正因如此,我们常说“满足不能在相邻位置都放桥的合法方案数可按斐波那契数列来计算”。

通过这种方式,我们就能快速统计出一行中所有合法桥梁配置的方案数,而这一结果又可以用于整个落梯问题的动态规划转移中。

如果你对这种递推关系如何运用于不同的状态转移、如何计算左右空段的方案数等有更多疑问,我们可以进一步探讨相关组合数学和 DP 的细节。

7. 总结

“落梯问题”本质上是一个将几何路径问题转化为有限状态转移的计数问题。

  • 每一行的桥梁安排决定了一种局部的交换(或不交换)操作。
  • 通过层层组合,我们可以利用动态规划的方法,计数出总共的安排方式,使得从顶端的起点经过所有行后最终落在指定的目标柱子。
  • 问题中限制桥梁“不能相邻”的规则,会让每行的合法桥梁配置数呈现出类似 Fibonacci 数列的性质,这也是众多类似问题中常见的组合数学现象。

这种思路既美观又实用,既考验对组合数学的理解,又锻炼对状态转移和路径复合的思考。进一步的扩展可能还会考虑不同的边界条件、更多的初始与目标位置,或修改移动规则,这些都能从当前的模型中衍生出更丰富的数学故事。

8.代码

#include 
#include 
using namespace std;
typedef long long ll;
const ll MOD = 1000000007;

int main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int H, W, K;
    cin >> H >> W >> K;
    // 注意:题目中柱子编号从 1 开始,而在代码中我们使用 0-index,
    // 因此起点是 0(第一根柱子),目标为 K-1

    // dp[i][j] 表示经过 i 层(横桥行)后落在第 j 根柱子的方案数
    vector> dp(H + 1, vector(W, 0));
    dp[0][0] = 1; // 初始状态:从第一根柱子开始

    // 预处理 数组 f[]:在一行中,假定共有 n = W-1 个桥梁放置的位置,
    // 其中满足不能在相邻位置都放桥的合法方案数可按斐波那契数列来计算
    // 这里我们预处理 f[i] 表示 i 个连续空位的桥梁排列方式数
    // 边界:f[0] = 1, f[1] = 1; 对于 i >= 2:f[i] = f[i-1] + f[i-2] (取模 MOD)
    vector f(W + 1, 0);
    f[0] = 1;
    if(W >= 1)
        f[1] = 1;
    for (int i = 2; i <= W; i++){
        f[i] = (f[i - 1] + f[i - 2]) % MOD;
    }

    // 动态规划逐行递推。
    // 每一行的桥梁排列对运动方向的贡献:
    // 当学徒处在第 j 根柱子时:
    // 1. 向左移动(到 j-1):要求 j > 0,并且这一层对应位置 j-1 固定放桥,
    //    同时桥梁的左侧空白方案数为 f[j-1](左边段)和右侧空段 f[W-j-1] 两部分相乘。
    // 2. 向右移动(到 j+1):要求 j < W-1,固定 j 位置放桥,
    //    对应空段方案数为 f[j](左侧)和 f[W-j-2](右侧)。
    // 3. 不横移(保持 j):要求两侧(j-1 和 j 位置)都没有放桥,
    //    方案数为 f[j](左侧)和 f[W-j-1](右侧)。
    for (int i = 0; i < H; i++){
        for (int j = 0; j < W; j++){
            ll ways = dp[i][j];
            if(ways == 0) continue;
            
            // 情况1:从 j 向左
            if(j > 0) {
                ll add = ways;
                // 左侧空白部分有 f[j-1] 种方案,
                // 右侧空白部分有 f[W - j - 1] 种方案
                add = (add * f[j - 1]) % MOD;
                add = (add * f[W - j - 1]) % MOD;
                dp[i + 1][j - 1] = (dp[i + 1][j - 1] + add) % MOD;
            }
            
            // 情况2:从 j 向右移动
            if(j < W - 1) {
                ll add = ways;
                // 左侧空段: f[j] 种方案,
                // 右侧空段: f[W - j - 2] 种方案
                add = (add * f[j]) % MOD;
                add = (add * f[W - j - 2]) % MOD;
                dp[i + 1][j + 1] = (dp[i + 1][j + 1] + add) % MOD;
            }
            
            // 情况3:保持在 j 不横移
            {
                ll add = ways;
                // 左侧空段: f[j] ,右侧空段: f[W - j - 1]
                add = (add * f[j]) % MOD;
                add = (add * f[W - j - 1]) % MOD;
                dp[i + 1][j] = (dp[i + 1][j] + add) % MOD;
            }
        }
    }

    // 输出答案:目标为第 K 根柱子,即 dp[H][K-1]
    cout << dp[H][K - 1] % MOD << "\n";
    return 0;
}

你可能感兴趣的:(算法,算法,学习方法,c++,数据结构,动态规划)