P8786 [蓝桥杯 2022 省 B] 李白打酒加强版

P8786 [蓝桥杯 2022 省 B] 李白打酒加强版

题目描述

话说大诗人李白,一生好饮。幸好他从不开车。

一天,他提着酒壶,从家里出来,酒壶中有酒 \(2\) 斗。他边走边唱:

无事街上走,提壶去打酒。
逢店加一倍,遇花喝一斗。

这一路上,他一共遇到店 \(N\) 次,遇到花 \(M\) 次。已知最后一次遇到的是花,他正好把酒喝光了。

请你计算李白这一路遇到店和花的顺序,有多少种不同的可能?

注意:壶里没酒(\(0\) 斗)时遇店是合法的,加倍后还是没酒;但是没酒时遇花是不合法的。

输入格式

第一行包含两个整数 \(N\)\(M\)

输出格式

输出一个整数表示答案。由于答案可能很大,输出模 \(1000000007\)(即 \(10^9+7\))的结果。

输入输出样例 #1

输入 #1

5 10

输出 #1

14

说明/提示

【样例说明】

如果我们用 0 代表遇到花,1 代表遇到店,\(14\) 种顺序如下:

010101101000000
010110010010000
011000110010000
100010110010000
011001000110000
100011000110000
100100010110000
010110100000100
011001001000100
100011001000100
100100011000100
011010000010100
100100100010100
101000001010100

【评测用例规模与约定】

对于 \(40 \%\) 的评测用例:\(1 \leq N, M \leq 10\)

对于 \(100 \%\) 的评测用例:\(1 \leq N, M \leq 100\)

蓝桥杯 2022 省赛 B 组 I 题。


解答

一、两种代码的核心定位

  1. 逆推 DP
    终点倒推回起点,反着走。
  2. 顺推 DP
    起点推到终点,正着走。

二、状态定义对比

🧠 逆推 DP

dp[i][j][k]
含义:
遇到 i 个店,j 个花,当前有 k 斗酒
总共有多少种方案能到达这个状态

方向:从结果往回走

🧠 顺推 DP

dp[i][j][w][last]
含义:
走了 i 个店,j 个花,剩 w 斗酒
last=0 最后一步是店,last=1 最后一步是花
总共有多少种方案

方向:从起点往前走

三、转移思路对比

1. 逆推 DP(倒着走)

规则反过来:

  • 遇店(正):酒 ×2
    逆推:当前酒必须是偶数 → 上一步酒 = k/2
  • 遇花(正):酒 -1
    逆推:上一步酒 = k+1

逆推完整代码

import java.util.Scanner;
public class Main {
    static final int MOD = 1000000007;
    static long dp[][][] = new long[110][110][110];

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt(); // 店总数
        int m = sc.nextInt(); // 花总数

        dp[0][0][2] = 1; // 初始:0店0花,2斗酒

        // 逆推枚举所有状态
        for (int i = 0; i <= n; i++) {
            for (int j = 0; j <= m; j++) {
                for (int k = 0; k <= m - j; k++) {

                    // 情况1:上一步是【店】
                    // 正:遇店酒×2 → 逆推:当前酒必须是偶数,上一步= k/2
                    if (i > 0 && k % 2 == 0) {
                        dp[i][j][k] = dp[i-1][j][k / 2];
                    }

                    // 情况2:上一步是【花】
                    // 正:遇花酒-1 → 逆推:上一步酒 = k+1
                    if (j > 0) {
                        dp[i][j][k] += dp[i][j-1][k+1];
                        dp[i][j][k] %= MOD;
                    }
                }
            }
        }

        // 答案:店用完,花剩最后一朵,酒=1
        // 最后一步必是花,喝掉变0,完美合法
        System.out.println(dp[n][m-1][1] % MOD);
    }
}

2. 顺推 DP(正着走)

规则和题目完全一样:

  • 遇店:酒 ×2
  • 遇花:酒 -1(必须 ≥1)
    必须加一维记录最后一步动作,排除非法方案。

顺推完整代码

import java.util.Scanner;

public class Main {
    static final int MOD = 1000000007;
    // dp[i][j][w][op]
    // i:店数 j:花数 w:酒量 op:最后一步 0店 1花
    static long[][][][] dp = new long[105][105][205][2];

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int m = sc.nextInt();

        // 初始:0店 0花 2斗酒,无前置操作,单独初始化
        dp[0][0][2][0] = 1;

        for (int i = 0; i <= n; i++) {
            for (int j = 0; j <= m; j++) {
                for (int w = 0; w <= 200; w++) {
                    // 遍历两种结尾状态
                    for (int last = 0; last <= 1; last++) {
                        long cnt = dp[i][j][w][last];
                        if (cnt == 0) continue;  //剪枝

                        // 选择1:下一步遇到店 → 酒翻倍,最后一步变店(0)
                        if (i < n) {
                            int nw = w * 2;
                            if (nw <= 200) {
                                dp[i+1][j][nw][0] = (dp[i+1][j][nw][0] + cnt) % MOD;
                            }
                        }

                        // 选择2:下一步遇到花,必须有酒 → 酒减1,最后一步变花(1)
                        if (j < m && w >= 1) {
                            int nw = w - 1;
                            dp[i][j+1][nw][1] = (dp[i][j+1][nw][1] + cnt) % MOD;
                        }
                    }
                }
            }
        }

        // 合法答案:用完所有店花、酒为0、最后一步一定是花
        System.out.println(dp[n][m][0][1] % MOD);
    }
}

四、答案为什么不一样?

逆推答案:dp[n][m-1][1]

  • 最后一步必须是花
  • 最后一步前状态:
    店用完 = n
    花用了 = m-1
    酒 = 1
  • 走最后一朵花 → 酒变 0
    逆推天然无非法方案!

顺推答案:dp[n][m][0][1]

  • 顺推会产生两种酒=0的方案:
    1. 最后一步(合法)
    2. 最后一步(非法,0×2=0)
  • 必须加一维 last=1 过滤非法方案

五、优缺点终极对比

逆推 DP 顺推 DP
思考方向 从终点倒推 从起点正推
代码长度 极短 稍长
是否好理解 难(反直觉) 易(贴合题意)
非法方案 自动全部过滤 必须加一维过滤
酒量限制 自动约束 需手动设上限
考试推荐 ⭐⭐⭐⭐⭐ 标答 ⭐⭐⭐⭐ 正确稳妥
是否AC ✅ 100% ✅ 100%

六、最简单记忆口诀

逆推

店回来酒减半,花回来酒加一
答案 dp[n][m-1][1]

顺推

遇店酒翻倍,遇花酒减一
最后一步必须是花
答案 dp[n][m][0][1]

逆推DP中赋值=与累加+=区分原因

一、前置回顾

状态定义:\(dp[i][j][k]\) 表示已经经过\(i\)家店、\(j\)朵花,当前剩余\(k\)斗酒的合法行走方案总数。
整体采用逆向动态规划思想:从终点状态反向推导初始状态,把正向操作规则反向使用。
正向规则:

  1. 经过店铺:酒量 \(w \Rightarrow w\times2\)
  2. 经过花朵:酒量 \(w \Rightarrow w-1\)\(w\ge1\)

逆向推导规则:

  1. 逆向回溯店铺:当前酒量\(k\) 一定由上一状态酒量\(\boldsymbol{k/2}\) 翻倍得到,因此\(k\)必须为偶数
  2. 逆向回溯花朵:当前酒量\(k\) 一定由上一状态酒量\(\boldsymbol{k+1}\) 减1得到

二、语句拆分正式释义

1. 回溯店铺逻辑

if (i > 0 && k % 2 == 0) {
    dp[i][j][k] = dp[i-1][j][k / 2];
}
  1. 判定条件
    • \(i>0\):保证存在可回溯的店铺步数
    • \(k\%2==0\):满足逆向回退店铺的酒量硬性条件,只有偶数酒量才能由翻倍操作逆向还原
  2. 赋值使用等号=而非累加+=的正式原因
    从逆向路径唯一性分析:一个确定的\((i,j,k)\)状态,不可能存在两种不同店铺回溯路径
    想要逆向走到「\(i\)\(j\)\(k\)斗酒」,若上一步是店铺,则唯一前驱状态只能是:\(i-1\)店、\(j\)花、\(k/2\)斗酒,前驱状态唯一
    该状态的方案数直接等价于唯一前驱的方案数,不存在多条店铺路径叠加的情况,因此直接赋值即可,无需累加。
  3. 无需取模运算原因
    前驱状态\(dp[i-1][j][k/2]\)在之前的循环运算中已经完成取模约束,数值天然小于模数\(10^9+7\),直接赋值不会出现数值溢出,无需重复取模。

2. 回溯花朵逻辑

if (j > 0) {
    dp[i][j][k] += dp[i][j-1][k+1];
    dp[i][j][k] %= MOD;
}
  1. 判定条件
    \(j>0\):保证存在可回溯的花朵步数,花朵回溯无酒量奇偶限制
  2. 使用累加+=的正式原因
    同一\((i,j,k)\)状态,路径来源不唯一
    该状态既可以由回溯店铺得到,也可以由回溯花朵得到。
    店铺路径已经通过赋值完成基础赋值,花朵路径属于新增合法路径,必须使用累加运算合并两类不同来源的方案总数。
  3. 必须进行取模运算原因
    累加操作会持续增大方案数值,极易超出long类型存储上限、超出题目规定模数范围,因此每一次累加后都要对结果取模,保证数据合法。

三、核心逻辑总结(正式结论)

  1. 逆向回溯店铺:前驱状态唯一,路径来源单一,采用直接赋值=,无取模操作;
  2. 逆向回溯花朵:前驱状态独立,可与店铺路径共存,路径来源多元,采用累加+=,累加后强制取模;
  3. 整体逻辑:先确定店铺回溯带来的基础方案数,再叠加花朵回溯带来的新增方案数,最终合并得到该状态下所有合法行走方案总数。

四、顺推与逆推写法差异补充(对照区分)

正向顺推DP中,前往店铺、前往花朵均属于向外拓展新状态,所有新状态都存在多条抵达路径,因此全部统一使用+=累加+取模
而本题逆推DP存在路径唯一性差异,是本题专属写法特征,也是该代码精简高效的核心原因。

posted @ 2026-05-20 12:22  kitic  阅读(9)  评论(0)    收藏  举报