P10376 [GESP202403 六级] 游戏

记录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 开始,不断减 ab,直到 ≤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 - ax - 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 ≤ 2e5
  • dp 大小 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-an-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 - ai - 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 ≤ cdp[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 ≤ ci ≤ 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 > c1 > 1 为假)
    • 然后累加 i=0 to N+1dp[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 ≤ 1dp[N+x] 之和 = 176 ✅

总结

技巧 说明
反向传播 DP 从起点 n 向下传播路径数,而非递归计算
偏移量处理负下标 dp[N + x] 表示 x 的状态,避免负数下标
倒序遍历 确保每个状态只被处理一次,路径不重复计算
统一结束条件 所有 x ≤ c 都是合法终点,直接累加

用 O(n) 时间和空间解决了路径计数问题

相关推荐
智者知已应修善业1 小时前
【51单片机4个IO实现16按键可扩展独立按键64矩阵驱动显示矩阵原值】2023-5-8
c++·经验分享·笔记·算法·51单片机
hui-梦苑1 小时前
[GROMACS]模拟数据分析前轨迹文件生成-轨迹预处理
人工智能·算法·数据分析
蒸汽求职1 小时前
低延迟系统优化:针对金融 IT 与高频交易,如何从 CPU 缓存行(Cache Line)对齐展现硬核工程底蕴?
sql·算法·缓存·面试·职场和发展·金融·架构
田梓燊2 小时前
leetcode 239
数据结构·算法·leetcode
CoderCodingNo10 小时前
【NOIP】2011真题解析 luogu-P1003 铺地毯 | GESP三、四级以上可练习
算法
iFlyCai10 小时前
C语言中的指针
c语言·数据结构·算法
Laurence10 小时前
C++ 引入第三方库(一):直接引入源文件
开发语言·c++·第三方库·添加·添加库·添加包·源文件
查古穆10 小时前
栈-有效的括号
java·数据结构·算法
再一次等风来10 小时前
近场声全息(NAH)仿真实现:从阵列实值信号到波数域重建
算法·matlab·信号处理·近场声全息·nah