及格的组合方式探索
问题描述
小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 数组的角色
-
old_dp:这个数组用于存储当前已经考虑的课程组合的可能分数。
old_dp[i]表示考虑了前i门课程后,得到分数为i的组合数量。 初始化时,old_dp[0] = 1表示在0门课程的情况下,总分为0的组合数量为1(即什么都不选的情况)。 -
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;
}
总结
-
数组越界问题:
- 在代码中,
new_dp的大小是MaxScore + 1,但在内层循环中,j的最大值是MaxScore,而k的最大值是 20。因此,j - k可能会小于 0,导致数组越界。 - 你需要确保
j - k不会小于 0。
- 在代码中,
-
模运算的正确性:
- 在代码中,你已经使用了模运算
% MOD,但需要确保在所有涉及模运算的地方都正确使用了% MOD。
- 在代码中,你已经使用了模运算
-
初始化问题:
old_dp的初始化是vector<long long> old_dp(21, 0);,并且old_dp[0] = 1;。这表示初始状态下只有得分为 0 的情况是可能的。- 确保这个初始化是正确的,并且符合题意。
-
结果的范围:
- 在计算最终结果时,你从
12 * n到20 * n遍历old_dp,并累加结果。确保这个范围是正确的,并且符合题意。
- 在计算最终结果时,你从
-
测试用例的输出:
- 你在
main函数中使用了cout << (solution(3) == "19195617") << endl;这样的测试用例。确保这些测试用例的输出是正确的,并且符合题意。
- 你在