记录96
cpp
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+5,MOD=1e9+7;
int dp[N+N];//防止出现负下标的情况
int main(){//反向推路径
int n,a,b,c;
cin>>n>>a>>b>>c;
dp[N+n]=1;//开始的位置是第一个路径
for(int i=n;i>c;i--){//a,b两种方法的路径都要尝试
dp[N+i-a]=(dp[N+i-a]+dp[N+i])%MOD;
dp[N+i-b]=(dp[N+i-b]+dp[N+i])%MOD;
}//目标位置=目标位置的路径数(有可能另一条路提前来过)+上一条路径的数量
int ans=0;
for(int i=0;i<=N+c;i++) ans=(ans+dp[i])%MOD;
cout<<ans;
return 0;
}
题目传送门
https://www.luogu.com.cn/problem/P10376
突破口
你有四个正整数 n,a,b,c,并准备用它们玩一个简单的小游戏。
在一轮游戏操作中,你可以选择将 n 减去 a,或是将 n 减去 b。游戏将会进行多轮操作,直到当 n≤c 时游戏结束。
你想知道游戏结束时有多少种不同的游戏操作序列。两种游戏操作序列不同,当且仅当游戏操作轮数不同,或是某一轮游戏操作中,一种操作序列选择将 n 减去 a,而另一种操作序列选择将 n 减去 b。如果 a=b,也认为将 n 减去 a 与将 n 减去 b 是不同的操作。
由于答案可能很大,你只需要求出答案对 10^9+7 取模的结果。
思路
🎯 游戏规则
- 初始值为
n - 每轮操作:选择减去
a或减去b - 游戏在
n ≤ c时结束 - 问:有多少种不同的操作序列?
⚠️ 注意:
- 即使
a = b,也认为"减 a"和"减 b"是两种不同操作(题目明确说明)- 序列不同 ⇨ 轮数不同,或某一轮选择不同
✅ 举例
输入:n=1, a=1, b=1, c=1
- 初始
n=1 ≤ c=1→ 游戏立即结束,0 轮操作 - 但题目输出是
1→ 说明 "空操作序列"也算一种方案
📌 结论:当初始 n ≤ c 时,答案 = 1(什么都不做)
🧠 二、解题思路:动态规划(反向递推)
正向思考的问题
- 从
n开始,不断减a或b,直到 ≤c - 但路径可能非常多,且状态重复(如
n - a - b = n - b - a) - 需要记忆化搜索或 DP
更优策略:反向 DP(从终点往起点推)
但本题代码采用的是 正向状态 + 反向转移 的写法:
- 定义
dp[x]= 从当前值x出发,到游戏结束的不同操作序列数 - 目标:求
dp[n]
状态转移(正向定义):
- 若
x ≤ c→ 游戏结束 →dp[x] = 1(空序列) - 否则:
dp[x] = dp[x - a] + dp[x - b]
❗ 但注意:
x - a或x - b可能 ≤0,甚至负数!
然而,题目保证 a, b ≥ 1,且 n ≤ 2e5,所以 x - a 最小为 1 - 2e5 ≈ -2e5
→ 需要处理负下标
💡 代码的巧妙设计:偏移量(Offset)避免负下标
- 定义数组
dp[N + N],其中N = 2e5 + 5 - 实际用
dp[N + x]表示dp[x] - 这样即使
x为负(最小约-2e5),N + x ≥ 0,不会越界
✅ 偏移量技巧:将实际值
x映射到数组下标N + x
代码解析
cpp
#include<bits/stdc++.h>
using namespace std;
const int N = 2e5 + 5, MOD = 1e9 + 7;
int dp[N + N]; // 数组大小 4e5+10,足够覆盖 [ -2e5, 2e5 ]
N = 200005,确保n ≤ 2e5dp大小2*N,下标范围[0, 2N-1]- 实际值
x对应下标N + x,所以x ∈ [-N, N)都安全
cpp
int main(){
int n, a, b, c;
cin >> n >> a >> b >> c;
- 读入四个正整数
cpp
dp[N + n] = 1; // 初始状态:从 n 出发有 1 种"当前路径"
- 这里不是传统 DP 的"答案初始化",而是模拟正向传播
- 思路:从起点
n开始,把它的"路径数"向后传递给n-a和n-b - 所以
dp[N + n] = 1表示"有一条路径到达了 n(起点)"
🔄 这是一种 BFS 式的 DP 传播:从高值向低值扩散路径数
cpp
for(int i = n; i > c; i--){ // 从 n 递减到 c+1
dp[N + i - a] = (dp[N + i - a] + dp[N + i]) % MOD;
dp[N + i - b] = (dp[N + i - b] + dp[N + i]) % MOD;
}
- 关键循环 :从大到小遍历
i - 对每个
i > c(游戏未结束):- 它可以转移到
i - a和i - b - 把
dp[i]的路径数加到dp[i - a]和dp[i - b]上
- 它可以转移到
🌟 为什么倒序?
- 因为每次只用当前
i的值去更新更小的值- 不会重复使用已更新的值(类似完全背包 vs 0-1 背包)
- 这里是每一步只能走一次 (操作序列顺序重要),所以必须倒序保证每个
i只被处理一次
✅ 举例:
dp[n] = 1- 处理
i = n→ 把 1 加到dp[n-a]和dp[n-b] - 处理
i = n-1→ 如果之前被更新过,就继续传播
最终,所有能到达的 x ≤ c 的位置,其 dp[x] 就是从 n 出发、以 x 为终点的路径数
cpp
int ans = 0;
for(int i = 0; i <= N + c; i++)
ans = (ans + dp[i]) % MOD;
- 累加所有 游戏结束状态 的路径数
- 游戏结束条件:
x ≤ c - 在偏移数组中,
x ≤ c对应下标N + x ≤ N + c - 但注意:
x可能为负!所以理论上应累加所有x ≤ c的dp[N + x]
然而,代码写的是
cpp
for(int i = 0; i <= N + c; i++) ans += dp[i];
i是数组下标i ≤ N + c⇨ 对应的实际值x = i - N ≤ c- ✅ 完全正确!因为
x = i - N ≤ c⇨i ≤ N + c
💡 同时,
x最小可能为负,但i = N + x ≥ 0(因x ≥ -N),所以i从 0 开始是安全的
cpp
cout << ans;
return 0;
}
🧪 样例验证
样例 1:n=1, a=1, b=1, c=1
- 初始
n=1 ≤ c=1→ 游戏不进行任何操作 - 但代码:
dp[N+1] = 1- 循环
i from 1 to >1→ 不执行(因为i > c即1 > 1为假) - 然后累加
i=0 to N+1的dp[i] - 其中
dp[N+1] = 1,且N+1 ≤ N+c = N+1→ 被计入 - 所以
ans = 1✅
🤔 但按题意,
n=1已经结束,不应有操作,为何dp[N+1]被算作结束状态?
- 因为
1 ≤ c,所以x=1是结束状态!- 所以
dp[N+1]代表"在 1 结束"的路径数 = 1(空序列)
✅ 完全符合!
样例 2:n=114, a=51, b=4, c=1
- 代码会从 114 逐步向下传播路径数
- 最终所有
x ≤ 1的dp[N+x]之和 = 176 ✅
总结
| 技巧 | 说明 |
|---|---|
| 反向传播 DP | 从起点 n 向下传播路径数,而非递归计算 |
| 偏移量处理负下标 | 用 dp[N + x] 表示 x 的状态,避免负数下标 |
| 倒序遍历 | 确保每个状态只被处理一次,路径不重复计算 |
| 统一结束条件 | 所有 x ≤ c 都是合法终点,直接累加 |
用 O(n) 时间和空间解决了路径计数问题