奶牛沙盘队(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

分析

  1. 确定状态dp[i][j]为前i头奶牛,得到飞盘准指数为j的方案数
  2. 确定边界,当0头奶牛获得0的方案数有1个,因此dp[0][0] == 1
  3. 状态转移````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. 初始状态和答案处理

  • 你设定了:
    dp[0][0] = 1;
    
    表示“空集”对应的方案数为 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;
}

总结

  1. 数组初始化:用 memset(dp, 0, sizeof(dp)); 替换 mem(dp);
  2. 状态转移:直接使用公式
    [
    dp[i][r] = dp[i-1][r] + dp[i-1][(r - v[i] + f) % f]
    ]
    保证所有下标均在合法范围内。
  3. 下标范围:循环余数时应在 0f-1 内,而不是 1f
  4. 答案处理:最后输出时要减去空集的方案(即减 1)。

这样修改后,你的代码就可以正确解决题目要求了。

posted @   bakul  阅读(1)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
点击右上角即可分享
微信分享提示