伴学笔记2

192 阅读5分钟

及格的组合方式探索

问题描述

小S在学校选择了3门必修课和n门选修课程来响应全面发展的教育政策。现在期末考核即将到来,小S想知道他所有课程的成绩有多少种组合方式能使他及格。及格的条件是所有课程的平均分不低于60分。每门课程的成绩是由20道选择题决定,每题5分,答对得分,答错不得分。为了计算方便,你需要将结果对202220222022取模。

测试样例

样例1:

输入:n = 3 输出:'19195617'

样例2:

输入:n = 6 输出:'135464411082'

样例3:

输入:n = 49 输出:'174899025576'

样例4:

输入:n = 201 输出:'34269227409'

样例5:

输入:n = 888 输出:'194187156114'

问题理解

题目要求我们求出所有的组合,每门课程都有21种可能,注意不要忽略0的组合, 然后就是暴力枚举所有组合,但是显然数据量太大,肯定会RTE,所以我们需要用动态规划来解决数据量爆炸的问题。

动态规划思路

dp 数组的角色

  1. old_dp

    这个数组用于存储当前已经考虑的课程组合的可能分数。 old_dp[i] 表示考虑了前 i 门课程后,得到分数为 i 的组合数量。 初始化时,old_dp[0] = 1 表示在0门课程的情况下,总分为0的组合数量为1(即什么都不选的情况)。

  2. new_dp

    这个数组用于计算当前门课程的得分组合,并将结果传递到下一次迭代。 每当我们添加一门新课程时,使用 new_dp 来存储添加这门课程后所有可能得分的组合数(从0到当前最大分数 MaxScore)。

状态转移的逻辑

在每一次外层循环之中,我们都会更新 new_dp

  • 遍历现有的总分
    从0到 MaxScore(当前课程可能达到的最大分数),我们会检查每一个分数 j

  • 遍历当前课程可能的分数
    对于当前课程,可能的分数 k 从0到20(即每门课程可以得0分到100分,实际上是以5分为单位的)。

  • 状态转移

    • 如果当前的总分为 j,我们想要通过当前课程得分 k 来更新:

      ini
       代码解读
      复制代码
      cpp
      new_dp[j] = (new_dp[j] + old_dp[j - k]) % MOD;
      
    • 这表示,如果当前总分为 j,并且我们选择当前课程得分为 k,那么之前的组合中总分为 j - k 的组合数量将会影响到当前组合数 new_dp[j]

条件判断

  • if (j - k >= 0 && j - k <= (i - 1) * 20) :

    • 这条判断确保我们只在合法的范围内更新 new_dp[j]
    • j - k >= 0 确保我们不会访问负索引,而 j - k <= (i - 1) * 20 确保我们只统计前 i-1 门课程的组合分数。
  • else if (j - k < 0) { break; } :

    • 如果 j - k 小于0,说明已经到达当前分数以下的范围,后面的 k 值将会更大,因此可以退出内层循环。

最终更新

  • 每完成一门课程的计算后,old_dp 被更新为 new_dp,这意味着在下一次外层循环中,old_dp 包含了当前所有课程(包括新加的课程)的分数组合。这样可以为下次迭代提供最新的状态基础。

完整源代码

cpp
 代码解读
复制代码
#include <iostream>
#include <vector>
using namespace std;

const long long MOD = 202220222022LL;

string solution(int n) {
    n += 3;
    vector<long long> old_dp(21, 0); // optimized space from a large array to only up to score 20
    old_dp[0] = 1;
    
    for (int i = 1; i <= n; ++i) {
        int MaxScore = 20 * i;
        vector<long long> new_dp(MaxScore + 1, 0);
        for (int j = 0; j <= MaxScore; ++j) {
            for (int k = 0; k <= 20; ++k) {
                if (j - k >= 0 && j - k <= (i - 1) * 20) {
                    new_dp[j] = (new_dp[j] + old_dp[j - k]) % MOD;
                } else if (j - k < 0) {
                    break;
                }
            }
        }
        old_dp = new_dp;
    }

    long long ans = 0;
    for (int i = 12 * n; i <= 20 * n; ++i) {
        ans = (ans + old_dp[i]) % MOD;
    }
    return to_string(ans);
}

int main() {
    // Test cases
    cout << (solution(3) == "19195617") << endl;
    cout << (solution(6) == "135464411082") << endl;
    cout << (solution(49) == "174899025576") << endl;
    cout << (solution(201) == "34269227409") << endl;
    cout << (solution(888) == "194187156114") << endl;
    return 0;
}

总结

  1. 数组越界问题

    • 在代码中,new_dp 的大小是 MaxScore + 1,但在内层循环中,j 的最大值是 MaxScore,而 k 的最大值是 20。因此,j - k 可能会小于 0,导致数组越界。
    • 你需要确保 j - k 不会小于 0。
  2. 模运算的正确性

    • 在代码中,你已经使用了模运算 % MOD,但需要确保在所有涉及模运算的地方都正确使用了 % MOD
  3. 初始化问题

    • old_dp 的初始化是 vector<long long> old_dp(21, 0);,并且 old_dp[0] = 1;。这表示初始状态下只有得分为 0 的情况是可能的。
    • 确保这个初始化是正确的,并且符合题意。
  4. 结果的范围

    • 在计算最终结果时,你从 12 * n 到 20 * n 遍历 old_dp,并累加结果。确保这个范围是正确的,并且符合题意。
  5. 测试用例的输出

    • 你在 main 函数中使用了 cout << (solution(3) == "19195617") << endl; 这样的测试用例。确保这些测试用例的输出是正确的,并且符合题意。