奶牛沙盘队(DP)_
问题 R: 【基础】奶牛沙盘队
题目描述
Farmer Han开始玩飞盘之后,YDS也打算让奶牛们享受飞盘的乐趣.他要组建一只奶牛飞盘队.他的N(1≤N≤2000)只奶牛,每只奶牛有一个飞盘水准指数Ri(1≤Ri≤100000).YDS要选出1只或多于1只奶牛来参加他的飞盘队.由于YDS的幸运数字是F(1≤F≤1000),他希望所有奶牛的飞盘水准指数之和是幸运数字的倍数.
帮YDS算算一共有多少种组队方式.组队方式数模10^8取余的结果.
输入
第1行输入N和F,之后N行输入Ri
输出
组队方式数模10^8取余的结果
样例输入
4 5 12 8 2
样例输出
3
分析
- 确定状态
dp[i][j]
为前i头奶牛,得到飞盘准指数为j的方案数 - 确定边界,当0头奶牛获得0的方案数有1个,因此
dp[0][0] == 1
- 状态转移````dp[i][j] += dp[i-1][j] 当不选第i头牛的时候
dp[i][j] += dp[i-1][(j-v[i]+f)%f] 当选择第i头牛的时候```
注意到dp[i-1][(j-v[i]+f)%f]
,因为题目要求的是余数,因此不必考虑j >= v[i]
的条件
我写的一些错误
2. 状态定义和下标范围的问题
-
状态定义:
通常我们用dp[i][r]
表示前i
只奶牛构成的子集(通常包括空集)的方案数,其中这些奶牛的飞盘水准指数之和对 ( F ) 取模后等于 ( r )。 -
下标范围:
因为模 ( F ) 的余数只可能在 0 到 ( F-1 ),所以dp[i]
的第二维应该有 ( F ) 个位置,而不是 1005 个位置。如果题目中 ( F ) 最大为 1000,定义dp[2005][1005]
是可以的,但在状态转移时,下标应该在 0 到 ( F-1 ) 范围内。
但你的循环写的是for (int j = 1; j <= f; ++j)
这样就把下标范围写成了 1 到 ( F ),而下标 ( F ) 对应的余数其实应该是 0(因为 ( F \mod F = 0 )),这会导致混淆甚至可能出错。
3. 状态转移公式的问题
-
正确的状态转移应该是:
当考虑第 ( i ) 只奶牛(其水准指数经过取模后为 ( v[i] ))时,对于任意余数 ( r ) 应有:
[
dp[i][r] = dp[i-1][r] + dp[i-1][(r - v[i] + F) % F]
]
其中:- 第一项 ( dp[i-1][r] ) 表示不选第 ( i ) 只奶牛的方案数;
- 第二项 ( dp[i-1][(r - v[i] + F) % F] ) 表示选了第 ( i ) 只奶牛后,更新余数为 ( r ) 的方案数。
-
你写的代码中用了条件判断:
if(j >= v[i]) dp[i][j] += dp[i-1][j] + dp[i-1][(j-v[i])]; else dp[i][j] += dp[i-1][j];
这种写法只适用于普通的区间和问题,但对于模运算必须无条件地进行模的调整,也就是说,无论 ( j \ge v[i] ) 与否,都应使用公式:
dp[i][j] = dp[i-1][j] + dp[i-1][(j - v[i] + f) % f];
这样就可以保证下标不会出现负值,并且保持余数在 ( 0 ) 到 ( f-1 ) 范围内。
4. 初始状态和答案处理
- 你设定了:
表示“空集”对应的方案数为 1。dp[0][0] = 1;
此外在读入数据的循环中还写了:
这种处理可能导致后续转移时多次重复计入“空集”的方案。dp[i][0] = 1;
- 注意题目要求:
题目要求选择至少一只奶牛参加飞盘队(即不允许空集)。因此最后答案应为 ( dp[n][0] - 1 )(减去空集的方案),再取模。
5. 综合修改建议
下面给出一个修改后的参考代码,解决上述问题:
#include <iostream>
#include <cstring>
using namespace std;
const int MOD = 100000000; // 题目要求模 10^8
int v[2005];
int dp[2005][1005]; // dp[i][r] 表示前 i 只奶牛构成的子集,其和模 f 为 r 的方案数
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
memset(dp, 0, sizeof(dp)); // 初始化 dp 数组
int n, f;
cin >> n >> f;
for (int i = 1; i <= n; ++i) {
cin >> v[i];
v[i] %= f; // 只需要关心模 f 的值
}
// 初始状态:空集方案数为 1
dp[0][0] = 1;
// 状态转移
for (int i = 1; i <= n; ++i) {
for (int r = 0; r < f; ++r) {
// 不选第 i 只奶牛:状态保持不变
dp[i][r] = (dp[i][r] + dp[i-1][r]) % MOD;
// 选第 i 只奶牛:更新余数
int prev = (r - v[i] + f) % f;
dp[i][r] = (dp[i][r] + dp[i-1][prev]) % MOD;
}
}
// dp[n][0] 包含了空集的方案数,所以答案应为 dp[n][0] - 1
int ans = dp[n][0] - 1;
if (ans < 0) ans += MOD;
cout << ans << "\n";
return 0;
}
总结
- 数组初始化:用
memset(dp, 0, sizeof(dp));
替换mem(dp);
- 状态转移:直接使用公式
[
dp[i][r] = dp[i-1][r] + dp[i-1][(r - v[i] + f) % f]
]
保证所有下标均在合法范围内。 - 下标范围:循环余数时应在
0
到f-1
内,而不是1
到f
。 - 答案处理:最后输出时要减去空集的方案(即减 1)。
这样修改后,你的代码就可以正确解决题目要求了。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战