Easy - 题解
标签与难度
标签: 组合数学, 生成函数, 计数问题, 隔板法, 快速幂 难度: 2100
题目大意喵~
主人,你好呀~!这道题目是这样的喵:
我们要和 Mr. W 一起玩序列游戏!他会写下两个长度都为 的正整数序列 和 。
这两个序列需要满足两个条件:
- 序列 的所有元素之和为 ,也就是 。
- 序列 的所有元素之和为 ,也就是 。
对于每一对满足条件的序列 ,Mr. W 会得到一个分数 。
我们的任务是,计算出对于所有可能的序列对 ,他能获得的总分数是多少呢?因为答案可能很大,所以要对 取模哦,喵~
解题思路分析
这道题看起来有点吓人呢,又是求和又是求积的,直接去枚举所有可能的序列 和 肯定是不行的啦,会累坏的,喵~ (ΦωΦ)
当遇到这种复杂的计数问题,特别是混合了加法和乘法的时候,我的直觉告诉我,生成函数这个强大的魔法工具可能要登场啦!
构造生成函数
让我们先只考虑序列中的一个位置 。对于这一对数字 ,它们对分数的贡献是 。我们可以为这一对数字构造一个二维生成函数 ,其中 的指数对应 的值, 的指数对应 的值,而系数就是它们的得分 。
这个式子看起来有点复杂,我们来化简它。我们可以按照 的值 来分类讨论:
这个式子我好像写错了呢,应该是:
这里的第一项 对应 的情况。第二项对应 的情况。第三项对应 的情况。 把它们加起来,就是所有 的情况啦。
使用等比数列求和公式 (当 时),我们得到:
代入 的表达式中:
对于 这个级数,它等于 。所以,令 :
喵~ 看,我们得到了一个非常简洁漂亮的封闭形式!
求解总分数
我们有两个长度为 的序列,每个位置 都是独立的(除了总和的限制),所以总的生成函数就是 个 相乘:
我们要求的总分数,就是这个总生成函数 中 项的系数,记作 。
这等价于求下面这个表达式中 项的系数:
为了求这个系数,我们要用到广义二项式定理:
这是一个非常有用的公式,主人要记住哦!
现在我们把三个分式展开:
我们将这三个级数相乘,要凑出 项。这意味着我们需要找到所有满足 和 的非负整数 。
对于一个固定的 ,我们必须取 和 。 此时,对答案的贡献是这三项系数的乘积:
整理一下组合数:
再利用组合数的对称性 :
最后,我们对所有可能的 求和。 的取值范围是什么呢?因为 都必须是非负数,所以 且 。所以 的取值范围是 。
于是,我们得到了最终的求和公式!
实现细节
要实现这个公式,我们需要:
- 预处理阶乘和阶乘逆元:为了快速计算组合数 ,我们可以预先计算出一定范围内所有数的阶乘
fact[i]和阶乘的模逆元infact[i]。 - 快速幂:计算模逆元需要用到快速幂,根据费马小定理,(当 是质数时)。
- 循环求和:按照上面的公式,写一个循环从 到 ,累加每一项的值就可以啦。
好啦,思路清晰了,我要开始写代码啦!ฅ^•ﻌ•^ฅ
代码实现
#include <iostream>
#include <vector>
#include <algorithm>
// 使用 long long 防止中间计算溢出,喵~
using int64 = long long;
const int MOD = 998244353;
const int MAX_SIZE = 1000000 + 10; // 题目数据范围 N, M <= 10^6
// 预处理阶乘和阶乘的逆元
std::vector<int64> fact(MAX_SIZE);
std::vector<int64> inv_fact(MAX_SIZE);
// 快速幂函数,用来求模逆元
int64 power(int64 base, int64 exp) {
int64 res = 1;
base %= MOD;
while (exp > 0) {
if (exp % 2 == 1) {
res = (res * base) % MOD;
}
base = (base * base) % MOD;
exp /= 2;
}
return res;
}
// 求模逆元
int64 mod_inverse(int64 n) {
return power(n, MOD - 2);
}
// 预处理阶乘和逆阶乘的函数
void precompute_factorials() {
fact[0] = 1;
inv_fact[0] = 1;
for (int i = 1; i < MAX_SIZE; ++i) {
fact[i] = (fact[i - 1] * i) % MOD;
}
inv_fact[MAX_SIZE - 1] = mod_inverse(fact[MAX_SIZE - 1]);
for (int i = MAX_SIZE - 2; i >= 1; --i) {
inv_fact[i] = (inv_fact[i + 1] * (i + 1)) % MOD;
}
}
// 计算组合数的函数 C(n, k)
int64 combinations(int n, int k) {
if (k < 0 || k > n) {
return 0; // k 不合法,返回0种方案
}
return (((fact[n] * inv_fact[k]) % MOD) * inv_fact[n - k]) % MOD;
}
void solve() {
int n, m, k;
std::cin >> n >> m >> k;
// a_i 和 b_i 都是正整数,所以 N, M 必须至少是 K
if (n < k || m < k) {
std::cout << 0 << "\n";
return;
}
int64 total_score = 0;
int upper_bound = std::min(n - k, m - k);
// 循环变量 s 对应我们推导出的公式中的 s
for (int s = 0; s <= upper_bound; ++s) {
int64 term1 = combinations(k + s - 1, s);
int64 term2 = combinations(n - s - 1, k - 1);
int64 term3 = combinations(m - s - 1, k - 1);
int64 current_term = (term1 * term2) % MOD;
current_term = (current_term * term3) % MOD;
total_score = (total_score + current_term) % MOD;
}
std::cout << total_score << "\n";
}
int main() {
// 加速输入输出,让程序跑得更快,喵~
std::ios_base::sync_with_stdio(false);
std::cin.tie(NULL);
precompute_factorials();
int t;
std::cin >> t;
while (t--) {
solve();
}
return 0;
}复杂度分析
时间复杂度:
- precompute_factorials() 函数需要 的时间来计算阶乘和逆阶乘,其中 MAX_SIZE 是 的最大可能值。
- 对于每组测试数据,我们需要一个循环来计算总和。循环的次数是 ,所以这部分的复杂度是 。
- 因此,总的时间复杂度是预处理加上所有测试用例的计算时间。
空间复杂度:
- 我们需要两个数组
fact和inv_fact来存储预计算的结果,每个数组的大小都是MAX_SIZE。
- 我们需要两个数组
知识点总结
这道题虽然名字叫 "Easy",但其实一点也不 "Easy" 呢,哼哼~ 它考察了我们很多重要的知识点:
- 生成函数: 它是解决复杂组合计数问题的终极武器之一!通过将组合问题转化为代数问题(多项式/级数的运算),可以大大简化问题。
- 组合数学: 核心是组合数 的计算和其性质的应用,比如广义二项式定理。
- 模运算: 在算法竞赛中,当结果很大时,模运算是家常便饭。我们需要熟悉模加法、模乘法以及如何求模逆元。
- 费马小定理: 求模逆元的一个常用方法,当模数是质数时非常方便。
- 预处理/空间换时间: 通过预先计算阶乘和逆阶乘,我们将每次计算组合数的时间从 或 降低到了 ,这是处理多组查询的常用技巧。
希望这篇题解能帮助到你哦!如果还有不明白的地方,随时可以来问我,喵~ (づ。◕‿‿◕。)づ