题目描述
小S在学校选择了3门必修课和n门选修课程,以响应全面发展的教育政策。期末考核即将到来,小S希望了解所有课程成绩的组合方式有多少种能够使他的平均成绩及格。具体来说,及格的条件是所有课程的平均分不低于60分。每门课程的成绩由20道选择题决定,每题答对得5分,答错不得分。为了计算方便,最终结果需要对202220222022取模。
解题思路
本题的核心在于计算小S在所有课程中的成绩组合方式,使得其平均分不低于60分。具体分析如下:
-
成绩与及格条件的转换:
- 每门课程有20道选择题,每题5分,因此每门课程的最高得分为100分(20题 × 5分)。
- 平均分不低于60分意味着总分不低于60分 × 总课程数。
- 设总课程数为
totalTests = n + 3,则总分不低于60 × totalTests分。 - 因为每题5分,所以总分可以转换为总正确答案数不低于
12 × totalTests(因为60分 ÷ 5分 = 12)。
-
动态规划的应用:
- 定义状态
dp[i][j]表示前i门课程中,累计正确答案数为j的组合方式数。 - 初始状态:对于第一门课程,正确答案数可以从0到20,每种情况下的组合方式数都是1,即
dp[1][j] = 1,其中0 ≤ j ≤ 20。 - 转移方程:对于第
testCnt门课程,累计正确答案数为totalCorrect,可以从前testCnt - 1门课程中累计正确答案数为totalCorrect - thisCorrect的状态转移过来,其中thisCorrect表示当前课程的正确答案数,范围为0到20。 - 最终目标是计算
dp[totalTests][j],其中j从12 × totalTests到20 × totalTests的所有情况的总和。
- 定义状态
-
优化与模运算:
- 由于组合方式数可能非常大,需要在每一步进行模运算,以避免整数溢出,并保证最终结果在规定范围内。
- 使用长整型(
long)来存储中间结果,确保计算过程中数值的准确性。
代码实现
以下是题目给出的正确代码实现:
public class Main {
public static String solution(int n) {
// 总课程数,包括3门必修课和n门选修课
int totalTests = n + 3;
// 每门课程的题目数
int problemsEachTest = 20;
// 所有课程的总题目数
int totalProblems = totalTests * problemsEachTest;
// dp[i][j] 表示前i门课程累计j个正确答案的组合方式数
long[][] dp = new long[totalTests + 1][totalProblems + 1];
// 初始化第一门课程的状态
for (int i = 0; i <= problemsEachTest; i++) {
dp[1][i] = 1;
}
// 动态规划计算所有课程的组合方式数
for (int testCnt = 2; testCnt <= totalTests; testCnt++) {
for (int totalCorrect = 0; totalCorrect <= totalProblems; totalCorrect++) {
long s = 0;
// 当前课程可能的正确答案数
for (int thisCorrect = 0; thisCorrect <= problemsEachTest; thisCorrect++) {
if (0 <= totalCorrect - thisCorrect && totalCorrect - thisCorrect <= totalProblems) {
s = (s + dp[testCnt - 1][totalCorrect - thisCorrect]) % 202220222022L;
}
}
dp[testCnt][totalCorrect] = s;
}
}
// 计算总分不低于及格线的所有组合方式数
long ret = 0;
// 及格所需的最少正确答案数
int leastCorrect = totalTests * 12;
for (int i = leastCorrect; i <= totalProblems; i++) {
ret = (ret + dp[totalTests][i]) % 202220222022L;
}
return String.valueOf(ret);
}
public static void main(String[] args) {
// 测试用例
System.out.println(solution(3).equals("19195617"));
System.out.println(solution(6).equals("135464411082"));
System.out.println(solution(49).equals("174899025576"));
System.out.println(solution(201).equals("34269227409"));
System.out.println(solution(888).equals("194187156114"));
}
}
代码解析
-
变量初始化:
totalTests表示总课程数,即n + 3。problemsEachTest固定为20,表示每门课程的题目数。totalProblems为所有课程的总题目数,即totalTests × 20。
-
动态规划数组
dp的定义与初始化:dp[i][j]表示前i门课程中,累计正确答案数为j的组合方式数。- 对于第一门课程,正确答案数可以从0到20,每种情况下的组合方式数均为1,即
dp[1][j] = 1。
-
动态规划状态转移:
- 外层循环遍历课程数
testCnt从2到totalTests。 - 中层循环遍历累计正确答案数
totalCorrect从0到totalProblems。 - 内层循环遍历当前课程可能的正确答案数
thisCorrect从0到20。 - 对于每一个
thisCorrect,如果totalCorrect - thisCorrect在合法范围内,则将dp[testCnt - 1][totalCorrect - thisCorrect]累加到当前状态dp[testCnt][totalCorrect]中,并对模数进行取余操作。
- 外层循环遍历课程数
-
最终结果的计算:
- 及格所需的最少正确答案数为
12 × totalTests。 - 遍历
dp[totalTests][i],其中i从leastCorrect到totalProblems,累加所有满足及格条件的组合方式数,并对模数进行取余。
- 及格所需的最少正确答案数为
-
主函数中的测试用例:
- 通过调用
solution函数并与预期结果进行比较,验证代码的正确性。
- 通过调用
时间复杂度与空间复杂度
-
时间复杂度:
-
动态规划的主要部分包括三层嵌套循环:
- 第一层循环遍历课程数
testCnt,范围为2到totalTests,共O(totalTests)次。 - 第二层循环遍历累计正确答案数
totalCorrect,范围为0到totalProblems,共O(totalProblems)次。 - 第三层循环遍历当前课程的正确答案数
thisCorrect,范围为0到20,共O(1)次,因为20是一个常数。
- 第一层循环遍历课程数
-
因此,总的时间复杂度为
O(totalTests × totalProblems)。 -
由于
totalTests = n + 3,totalProblems = (n + 3) × 20,时间复杂度可以表示为O(n^2),其中n为选修课程数。
-
-
空间复杂度:
- 动态规划数组
dp的大小为(totalTests + 1) × (totalProblems + 1)。 - 因此,空间复杂度为
O(totalTests × totalProblems),即O(n^2)。
- 动态规划数组
专业术语解析
- 动态规划(Dynamic Programming, DP) :一种将复杂问题分解为更小子问题的方法,通过存储子问题的结果来避免重复计算,从而提高效率。在本题中,通过构建
dp数组记录前i门课程累计j个正确答案的组合方式数,逐步构建最终结果。 - 状态转移方程(State Transition Equation) :描述如何从一个状态转移到另一个状态的数学表达式。在本题中,
dp[testCnt][totalCorrect] = dp[testCnt - 1][totalCorrect - thisCorrect],即当前状态由前一状态转移而来。 - 模运算(Modular Arithmetic) :对数值进行取模运算,以限制数值范围,防止溢出。在本题中,所有计算结果都对
202220222022取模,以确保结果在规定范围内。 - 时间复杂度与空间复杂度(Time and Space Complexity) :衡量算法在运行时间和所需空间上的效率指标。了解这些复杂度有助于评估算法在处理大规模数据时的表现。
结论
通过动态规划的方法,本文有效地解决了小S课程成绩组合方式的计数问题。通过合理地定义状态和转移方程,并结合模运算优化计算过程,成功地在可接受的时间和空间复杂度内得出了正确答案。这一方法不仅适用于本题,也具有广泛的应用前景,在类似的组合计数问题中具有重要的参考价值。