AtCoder Beginner Contest 439 题解
AtCoder Beginner Contest 439 题解
| 项目 | 内容 |
|---|---|
| 比赛时间 | 2026-01-03 |
| 比赛链接 | https://atcoder.jp/contests/abc439 |
| 题目数量 | A–G 共 7 题 |
比赛概览
本场是 2026 新年第一场 ABC,多道题与数字 呼应(A 题在 时答案为 ,B 题样例为 ,C 题标题就是 )。整体难度分布均匀:A、B 为签到/入门题;C 需要"反过来枚举 "的 技巧;D 是"按比例分组 + 事件扫描容斥";E 是经典的"线段不交 LIS"转化;F 考察对"峰/谷"结构的洞察力并转化为组合计数,较有区分度;G 是全场最难的压轴题,需要掌握 Power Projection(幂投影)与形式幂级数(FPS),涉及 NTT 等大量前置知识。其中 F 题官方未提供题解,下面给出的是根据题意自行推导的解法。
A - 2^n - 2*n
题目链接:https://atcoder.jp/contests/abc439/tasks/abc439_a
题目大意
给定整数 (),输出 的值。
思路
这是纯粹的数值计算题,没有任何陷阱。直接计算 与 并相减即可。
计算 有若干种方式:
- 用
for循环乘 次; - 用快速幂;
- 直接用位运算
1 << N(因为 ); - 或用内置
pow(注意对整数用pow会返回double,可能产生精度问题,本题范围很小不会有问题,但推荐避免)。
由于 ,,用 int 就足够,不会溢出。容易验证 时 ,与标题呼应。
易错点:注意运算优先级,位运算 << 的优先级低于 - 与 *,因此要写成 (1 << n) - 2 * n。
参考代码
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n;
cout << (1 << n) - 2 * n << "\n";
return 0;
}官方题解还给出了 Python 写法(一行完成):
n = int(input())
print(2**n - 2*n)复杂度分析
- 时间复杂度:(位运算)或 (循环累乘),均远小于限制。
- 空间复杂度:。
B - Happy Number
题目链接:https://atcoder.jp/contests/abc439/tasks/abc439_b
题目大意
给定正整数 (),判断它是否为"快乐数":反复执行"把数替换为其十进制各位数字的平方和"这一操作,若经过有限次操作后能变成 ,则输出 Yes,否则输出 No。
思路
关键观察 1:设 ( 遍历 的十进制位)。若从 开始反复应用 过程中再次碰到一个已经出现过的数(且未到达 ),那么之后会永远在这个环上循环,不可能到达 。因此可以用一个 set 记录访问历史:
- 只要 ,就判断 是否访问过:若访问过则输出
No;否则记录并把 替换为 ; - 循环正常退出()则输出
Yes。
关键观察 2(迭代次数上界):因为 至多 4 位,所以 ;再应用一次则 。之后所有不超过 的数的函数值仍不超过 ( 不增:对任意 ,)。因此迭代值被"困"在 内,最多迭代约 次必然出现重复或到达 。所以上面的 set 判重写法必然快速终止。
易错点:经典快乐数序列 会进入不含 的环,必须正确识别为 No;而不要只循环固定次数就武断下结论(虽然固定迭代约 次再判断也可行,但判重写法更稳妥)。
参考代码
#include <bits/stdc++.h>
using namespace std;
int f(int n) {
int res = 0;
while (n) {
int d = n % 10;
res += d * d;
n /= 10;
}
return res;
}
int main() {
int n;
cin >> n;
set<int> seen;
while (n > 1) {
if (seen.count(n)) {
cout << "No\n";
return 0;
}
seen.insert(n);
n = f(n);
}
cout << "Yes\n";
return 0;
}复杂度分析
- 时间复杂度:迭代次数不超过约 ,每次求 至多 4 位,可视为 。
- 空间复杂度:(记录访问过的数不超过约 个)。
C - 2026
题目链接:https://atcoder.jp/contests/abc439/tasks/abc439_c
题目大意
正整数 称为好整数,当且仅当恰好存在一对整数 满足 且 。给定 (),输出所有不超过 的好整数:第一行为个数 ,第二行为升序排列的 个数(若 ,第二行为空行)。
思路
先想朴素的错误做法:对每个 ()枚举 (),计算 判断是否为整数且 ,统计满足条件的 对数是否为 1。这样做的时间为 ,对 完全不可行。
关键转化:反过来枚举 。既然 ,那么 。不妨直接枚举所有满足 且 的整数对 ,用一个计数数组 记录每个 被覆盖的次数:
最后满足 的 就是好整数(恰好被一对 表示)。
为什么复杂度是 :满足 的格点 ()数量大约是第一象限内半径 的四分之一圆面积,即 。枚举时以 为外层循环、 为内层循环,并且在内层当 时及时 break。
边界与易错点:
- 要求 ,所以 从 开始,恰好覆盖每一对无序对一次(不会重复计数,也不会把 与 算两次)。
- 题目要求"恰好一对",所以输出 的 ,而不是 。
- 输出格式: 时第二行输出空行(上面代码在
ans为空时只输出第一行的0,符合要求)。
参考代码
#include <bits/stdc++.h>
using namespace std;
int main() {
int N;
cin >> N;
vector<int> c(N + 1, 0);
for (int x = 1; x * x <= N; x++) {
for (int y = x + 1; x * x + y * y <= N; y++) {
c[x * x + y * y]++;
}
}
vector<int> ans;
for (int n = 1; n <= N; n++) {
if (c[n] == 1) ans.push_back(n);
}
cout << ans.size() << "\n";
for (int i = 0; i < (int)ans.size(); i++) {
cout << ans[i] << (i + 1 == (int)ans.size() ? "\n" : " ");
}
return 0;
}复杂度分析
- 时间复杂度:(枚举的 对数约为 )。
- 空间复杂度:(计数数组)。
D - Kadomatsu Subsequence
题目链接:https://atcoder.jp/contests/abc439/tasks/abc439_d
题目大意
给定长度 ()的整数序列 。求满足以下条件的三元组 的个数:
- ;
- ;
- 或 。
思路
关键观察 1:按比例分组。因为 中元素都是正整数, 等价于存在正整数 使得
由于 ,三个元素值两两不同,因此 三个下标也必然两两不同。对每个 ,分别收集值为 、、 的下标集合 。忽略下标条件时,贡献为
关键观察 2:处理下标条件。条件" 或 "意味着 是三者的下标中最小或最大,也就是 不可能是三者的中间下标。由于 两两不同,被排除的只是 与 两类。
于是对每个 :先按全部计数 ,再减去" 在中间"的个数。
如何统计" 在中间":把 中的下标合并后按下标升序扫描,同时维护:
- :已经扫描过的 下标个数;:尚未扫描的 下标个数;
- :已经扫描过的 下标个数;:尚未扫描的 下标个数。
当遇到一个 下标 时:
- 的情形数为 ( 是已扫过的 下标、 是未扫过的 下标);
- 的情形数为 。
把这两者从总数中减去即可。
正确性:每个" 在中间"的三元组恰好在扫描到该 时被减去一次;不在中间的不会被减。对所有 求和即为答案。
易错点:
- 答案上限约为 ,超出 32 位整数范围,必须用 64 位整数(
long long)。 - 某 只出现在 而不在 时贡献为 0,可以直接跳过;用
map按 分组即可。
参考代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
// 对单个 t 计算:总数减去 "j 在下标中间" 的个数
ll solve_group(vector<ll>& v7, vector<ll>& v5, vector<ll>& v3) {
ll res = (ll)v7.size() * v5.size() * v3.size();
if (res == 0) return 0;
vector<pair<ll, int>> vp; // (下标, 类型):7 / 5 / 3
for (auto x : v7) vp.push_back({x, 7});
for (auto x : v5) vp.push_back({x, 5});
for (auto x : v3) vp.push_back({x, 3});
sort(vp.begin(), vp.end());
ll p7 = 0, s7 = v7.size();
ll p3 = 0, s3 = v3.size();
for (auto& pr : vp) {
if (pr.second == 7) { p7++; s7--; }
else if (pr.second == 5) {
res -= p7 * s3; // i < j < k
res -= p3 * s7; // k < j < i
} else { p3++; s3--; }
}
return res;
}
int main() {
int n;
cin >> n;
map<pair<ll, int>, vector<ll>> mp;
set<ll> tst;
for (ll i = 0; i < n; i++) {
ll a;
cin >> a;
if (a % 7 == 0) { mp[{a / 7, 7}].push_back(i); tst.insert(a / 7); }
if (a % 5 == 0) mp[{a / 5, 5}].push_back(i);
if (a % 3 == 0) mp[{a / 3, 3}].push_back(i);
}
ll ans = 0;
for (auto t : tst) {
ans += solve_group(mp[{t, 7}], mp[{t, 5}], mp[{t, 3}]);
}
cout << ans << "\n";
return 0;
}复杂度分析
- 时间复杂度:(按 分组用
map,每组内排序扫描)。 - 空间复杂度:。
E - Kite
题目链接:https://atcoder.jp/contests/abc439/tasks/abc439_e
题目大意
()个人站在河边,第 个人站在 ,风筝在 。若两条""线段相交(含端点相触),则两人不能同时放风筝。求能同时放风筝的最大人数。
思路
关键观察 1:两条线段何时相交? 线段 在高度 处的横坐标为 (线性插值)。两条线段相交当且仅当存在 使得
即 与 异号(或其一为 0)。因此两人可以同时放风筝当且仅当
即点对 与 严格同序。这也对应线段整体上"一条始终在另一条上方"而不交叉。
关键观察 2:转化为 LIS。把所有人按 升序排列,问题变成:选取尽可能多的点,使得任意两点的 也严格递增——这正是对 序列求最长上升子序列(LIS)。
关于 相同的平局处理:若两人 相等,则起点相同,线段在端点 处相触,属于"不能同时"(题目明确包含端点相触)。在排序时按 升序、 相同时按 降序 排列,这样 相同的点在 序列里是严格递减的,LIS(严格递增)不可能同时选到它们,从而自动规避了冲突。
LIS 的 求法:维护数组 dp,dp[i] 表示长度为 的上升子序列的最小末尾值;对每个 ,用 lower_bound 找到第一个不小于它的位置并替换,dp 的长度即为 LIS 长度。
易错点:平局排序方向必须与 LIS 的方向配合——由于我们要求严格递增, 相同必须降序排 ;若误升序排则会错误地允许 相同的两人同时入选。
参考代码
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int N;
cin >> N;
vector<pair<int, int>> AB(N);
for (auto& [a, b] : AB) cin >> a >> b;
// A 升序;A 相同时 B 降序(防止同 A 的两点同时入选)
sort(AB.begin(), AB.end(), [](auto& l, auto& r) {
if (l.first == r.first) return l.second > r.second;
return l.first < r.first;
});
vector<int> dp;
for (auto& [a, b] : AB) {
int i = lower_bound(dp.begin(), dp.end(), b) - dp.begin();
if (i == (int)dp.size()) dp.push_back(b);
else dp[i] = b;
}
cout << dp.size() << "\n";
return 0;
}复杂度分析
- 时间复杂度:(排序 + 二分)。
- 空间复杂度:。
F - Beautiful Kadomatsu
题目链接:https://atcoder.jp/contests/abc439/tasks/abc439_f
(本题暂无官方题解,以下为根据题意推导的解法。)
题目大意
长度为 的序列 称为 kadomatsu-like,当且仅当其"峰数"严格大于"谷数",其中:
- 峰:满足 且 的 ()的个数,记为 ;
- 谷:满足 且 的 的个数,记为 ;
要求 。给定 的一个排列 (),求 的子序列(不必连续)中是 kadomatsu-like 的个数,对 取模。
思路
Step 1:把"峰多于谷"转化为"首升尾降"。
设子序列相邻元素的差符号序列为 ()。于是:
- 峰 = 符号对 的出现次数(先升后降);
- 谷 = 符号对 的出现次数。
把符号序列中连续的相同符号合并成一段,得到一个严格交替的符号段序列。任意两个相邻符号段之间恰好发生一次 或 的转变,且转变类型必然交替出现()。因此 与 之差至多为 1。具体地:
- 符号段首为 、尾为 转变序列为 ,故 ;
- 首为 、尾为 ;
- 首为 、尾为 ;
- 首为 、尾为 。
所以
即一个子序列是 kadomatsu-like 当且仅当它的第一步上升、最后一步下降。注意长度为 1 或 2 的子序列不可能同时满足这两个条件( 时要求 又要求 ,矛盾),自动被排除。
Step 2:组合计数。
合法子序列由三部分组成:
- 上升边:首两个元素 ,满足 且 ;
- 中间部分:下标严格介于 与 之间的任意子集(可为空);
- 下降边:末两个元素 ,满足 且 。
上升边、下降边、中间子集三者唯一决定一个合法子序列,且无重复计数。
固定上升边 与下降边 (需 ),中间有 个位置可以任意选入子序列,方案数为 (当 时中间为空,方案数为 1)。因此
Step 3:用前缀/后缀信息 计算。
定义(以下下标按 0 起始):
把和式按 与 的关系拆分:
- 的情形:贡献 ;
- 的情形:上升边 有 条、下降边 有 条,中间权重 ,故贡献
综合得到
可用树状数组从左到右扫描求出(查询小于 的前缀元素个数); 用树状数组从右到左扫描求出; 是 的后缀和,直接 递推。所有运算在模 下进行, 用 2 的逆元()的幂次表示。
正确性小结:Step 1 证明了"峰>谷"与"首升尾降"等价;Step 2 把计数分解为不重复的(上升边, 中间子集, 下降边)三元组;Step 3 用树状数组与后缀和把暴力枚举压缩到 。
易错点:指数 在 时对应的是"空中间",应取权重 1,因此用 单独求和、 时用 的拆分方式( 与 的乘积)避免出现 。实测对全部样例与随机暴力对拍均一致。
参考代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll MOD = 998244353;
struct BIT {
int n;
vector<int> bit;
BIT(int n) : n(n), bit(n + 1, 0) {}
void add(int i, int v) { for (; i <= n; i += i & -i) bit[i] += v; }
int sum(int i) { int s = 0; for (; i > 0; i -= i & -i) s += bit[i]; return s; }
};
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int N;
cin >> N;
vector<int> P(N);
for (int& x : P) cin >> x;
ll inv2 = (MOD + 1) / 2;
vector<ll> pow2(N + 2), inv2p(N + 2);
pow2[0] = inv2p[0] = 1;
for (int i = 1; i <= N + 1; i++) {
pow2[i] = pow2[i - 1] * 2 % MOD;
inv2p[i] = inv2p[i - 1] * inv2 % MOD;
}
vector<ll> U(N), D(N);
{ // U[b] = #{a < b : P[a] < P[b]}
BIT bit(N);
for (int b = 0; b < N; b++) {
U[b] = bit.sum(P[b] - 1);
bit.add(P[b], 1);
}
}
{ // D[c] = #{d > c : P[c] > P[d]}
BIT bit(N);
for (int c = N - 1; c >= 0; c--) {
D[c] = bit.sum(P[c] - 1);
bit.add(P[c], 1);
}
}
// suff[b] = sum_{c > b} D[c] * 2^c
vector<ll> suff(N + 1, 0);
for (int c = N - 1; c >= 0; c--) {
suff[c] = (suff[c + 1] + D[c] * pow2[c]) % MOD;
}
ll term1 = 0, term2 = 0;
for (int b = 0; b < N; b++) {
term1 = (term1 + U[b] * D[b]) % MOD; // c = b
term2 = (term2 + U[b] * inv2p[b + 1] % MOD * suff[b + 1]) % MOD; // c > b
}
cout << (term1 + term2) % MOD << "\n";
return 0;
}复杂度分析
- 时间复杂度:(两个树状数组)。
- 空间复杂度:。
G - Sugoroku 6
题目链接:https://atcoder.jp/contests/abc439/tasks/abc439_g
题目大意
有编号 的 个格子()和一个 面骰子,各面为两两不同的正整数 (),掷出各面的概率均为 。()个人的棋子最初都在格子 ,按 轮流掷骰:掷出 时棋子从 移到 。第一个到达格子 的人获胜。对每个人求其获胜概率对 取模后的值。
思路
这是本场最难的题,需要 Power Projection(幂投影) 与形式幂级数(FPS)技术。整体分三步:先求单个棋子的相关概率,再套用"第 人在第 轮获胜"的概率公式,最后用分式合并技巧一次性求出前 个人的答案。
Step 1:单个棋子的概率生成函数。
定义:
- :单个棋子操作 次后仍未到达格子 的概率;
- :单个棋子恰好第 次操作到达格子 的概率。
由于棋子位置单调不减,到达 后不会再变,因此
(每次至少前进 1 格, 次内必然到达 。)
令 为一次掷骰子前进格数的概率生成函数:
那么掷 次后棋子位于格子 的概率为 (前进格数之和恰好为 且 时未到达终点;由于 已被封顶, 正是"未到达")。于是
记 、。我们需要的正是
即形如 的一族值——恰好是 Power Projection 的标准形式。
Step 2:Power Projection。
Power Projection 问题:给定次数为 的多项式 ,求 ()。利用
正是双变量形式幂级数 中 的系数。当 时(本题 无常数项,满足),可以用 Bostan–Mori 算法对 的次数做对半递推,同时 的次数翻倍:
其中 只含 的偶次项,于是 , 的次数每次减半。每轮 次数 、 次数 ,用分块填充把双变量乘法变成一维卷积 + NTT,每轮 ,共 轮,总复杂度 。最后 次数变为 0,返回 的系数即可。
Step 3:把 组装成每个人的获胜概率。
第 个人在自己的第 次掷骰时获胜,要求:
- 前 个人在各自的前 次掷骰内均未到达 :概率 ;
- 后 个人在各自的前 次掷骰内均未到达 :概率 ;
- 第 个人恰好第 次到达:概率 。
三件事互相独立,故第 人的胜率为
对 :,可直接 求和
对 :若 ,该项为 可忽略;否则令 ,则
于是
这是 个"一次分式"的和。用分治/线段树合并:每个分式是 ,两两合并
所有多项式只保留前 个系数(模 ),合并过程总代价 。最终得到 ,用多项式求逆求出 后与 相乘, 即 。总复杂度 。
易错点与边界:
- 所有概率均在模 下计算,掷骰概率为 。
- 的面在 中不会贡献(涉及它的项次数至少 ),可安全地忽略,代码中
if (a < N)即为此。 - 第 项:对 有 ;对 需要保留 项(其 ,恰为常数项 ),因此分式合并需要遍历 而非 ; 的求和里 项为 ,遍历 即可。
- 该解法的参考实现是"教学级"的(为便于理解未做极限常数优化);在正式评测环境下可用 AtCoder Library(ACL)的
convolution/formal_power_series提速。
参考代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int MOD = 998244353;
const int ROOT = 3;
int modpow(int a, ll e) {
ll r = 1, base = a;
while (e) {
if (e & 1) r = r * base % MOD;
base = base * base % MOD;
e >>= 1;
}
return (int)r;
}
void ntt(vector<int>& a, bool invert) {
int n = (int)a.size();
for (int i = 1, j = 0; i < n; i++) {
int bit = n >> 1;
for (; j & bit; bit >>= 1) j ^= bit;
j ^= bit;
if (i < j) swap(a[i], a[j]);
}
for (int len = 2; len <= n; len <<= 1) {
int wlen = modpow(ROOT, (MOD - 1) / len);
if (invert) wlen = modpow(wlen, MOD - 2);
for (int i = 0; i < n; i += len) {
ll w = 1;
for (int j = 0; j < len / 2; j++) {
int u = a[i + j];
int v = (int)((ll)a[i + j + len / 2] * w % MOD);
int x = u + v; if (x >= MOD) x -= MOD;
int y = u - v; if (y < 0) y += MOD;
a[i + j] = x;
a[i + j + len / 2] = y;
w = w * wlen % MOD;
}
}
}
if (invert) {
int inv_n = modpow(n, MOD - 2);
for (int& x : a) x = (int)((ll)x * inv_n % MOD);
}
}
vector<int> convolution(const vector<int>& a, const vector<int>& b) {
if (a.empty() || b.empty()) return {};
int need = (int)a.size() + (int)b.size() - 1;
if (min((int)a.size(), (int)b.size()) <= 60) { // 小规模直接暴力
vector<int> res(need);
for (int i = 0; i < (int)a.size(); i++)
for (int j = 0; j < (int)b.size(); j++)
res[i + j] = (res[i + j] + (ll)a[i] * b[j]) % MOD;
return res;
}
int n = 1;
while (n < need) n <<= 1;
vector<int> fa(a.begin(), a.end()), fb(b.begin(), b.end());
fa.resize(n); fb.resize(n);
ntt(fa, false); ntt(fb, false);
for (int i = 0; i < n; i++) fa[i] = (ll)fa[i] * fb[i] % MOD;
ntt(fa, true);
fa.resize(need);
return fa;
}
// 多项式求逆:a[0] != 0,求 a 的逆模 x^n
vector<int> fps_inv(const vector<int>& a, int n) {
vector<int> r(1, modpow(a[0], MOD - 2));
int m = 1;
while (m < n) {
m <<= 1;
vector<int> f(a.begin(), a.begin() + min((int)a.size(), m));
f.resize(m);
auto t = convolution(f, r);
t.resize(m);
for (int& x : t) x = x ? MOD - x : 0;
t[0] += 2; if (t[0] >= MOD) t[0] -= MOD;
r = convolution(r, t);
r.resize(m);
}
r.resize(n);
return r;
}
int npow2(int x) {
int r = 1;
while (r < x) r <<= 1;
return r;
}
// 幂投影:返回 pp[i] = [x^{n}] f(x)^i g(x)(i = 0..n,n = f.size()-1,要求 [x^0] f = 0)
vector<int> power_projection(const vector<int>& f, const vector<int>& g) {
int n = (int)f.size() - 1;
int n0 = n;
int k = 1; // 当前 y 的次数
int K = npow2(2 * k + 1); // 分块大小,避免 y 方向进位
vector<int> P((n + 1) * K, 0), Q((n + 1) * K, 0);
for (int i = 0; i <= n; i++) {
P[i * K] = g[i];
Q[i * K + 1] = f[i] ? MOD - f[i] : 0;
}
Q[0] = 1; // Q(x,y) = 1 - y f(x)
while (n > 0) {
vector<int> R = Q; // R(x,y) = Q(-x,y)
for (int i = 1; i <= n; i += 2)
for (int j = 0; j <= k; j++) {
int& x = R[i * K + j];
x = x ? MOD - x : 0;
}
int len = (n + 1) * K;
int need = 2 * len - 1;
int sz = 1;
while (sz < need) sz <<= 1;
vector<int> a(P.begin(), P.begin() + len);
vector<int> b(R.begin(), R.begin() + len);
vector<int> q(Q.begin(), Q.begin() + len);
a.resize(sz); b.resize(sz); q.resize(sz);
ntt(a, false); ntt(b, false); ntt(q, false);
for (int i = 0; i < sz; i++) {
a[i] = (ll)a[i] * b[i] % MOD; // S = P * R
q[i] = (ll)q[i] * b[i] % MOD; // T = Q * R
}
ntt(a, true); ntt(q, true);
a.resize(need); q.resize(need);
int n2 = n / 2, k2 = 2 * k;
int K2 = npow2(2 * k2 + 1);
vector<int> U((n2 + 1) * K2, 0), V((n2 + 1) * K2, 0);
int parity = n & 1;
for (int i = 0; i <= n2; i++) {
int rowS = 2 * i + parity;
int rowT = 2 * i;
for (int j = 0; j <= k2; j++) {
U[i * K2 + j] = a[rowS * K + j];
V[i * K2 + j] = q[rowT * K + j];
}
}
P = move(U);
Q = move(V);
n = n2;
k = k2;
K = K2;
}
vector<int> num(k + 1), den(k + 1);
for (int j = 0; j <= k; j++) {
num[j] = P[j];
den[j] = Q[j];
}
auto den_inv = fps_inv(den, n0 + 1);
auto res = convolution(num, den_inv);
res.resize(n0 + 1);
return res;
}
// 合并两个一次分式:(n1,d1) + (n2,d2) = (n1*d2 + n2*d1, d1*d2),只保留前 T 个系数
pair<vector<int>, vector<int>> merge_frac(const vector<int>& n1, const vector<int>& d1,
const vector<int>& n2, const vector<int>& d2, int T) {
auto a = convolution(n1, d2);
auto b = convolution(n2, d1);
vector<int> num(T, 0), den(T, 0);
for (int i = 0; i < T; i++) {
int x = i < (int)a.size() ? a[i] : 0;
int y = i < (int)b.size() ? b[i] : 0;
int s = x + y; if (s >= MOD) s -= MOD;
num[i] = s;
}
auto d = convolution(d1, d2);
for (int i = 0; i < T; i++) den[i] = i < (int)d.size() ? d[i] : 0;
return {num, den};
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int N, M, L;
cin >> N >> M >> L;
vector<int> A(M);
for (auto& x : A) cin >> x;
int invM = modpow(M, MOD - 2);
vector<int> D(N, 0); // D(x) = (1/M) sum x^{A_i},次数 0..N-1
for (auto a : A) {
if (a < N) { // 次数 >= N 的项不影响 [x^{N-1}]
D[a] += invM; if (D[a] >= MOD) D[a] -= MOD;
}
}
vector<int> G(N, 1); // 1 + x + ... + x^{N-1}
auto f = power_projection(D, G); // f[i] = f_i(i = 0..N-1)
f.resize(N + 1);
f[N] = 0; // f_N = 0
vector<int> g(N + 1); // g[k] = f[k-1] - f[k],k = 1..N
for (int k = 1; k <= N - 1; k++) {
int x = f[k - 1] - f[k]; if (x < 0) x += MOD;
g[k] = x;
}
g[N] = f[N - 1];
// 前 L-1 个人:分式之和 S(y) = sum_k (g_k f_{k-1}^{L-1}) / (1 - r_k y)
int T = L;
vector<pair<vector<int>, vector<int>>> fracs;
for (int k = 1; k <= N; k++) {
if (f[k - 1] == 0) continue;
int fkm1 = f[k - 1];
int num0 = (ll)g[k] * modpow(fkm1, L - 1) % MOD;
int rk = (ll)f[k] * modpow(fkm1, MOD - 2) % MOD;
vector<int> num(1, num0);
vector<int> den = {1, rk ? MOD - rk : 0};
fracs.push_back({num, den});
}
while (fracs.size() > 1) { // 分治合并所有分式
vector<pair<vector<int>, vector<int>>> nxt;
for (int i = 0; i + 1 < (int)fracs.size(); i += 2) {
nxt.push_back(merge_frac(fracs[i].first, fracs[i].second,
fracs[i + 1].first, fracs[i + 1].second, T));
}
if (fracs.size() % 2 == 1) nxt.push_back(fracs.back());
fracs = move(nxt);
}
vector<int> ans(L + 1, 0);
if (!fracs.empty()) {
auto& num = fracs[0].first;
auto& den = fracs[0].second;
auto den_inv = fps_inv(den, T);
auto coeff = convolution(num, den_inv);
for (int i = 1; i <= L - 1; i++) {
if (i - 1 < (int)coeff.size()) ans[i] = coeff[i - 1];
}
}
// 第 L 个人:ans[L] = sum_k g_k f_k^{L-1}(k=N 的项为 0,不必枚举)
int sumL = 0;
for (int k = 1; k <= N - 1; k++) {
sumL = (sumL + (ll)g[k] * modpow(f[k], L - 1)) % MOD;
}
ans[L] = sumL;
for (int i = 1; i <= L; i++) cout << ans[i] << "\n";
return 0;
}复杂度分析
- 时间复杂度:(Power Projection 每轮 共 轮;分式合并 ;末尾多项式求逆与乘法 )。
- 空间复杂度:。