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 题。
解答
一、两种代码的核心定位
- 逆推 DP
从终点倒推回起点,反着走。 - 顺推 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的方案:
- 最后一步花(合法)
- 最后一步店(非法,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\)斗酒的合法行走方案总数。
整体采用逆向动态规划思想:从终点状态反向推导初始状态,把正向操作规则反向使用。
正向规则:
- 经过店铺:酒量 \(w \Rightarrow w\times2\)
- 经过花朵:酒量 \(w \Rightarrow w-1\)(\(w\ge1\))
逆向推导规则:
- 逆向回溯店铺:当前酒量\(k\) 一定由上一状态酒量\(\boldsymbol{k/2}\) 翻倍得到,因此\(k\)必须为偶数
- 逆向回溯花朵:当前酒量\(k\) 一定由上一状态酒量\(\boldsymbol{k+1}\) 减1得到
二、语句拆分正式释义
1. 回溯店铺逻辑
if (i > 0 && k % 2 == 0) {
dp[i][j][k] = dp[i-1][j][k / 2];
}
- 判定条件
- \(i>0\):保证存在可回溯的店铺步数
- \(k\%2==0\):满足逆向回退店铺的酒量硬性条件,只有偶数酒量才能由翻倍操作逆向还原
- 赋值使用等号
=而非累加+=的正式原因
从逆向路径唯一性分析:一个确定的\((i,j,k)\)状态,不可能存在两种不同店铺回溯路径。
想要逆向走到「\(i\)店\(j\)花\(k\)斗酒」,若上一步是店铺,则唯一前驱状态只能是:\(i-1\)店、\(j\)花、\(k/2\)斗酒,前驱状态唯一。
该状态的方案数直接等价于唯一前驱的方案数,不存在多条店铺路径叠加的情况,因此直接赋值即可,无需累加。 - 无需取模运算原因
前驱状态\(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;
}
- 判定条件
\(j>0\):保证存在可回溯的花朵步数,花朵回溯无酒量奇偶限制 - 使用累加
+=的正式原因
同一\((i,j,k)\)状态,路径来源不唯一:
该状态既可以由回溯店铺得到,也可以由回溯花朵得到。
店铺路径已经通过赋值完成基础赋值,花朵路径属于新增合法路径,必须使用累加运算合并两类不同来源的方案总数。 - 必须进行取模运算原因
累加操作会持续增大方案数值,极易超出long类型存储上限、超出题目规定模数范围,因此每一次累加后都要对结果取模,保证数据合法。
三、核心逻辑总结(正式结论)
- 逆向回溯店铺:前驱状态唯一,路径来源单一,采用直接赋值
=,无取模操作; - 逆向回溯花朵:前驱状态独立,可与店铺路径共存,路径来源多元,采用累加
+=,累加后强制取模; - 整体逻辑:先确定店铺回溯带来的基础方案数,再叠加花朵回溯带来的新增方案数,最终合并得到该状态下所有合法行走方案总数。
四、顺推与逆推写法差异补充(对照区分)
正向顺推DP中,前往店铺、前往花朵均属于向外拓展新状态,所有新状态都存在多条抵达路径,因此全部统一使用+=累加+取模;
而本题逆推DP存在路径唯一性差异,是本题专属写法特征,也是该代码精简高效的核心原因。
浙公网安备 33010602011771号