AtCoder Beginner Contest 422 题解
AtCoder Beginner Contest 422 题解
| 项目 | 内容 |
|---|---|
| 比赛时间 | 2025-09-07 |
| 比赛链接 | https://atcoder.jp/contests/abc422 |
| 题目数量 | A–G 共 7 题 |
比赛概览
本场整体难度中等偏上,A–D 比较基础(其中 D 需要一点逆向思维与不变量论证),E–G 具有较强区分度。涉及的主要知识点包括:格式化输入与条件分支(A)、网格模拟与"哨兵"技巧(B)、贪心与构造性上界论证(C)、逆向构造与归纳证明(D)、随机化几何算法(E)、分层图上的 DP 建模(F)、以及生成函数与 NTT 卷积(G)。其中 E(随机化)、F(图上的费用建模)、G(生成函数)三题尤其值得仔细掌握。
A - Stage Clear
题目链接:https://atcoder.jp/contests/abc422/tasks/abc422_a
题目大意
游戏共有 个世界、每个世界 个关卡,关卡按顺序排列:第 世界第 关()的下一关是同世界第 关;第 世界第 关()的下一关是第 世界第 关;8-8 是最后一关。给定当前关卡名 (保证不是 8-8),输出下一关的关卡名。
思路
把关卡名 拆成世界编号 和关卡编号 两个整数即可:
- 若 ,下一关是 ;
- 若 ,说明这是当前世界的最后一关,下一关是 。
解析字符串有两种常见做法。一是直接操作字符串:读入整个字符串 ,若最后一个字符是 '8',则把第一个字符加 、最后一个字符改成 '1';否则把最后一个字符加 。二是利用 C 的 scanf 格式化输入:scanf("%d-%d", &a, &b) 会自动跳过中间的 -,直接得到两个整数,更简洁。
边界情况:题目保证 不是 8-8,所以" 且 "的情况不会出现,无需额外处理;但我们仍用 if (j == 9)(或 j > 8)的形式来处理进位,即使出现也不会越界。
参考代码
#include <bits/stdc++.h>
using namespace std;
int main() {
int i, j;
scanf("%d-%d", &i, &j); // 格式化输入,自动跳过 '-' 号
++j;
if (j == 9) { // 本世界最后一关:进入下一世界第 1 关
++i;
j = 1;
}
printf("%d-%d\n", i, j);
return 0;
}官方题解给出了一种直接操作字符串的写法,同样干净:
#include <bits/stdc++.h>
using namespace std;
int main() {
string S;
cin >> S;
if (S[2] == '8') { // 最后一个字符是 '8'
S[0] += 1; // 世界号加 1
S[2] = '1'; // 关卡号重置为 1
} else {
S[2] += 1; // 关卡号加 1
}
cout << S << '\n';
return 0;
}a, b = map(int, input().split('-'))
b += 1
if b > 8:
a += 1
b = 1
print(f"{a}-{b}")复杂度分析
- 时间复杂度:
- 空间复杂度:
B - Looped Rope
题目链接:https://atcoder.jp/contests/abc422/tasks/abc422_b
题目大意
有一个 的网格,第 行第 列的格子记作 。每个格子为白色(.)或黑色(#)。判断网格是否满足以下条件:对每个黑色格子,与其上下左右相邻的黑色格子个数为 或 。
思路
条件只对黑格提出要求,白格不限。因此直接枚举所有格子:若 是黑格,就统计四个方向()中黑格的个数 ,若 ,立即输出 No;若所有黑格都满足,输出 Yes。没有任何黑格时条件自然成立,输出 Yes。
实现技巧——哨兵(sentinel):判断四方向时最麻烦的是越界(比如左上角的格子只有两个邻居)。我们可以在原网格四周再围一圈白色格子,即开一个 的数组,边框全部为 .,把原网格填进中间。这样任何格子的四个方向邻居都一定存在,无需判断是否越界,代码更简洁。
需要注意"相邻"的定义是 ,即只考虑上下左右四个格子,不含斜对角。
参考代码
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int H, W;
cin >> H >> W;
// 在外围加一圈白色哨兵,避免越界判断
vector<string> field(H + 2, string(W + 2, '.'));
for (int i = 1; i <= H; ++i) {
string S;
cin >> S;
field[i] = "." + S + ".";
}
int dx[4] = {-1, 1, 0, 0};
int dy[4] = {0, 0, -1, 1};
for (int i = 1; i <= H; ++i) {
for (int j = 1; j <= W; ++j) {
if (field[i][j] != '#') continue;
int cnt = 0;
for (int d = 0; d < 4; ++d) {
if (field[i + dx[d]][j + dy[d]] == '#') ++cnt;
}
if (cnt != 2 && cnt != 4) {
cout << "No\n";
return 0;
}
}
}
cout << "Yes\n";
return 0;
}H, W = map(int, input().split())
field = ['.' * (W + 2)] + ['.' + input() + '.' for _ in range(H)] + ['.' * (W + 2)]
for i in range(1, H + 1):
for j in range(1, W + 1):
if field[i][j] == '#':
cnt = 0
for ni, nj in ((i - 1, j), (i + 1, j), (i, j - 1), (i, j + 1)):
cnt += (field[ni][nj] == '#')
if cnt != 2 and cnt != 4:
print("No")
exit()
print("Yes")复杂度分析
- 时间复杂度:
- 空间复杂度:
C - AtCoder AAC Contest
题目链接:https://atcoder.jp/contests/abc422/tasks/abc422_c
题目大意
Takahashi 有 个字母 A、 个字母 B、 个字母 C。每举办一场比赛需要恰好 个字母: 个 A、 个 C,外加任意 个字母(可以是 A、B 或 C)。给定 组询问,每组求最多能举办的比赛场数。
思路
设答案为 。先找上界:
- 每场比赛都要用 个
A,所以 ; - 每场比赛都要用 个
C,所以 ; - 每场比赛用 个字母,所以 。
因此
是答案的一个上界。
接下来证明 场一定可以办到(构造性论证):因为 且 ,我们先取出 个 A 和 个 C,配成 对""。剩余字母数为 。由 得 ,故
即剩余字母至少还有 个,足以给每一对 补上第三个任意字母。于是 场可达,答案就是 。
易错点: 最大为 ,超过 位有符号整数的上限,因此求和要用 64 位整数(long long)。
参考代码
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T;
cin >> T;
while (T--) {
long long a, b, c;
cin >> a >> b >> c;
cout << min({a, c, (a + b + c) / 3}) << '\n';
}
return 0;
}复杂度分析
- 时间复杂度:
- 空间复杂度:
D - Least Unbalanced
题目链接:https://atcoder.jp/contests/abc422/tasks/abc422_d
题目大意
对长度为 的非负整数序列 ,定义其 imbalance 为如下过程得到的值:初始 ;重复 次:先更新 ,再把 的相邻元素两两配对求和,得到长度减半的新序列。给定 ,请在"长度为 且元素和为 "的所有序列中构造一个使 imbalance 最小的序列,输出最小 imbalance 及该序列。
思路
关键观察:题目中的"配对求和"过程是可逆的。 一个数 是由两个数 ()配对求和得到的。反过来,从"长度 1"的序列 出发逆向构造:每步把当前层的每个元素 拆成两个和为 的数。为了尽量均衡,自然拆成
重复 次后得到长度 、和为 的序列。而 imbalance 计算过程中出现的每一层数组,正是构造过程中的每一层(顺序相反)。因此该序列的 imbalance 等于构造各层 的最大值。
引理(不变量):若某一层所有元素的 ,即所有元素只取 或 两种值,那么拆分后仍然满足 。
证明:对 拆分得到 ,两者差至多 ;对 同理。全体拆分结果组成的集合是
因为 。而
( 时为 ; 时亦为 ),所以拆分后 。初始层 的 ,归纳知所有层都满足,故构造序列的 imbalance 。
下界:
- 若 ,令所有 个元素都等于 ,则每一层都是全等序列,imbalance ,这是绝对最小值。
- 若 ,imbalance 不可能为 :因为 imbalance 意味着原序列所有元素相等,即 必须是整数,与假设矛盾。所以最小 imbalance 至少为 ,而上述构造恰好达到 ,故为最优。
实现:vector<long long> ans{K},重复 次:对 ans 中每个元素 ,加入 a/2 和 a - a/2。最后输出 max(ans)-min(ans) 以及整个 ans。注意 时 ,用 long long 存放中间值以避免任何溢出顾虑。
参考代码
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int N, K;
cin >> N >> K;
vector<long long> ans{K};
for (int t = 0; t < N; ++t) {
vector<long long> nxt;
nxt.reserve(ans.size() * 2);
for (long long a : ans) {
nxt.push_back(a / 2); // floor(a/2)
nxt.push_back(a - a / 2); // ceil(a/2)
}
ans = move(nxt);
}
long long mx = *max_element(ans.begin(), ans.end());
long long mn = *min_element(ans.begin(), ans.end());
cout << mx - mn << '\n';
for (int i = 0; i < (int)ans.size(); ++i) {
if (i) cout << ' ';
cout << ans[i];
}
cout << '\n';
return 0;
}复杂度分析
- 时间复杂度:
- 空间复杂度:
E - Colinear
题目链接:https://atcoder.jp/contests/abc422/tasks/abc422_e
题目大意
平面上有 个点( 为奇数,,坐标互异)。判断是否存在一条直线经过超过一半的点;若存在,输出该直线 的整数系数 (满足 )。
思路
为什么想到随机化? 若存在满足条件的直线 ,则 上至少有 个点( 为奇数)。随机独立地选取两个点 ,它们同时落在 上的概率为
而一旦两点都在 上,过这两点的直线就是 (两点确定一条直线)。于是"随机取两点 → 检验过这两点的直线是否穿过超过一半的点"这个试验,在答案存在时每次成功的概率超过 。单次试验只有 的代价,重复 次即可把失败概率压到任意小:
取 时失败概率约为 ,可忽略不计。
直线的表示与共线判定。 过 、 两点的直线可写成
即取 。判定点 是否在直线上,只需检查 (整数运算,不要用浮点)。关于溢出:,故 ,;求值过程中 的绝对值不超过 ,三者之和不超过 ,远小于 64 位有符号整数上限 ,用 long long 安全。
边界与易错点:
- 随机取点要保证 (点互异,故坐标不同)。
- 判断"超过一半"用
num * 2 > N( 为奇数,等价于 )。 - 若 次试验都没找到,输出
No。 - 本算法是蒙特卡洛随机化算法,存在极小概率误判;AtCoder 没有 hack 环节,固定随机种子即可;若在可 hack 的平台(如 Codeforces),应改用
random_device或chrono产生种子。
参考代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int N;
cin >> N;
vector<ll> X(N), Y(N);
for (int i = 0; i < N; ++i) cin >> X[i] >> Y[i];
mt19937_64 rng(58);
const int T = 100;
for (int t = 0; t < T; ++t) {
int i, j;
do {
i = rng() % N;
j = rng() % N;
} while (i == j);
ll a = Y[i] - Y[j];
ll b = X[j] - X[i];
ll c = X[i] * Y[j] - X[j] * Y[i];
ll num = 0;
for (int k = 0; k < N; ++k) {
num += (a * X[k] + b * Y[k] + c == 0);
}
if (num * 2 > N) { // 超过一半
cout << "Yes\n";
cout << a << ' ' << b << ' ' << c << '\n';
return 0;
}
}
cout << "No\n";
return 0;
}复杂度分析
- 时间复杂度:(,最坏约 次运算)
- 空间复杂度:
F - Eat and Ride
题目链接:https://atcoder.jp/contests/abc422/tasks/abc422_f
题目大意
给定 个点、 条边的连通无向图,顶点 有点权 。Takahashi 从顶点 出发(初始体重为 ),每次访问顶点 体重增加 ;每经过一条边,消耗"当前体重"等量的燃料。对每个顶点 ,求到达顶点 所需的最小燃料。
思路
问题转化。 设从 到 的一条路径为 。经过边 时消耗的是已访问顶点体重之和 ,因此总燃料为
即顶点 的贡献等于"离开它之后还要经过的边数"乘以 (最后一个顶点贡献为 )。
由此得到等价的"门票"模型:Takahashi 出发时带着若干张票(每张票对应一条尚待经过的边)。
- 访问顶点 时,若手里还剩 张票,则消耗 燃料;
- 每经过一条边,用掉一张票。
到达顶点 时恰好剩 张票,总花费最小化。
最优路径是简单路径。 若一条走法重复访问了某个顶点 ,删去从第一次访问 到最后一次访问 之间的闭环段,会得到一条更短的合法走法;由于所有 且所有系数都为正,删环后花费严格减少。故最优路径至多包含 个顶点,最多需要 张票。
分层 DP。 设 表示"到达顶点 ,且之后还剩 条边要走"时的最小已付燃料(已付部分包含所有已经离开过的顶点,不含 本身)。边界:(从顶点 出发、尚未离开,任何票数都可行)。转移:从 沿边走到 时,离开 时还剩 条边,需付 ,到 后剩 条边:
答案就是 。状态图是一张 DAG(票数严格递减),按 从 递减到 逐层 DP 即可,每层只需一维数组。
实现细节: 每层先复制上一层的 ,再初始化本层 为 并令 ,然后对每条无向边做双向松弛。自环边不会产生更好的值(),可放心保留。答案可能超过 (见样例 2),务必使用 64 位整数。
参考代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int N, M;
cin >> N >> M;
vector<ll> W(N);
for (auto& x : W) cin >> x;
vector<pair<int, int>> edges(M);
for (auto& [u, v] : edges) {
cin >> u >> v;
--u; --v;
}
const ll INF = (1LL << 62);
vector<ll> dp(N, INF), prev(N, INF);
dp[0] = 0; // f_{N-1}[1] = 0
for (int t = N - 1; t >= 1; --t) {
prev = dp; // prev = f_t
fill(dp.begin(), dp.end(), INF);
dp[0] = 0; // f_{t-1}[1] = 0
for (auto [u, v] : edges) {
dp[v] = min(dp[v], prev[u] + t * W[u]); // 离开 u 时还剩 t 条边
dp[u] = min(dp[u], prev[v] + t * W[v]);
}
}
for (int v = 0; v < N; ++v) cout << dp[v] << '\n'; // dp = f_0
return 0;
}复杂度分析
- 时间复杂度:
- 空间复杂度:
G - Balls and Boxes
题目链接:https://atcoder.jp/contests/abc422/tasks/abc422_g
题目大意
给定 。现有 3 个盒子,要求把 个球全部放入:盒子 1 的球数是 的倍数、盒子 2 的球数是 的倍数、盒子 3 的球数是 的倍数。求方案数模 ,其中:
- Problem 1: 个球彼此不可区分,两种放法不同当且仅当存在一个盒子球数不同;
- Problem 2: 个球被编号 ,彼此可区分,两种放法不同当且仅当存在一个球的归属盒子不同。
思路
这是一道生成函数(GF)入门题。核心思想:把"一个组合对象"编码成某个多项式中的一项,用多项式的乘除来对应组合结构的拼合。
Problem 1:普通生成函数(OGF)。 球不可区分,则一种放法只由三元组 决定,其中 ,且 ,方案数就是这样的三元组个数。令
表示"盒子 1 装了 个球"这一贡献记为 ( 遍历 的倍数)。三个盒子的整体方案对应三个多项式的乘积,故答案是
Problem 2:指数型生成函数(EGF)。 球可区分,不能直接用普通卷积。令
( 类似)。EGF 的乘法对应"带标号对象的拼接":把 个标号球分成盒子 1、2、3,恰好对应
即"先选 个球给盒子 1,再从剩下 个中选 个给盒子 2,其余给盒子 3"。故
计算:NTT 卷积。 模数 是 NTT 友好素数,原根为 。预处理阶乘与阶乘逆元;构造三个多项式的前 项( 时系数为 或 ),将它们统一补零到长度 ( 的幂),各做一次 NTT,频域逐点相乘,再逆变换,取第 项系数即可。因为 ,不存在循环卷积的环绕(wraparound),得到的正是乘积多项式的系数。若 中某个大于 ,对应多项式只有第 项非零,代码天然正确处理。
正确性要点: Problem 1 用 OGF 是因为"不可区分"只关心数量;Problem 2 用 EGF 是因为"可区分"需要组合数系数 ,而 EGF 卷积恰好产生这个因子。两类问题在生成函数层面被统一为"三次多项式卷积后取 "。
参考代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll MOD = 998244353;
const ll ROOT = 3; // MOD 的原根
ll modpow(ll a, ll e) {
ll r = 1;
while (e > 0) {
if (e & 1) r = r * a % MOD;
a = a * a % MOD;
e >>= 1;
}
return r;
}
void ntt(vector<ll>& 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) {
ll 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) {
ll u = a[i + j];
ll v = a[i + j + len / 2] * w % MOD;
a[i + j] = (u + v) % MOD;
a[i + j + len / 2] = (u - v + MOD) % MOD;
w = w * wlen % MOD;
}
}
}
if (invert) {
ll inv_n = modpow(n, MOD - 2);
for (ll& x : a) x = x * inv_n % MOD;
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int N, A, B, C;
cin >> N >> A >> B >> C;
vector<ll> fac(N + 1), invfac(N + 1);
fac[0] = 1;
for (int i = 1; i <= N; ++i) fac[i] = fac[i - 1] * i % MOD;
invfac[N] = modpow(fac[N], MOD - 2);
for (int i = N; i >= 1; --i) invfac[i - 1] = invfac[i] * i % MOD;
// 三个 N 次多项式相乘,最高次数 3N,取不小于 3N+1 的 2 的幂
int sz = 1;
while (sz < 3 * N + 1) sz <<= 1;
// Problem 1:OGF 乘积的 [x^N]
vector<ll> f1(sz), f2(sz), f3(sz);
for (int i = 0; i <= N; ++i) {
if (i % A == 0) f1[i] = 1;
if (i % B == 0) f2[i] = 1;
if (i % C == 0) f3[i] = 1;
}
ntt(f1, false); ntt(f2, false); ntt(f3, false);
for (int i = 0; i < sz; ++i) f1[i] = f1[i] * f2[i] % MOD * f3[i] % MOD;
ntt(f1, true);
cout << f1[N] << '\n';
// Problem 2:EGF 乘积的 [x^N] * N!
vector<ll> g1(sz), g2(sz), g3(sz);
for (int i = 0; i <= N; ++i) {
if (i % A == 0) g1[i] = invfac[i];
if (i % B == 0) g2[i] = invfac[i];
if (i % C == 0) g3[i] = invfac[i];
}
ntt(g1, false); ntt(g2, false); ntt(g3, false);
for (int i = 0; i < sz; ++i) g1[i] = g1[i] * g2[i] % MOD * g3[i] % MOD;
ntt(g1, true);
cout << g1[N] * fac[N] % MOD << '\n';
return 0;
}(若使用 AC Library,可直接用 atcoder::convolution 替换自写的 NTT,结果一致。)
复杂度分析
- 时间复杂度:(常数次 NTT,每次长度为 的 2 的幂)
- 空间复杂度: