AtCoder Beginner Contest 462 题解
AtCoder Beginner Contest 462 题解
| 项目 | 内容 |
|---|---|
| 比赛时间 | 2026-06-13 |
| 比赛链接 | https://atcoder.jp/contests/abc462 |
| 题目数量 | A–G 共 7 题 |
比赛概览
本场整体难度中等偏易,A、B 两题为入门送分题。C 是排序后数"前缀最小值"的经典套路,D 是用差分数组数"同时在场人数"的扫描题,两者都值得初学者反复体会。E 是一道带奇偶性约束的路径费用最小化,分类讨论很巧妙,是全场最有思维含量的一题;F 是状态设计较灵活的 DP;G 则是容斥原理 + 多项式乘积(NTT)的综合题,区分度最高,适合进阶学习。
A - Secret Numbers
题目链接:https://atcoder.jp/contests/abc462/tasks/abc462_a
题目大意
给定一个由小写英文字母和数字组成的字符串 (长度 )。把所有数字字符按原顺序取出并输出(若没有数字则输出空串)。
思路
逐字符遍历 ,判断每个字符是否为数字字符(即是否在 '0' 到 '9' 之间),是则追加到答案串末尾即可。在 C++ 中可用 c >= '0' && c <= '9' 判断,也可用 std::isdigit。
参考代码
#include <bits/stdc++.h>
using namespace std;
int main() {
string s, ans;
cin >> s;
for (char c : s)
if (c >= '0' && c <= '9') ans += c;
cout << ans << '\n';
return 0;
}官方题解给出的 Python 参考实现:
print("".join(filter(str.isdigit, input())))复杂度分析
- 时间复杂度:
- 空间复杂度:(答案字符串)
B - Gift
题目链接:https://atcoder.jp/contests/abc462/tasks/abc462_b
题目大意
个人互送礼物,第 个人给 个人(编号 )送了礼物。对每个 ,输出所有"送礼物给第 个人"的编号(升序)。
思路
用"逆邻接表":把边"第 个人送礼物给 "改记为" 收到了来自 的礼物",即向 rec[a] 中追加 。由于我们按 的顺序处理,对固定的收礼人 ,其 rec[a] 中追加的送礼人编号天然递增,无需再排序。最后对每个 输出 rec[i] 的长度与内容即可。
参考代码
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n;
vector<vector<int>> rec(n + 1);
for (int i = 1; i <= n; i++) {
int k;
cin >> k;
for (int j = 0; j < k; j++) {
int a;
cin >> a;
rec[a].push_back(i);
}
}
for (int i = 1; i <= n; i++) {
cout << rec[i].size();
for (int x : rec[i]) cout << ' ' << x;
cout << '\n';
}
return 0;
}官方题解给出的 Python 参考实现:
n = int(input())
ans = [[] for _ in range(n)]
for i in range(n):
a = list(map(int, input().split()))[1:]
for c in a:
ans[c - 1].append(i + 1)
for i in range(n):
a = [len(ans[i])] + ans[i]
print(*a)复杂度分析
- 时间复杂度:
- 空间复杂度:
C - Not Covered Points
题目链接:https://atcoder.jp/contests/abc462/tasks/abc462_c
题目大意
平面上有 个点,第 个点坐标为 ,其中 坐标和 坐标各自是 的一个排列。问有多少个 ,使得左下角为 、右上角为 的轴平行矩形的**内部(不含边界)**不含任何一个给定点。
思路
由于 、 各自是排列,所以不存在两个点有相同的 坐标或相同的 坐标。
矩形内部(不含边界)包含点 当且仅当
所有坐标都在 ,故第一个条件 恒成立,于是"点 在矩形内部"等价于 且 。因此点 满足题意当且仅当:不存在 使得 且 ,即
把点按 升序排序后,排序中第 个点满足条件当且仅当它是 值的前缀严格最小值(比之前所有点的 都小)。维护前缀最小值 从左到右扫描:若当前 ,则 计入答案并更新 。由于 是排列,不会出现相等,用严格小于即可。
参考代码
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n;
vector<pair<int, int>> pts(n);
for (int i = 0; i < n; i++) cin >> pts[i].first >> pts[i].second;
sort(pts.begin(), pts.end()); // 按 X 升序
int mn = n + 1, ans = 0;
for (auto [x, y] : pts) {
if (y < mn) { // y 是新的前缀严格最小值
mn = y;
ans++;
}
}
cout << ans << '\n';
return 0;
}官方题解给出的 Python 参考实现:
import sys
input = sys.stdin.readline
n = int(input())
xy = []
for i in range(n):
x, y = map(int, input().split())
xy.append((x - 1, y - 1))
xy.sort()
min_val = n
ans = 0
for x, y in xy:
min_val = min(min_val, y)
ans += min_val == y
print(ans)复杂度分析
- 时间复杂度:(排序)
- 空间复杂度:
D - Accomplice
题目链接:https://atcoder.jp/contests/abc462/tasks/abc462_d
题目大意
个嫌疑人,第 个在时刻 进入宅邸、 离开,其余时间不在。案件在某个整数时刻 开始,耗时 ,在 完成;两名罪犯在案发全程都在宅邸内(允许恰好在开始时刻进入或完成时刻离开)。问有多少种"(两名罪犯的无序组合,案件开始时刻 )"的组合是可能的。
思路
固定开始时刻 。第 个人可以作为罪犯,当且仅当他在 全程在场,即
所以只有满足 的人才可能在某个时刻作为罪犯,且他有效的开始时刻范围为整数区间 。
令 表示时刻 可作罪犯的人数(即满足 的 的数量)。则时刻 的两人组合数为
答案即为
的取值范围为 。用差分数组维护 :对每个满足 的人,令 、,然后做一次前缀和即可得到每个时刻的 。
注意两点:一是答案可能很大(最大约 ),必须用 64 位整数;二是当 很大、 时该人直接排除。
参考代码
#include <bits/stdc++.h>
using namespace std;
int main() {
int N, D;
cin >> N >> D;
const int M = 1000000; // 最大时刻
vector<long long> diff(M + 2, 0);
for (int i = 0; i < N; i++) {
int S, T;
cin >> S >> T;
if (T - S >= D) { // 可能成为罪犯
diff[S] += 1;
diff[T - D + 1] -= 1;
}
}
long long c = 0, ans = 0;
for (int t = 1; t <= M; t++) {
c += diff[t];
ans += c * (c - 1) / 2;
}
cout << ans << '\n';
return 0;
}官方题解给出的 Python 参考实现:
N, D = map(int, input().split())
M = 10**6 + 1
d = [0] * M
for i in range(N):
S, T = map(int, input().split())
if T - S >= D:
d[S] += 1
d[T - D + 1] -= 1
c, ans = 0, 0
for i in range(M):
c += d[i]
ans += c * (c - 1) // 2
print(ans)复杂度分析
- 时间复杂度:,其中
- 空间复杂度:
E - Alternating Costs
题目链接:https://atcoder.jp/contests/abc462/tasks/abc462_e
题目大意
棋子从 出发,每次可向上下左右四方向走一格。第 步()的费用取决于 的奇偶性: 为奇数时水平走费用 、垂直走费用 ; 为偶数时反过来(水平 、垂直 )。求到达 的最小总费用。 组数据。
思路
记 为所求最小费用。先把棋盘整体沿坐标轴翻转,费用不变,因此可以假设 。
情形一: 为奇数。 每走一步 的奇偶性都会翻转,所以到达 所需的步数 满足 ;当 为奇数时 为奇数,最后一步是"奇序号"步。对最优路径做翻转论证(把路径沿 与 翻折后费用不变),可以令最后一步为从 水平走到 (费用 ),或从 垂直走到 (费用 )。于是
从而把问题化归为 为偶数的情形( 与 均为偶数)。当 或 时上述式子会出现负坐标,公式依然成立(对应"先绕路多走几步、最后折回"的最优方案),代码中由闭式公式直接处理。
情形二: 为偶数。 设步数为 ,其中奇序号、偶序号各 步。由对称性可再假设 (否则交换 )、(否则交换 )。
由于 ,我们希望尽量让更多的步花费 。花费 的步只有两类:奇序号水平步和偶序号垂直步。对固定的 (可行范围为 ),最小费用为
直观理解:总共 步先全部按 计费,得 ;再考虑至少有 步必须花费 。原因是最多只有 步"偶序号垂直步"可以花 ,而垂直方向需要净位移 ,故至少有 步垂直移动(奇序号)不得不以 计费。这个下界可以达到:安排 个奇序号步做垂直移动(费用 ),其余奇序号步做水平移动、所有偶序号步做垂直移动(费用 ),多出来的水平移动用"一来一回"的对子抵消即可,总费用恰为 。
是 的线性函数,最小值必在可行区间的端点取得,所以只需比较
当 时(即 步内可以全部用费用 的步完成),增加 只会使总费用每对增加 ,最优点仍在 ,已被上式覆盖。故偶数情形的答案即为上述两者的较小值。
汇总算法:先取 ;若 为奇数,递归地取 ;否则直接 。其中 内部再交换到 并返回 。所有中间量都要用 64 位整数( 最大约 )。
参考代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
// 处理 X+Y 为偶数、且 |X|,|Y| 已归一化的情形(内部交换到 A<=B, X<=Y)
ll solve(ll a, ll b, ll x, ll y) {
if (a > b) swap(a, b);
if (x > y) swap(x, y);
return min(2 * a * y, a * (x + y) + (b - a) * (y - x) / 2);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T;
cin >> T;
while (T--) {
ll A, B, X, Y;
cin >> A >> B >> X >> Y;
X = llabs(X);
Y = llabs(Y);
ll ans;
if ((X + Y) & 1) {
ans = min(solve(A, B, X - 1, Y) + A, solve(A, B, X, Y - 1) + B);
} else {
ans = solve(A, B, X, Y);
}
cout << ans << '\n';
}
return 0;
}官方题解给出的 Python 参考实现:
def solve(a, b, x, y):
if a > b:
a, b = b, a
if x > y:
x, y = y, x
return min(2 * a * y, a * (x + y) + (b - a) * (y - x) // 2)
for _ in range(int(input())):
a, b, x, y = map(int, input().split())
x, y = abs(x), abs(y)
if (x + y) % 2 == 1:
print(min(solve(a, b, x - 1, y) + a, solve(a, b, x, y - 1) + b))
else:
print(solve(a, b, x, y))复杂度分析
- 时间复杂度:(每组 )
- 空间复杂度:
F - More ABC
题目链接:https://atcoder.jp/contests/abc462/tasks/abc462_f
题目大意
给定大写字母串 ,每次可以把一个字符替换成任意大写字母。问能否通过若干次替换,使 中"ABC"作为子串的出现次数恰好增加 ;若能,输出最少替换次数,否则输出 。 组数据,所有 的总长度不超过 ,。
思路
这是"从左往右逐位确定最终串"的 DP。记 为串 中"ABC"的出现次数。
设 (,)表示:考虑 的前 个字符,通过替换得到 的前 个字符,使 所需的最少替换次数,不可能则为 。初始 ,,答案为 。
在第 个位置,新出现的"ABC"只可能以 结尾,即窗口 。分两种情况讨论( 时不存在窗口,只走第二种):
情形 1:。 此时 、,故任何以 或 结尾的窗口都不可能是"ABC",于是
原串中"以 结尾"的 ABC 个数为 (这三个窗口两两重叠,最多出现一个)。于是需满足
再设 为把 改成"ABC"所需替换次数(),得到转移
情形 2:。 可以假设第 个字符没有被修改。理由:若 被修改而窗口仍不是"ABC",那么把 换回原字符 ,(以 结尾的窗口从"非 ABC"变回"非 ABC",不会新增出现次数),替换次数更少,矛盾。故 ,此时
设 (即原串中 是否为"ABC"),则需满足
得到转移
综上
下标越界的一项忽略即可( 恒成立,因为 ; 时该状态不可能通向最优解)。 可直接判断原串窗口是否为"ABC",, 数一数窗口内三个位置与"A、B、C"的失配个数,均可 预处理。若 输出 。
参考代码
#include <bits/stdc++.h>
using namespace std;
#define INF (int)1e9
int main() {
int t, n, k;
string s;
cin >> t;
while (t--) {
cin >> s;
cin >> k;
n = (int)s.size();
vector<int> x(n + 1), y(n + 1), z(n + 1);
vector<vector<int>> dp(n + 1, vector<int>(k + 1));
for (int i = 3; i <= n; i++) { // 预处理 Y_i, Z_i, X_i
if (s[i - 3] != 'A') y[i]++;
if (s[i - 2] != 'B') y[i]++;
if (s[i - 1] != 'C') y[i]++;
if (y[i] == 0) z[i] = 1; // 原串窗口 S[i-2..i] 是 ABC
for (int j = 0; j < 3; j++)
if (i + j <= n) x[i + j] += z[i]; // x[m] = Z_{m-2}+Z_{m-1}+Z_m
}
for (int j = 1; j <= k; j++) dp[0][j] = INF;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= k; j++) {
dp[i][j] = INF;
if (j + z[i] <= k)
dp[i][j] = min(dp[i][j], dp[i - 1][j + z[i]]); // 情形 2
if (i >= 3)
dp[i][j] = min(dp[i][j], dp[i - 3][j - 1 + x[i]] + y[i]); // 情形 1
}
}
cout << (dp[n][k] >= INF ? -1 : dp[n][k]) << '\n';
}
return 0;
}(官方题解给出的即为上述 C++ 实现,未提供独立的官方 Python。)
复杂度分析
- 时间复杂度:
- 空间复杂度:,可将 滚动优化至
G - Completely Wrong
题目链接:https://atcoder.jp/contests/abc462/tasks/abc462_g
题目大意
箱中有 个球,球 的颜色为 。第 次操作:从箱中均匀随机摸出一个球并丢弃,若摸出的球颜色恰为 则得 分。求 次操作后总分为 的概率,对 取模。
思路
摸球的顺序等价于随机排列 ( 表示第 次摸出的球),总分 。所求即
记 。由容斥原理,
答案 。
对固定的集合 ,令 ,,。若某颜色 满足 ,则 ;否则:
- 从 个满足 的位置中选出 个放进 : 种;
- 从 个颜色为 的球中选出 个并一一分配给这些位置: 种;
- 其余 个位置任意排列剩余球: 种。
于是贡献为 ,乘上符号 后求和。令 ,,则答案可写成
实现时: 的次数为 ,所有 的次数之和不超过 。用优先队列每次取出两个次数最小的多项式做 NTT 乘法后放回,总复杂度 。模数 ,原根为 ,很适合做 NTT。注意若每个颜色都有 ,则 ,答案为 。
参考代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll MOD = 998244353;
const ll ROOT = 3;
ll modpow(ll a, ll e) {
ll r = 1;
while (e) {
if (e & 1) r = r * a % MOD;
a = a * a % MOD;
e >>= 1;
}
return r;
}
void ntt(vector<ll>& a, bool inv) {
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 (inv) 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], 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 (inv) {
ll inv_n = modpow(n, MOD - 2);
for (auto& x : a) x = x * inv_n % MOD;
}
}
vector<ll> multiply(vector<ll> a, vector<ll> b) {
int need = (int)a.size() + (int)b.size() - 1;
int n = 1;
while (n < need) n <<= 1;
a.resize(n);
b.resize(n);
ntt(a, false);
ntt(b, false);
for (int i = 0; i < n; i++) a[i] = a[i] * b[i] % MOD;
ntt(a, true);
a.resize(need);
return a;
}
struct Cmp {
bool operator()(const vector<ll>& a, const vector<ll>& b) const {
return a.size() > b.size();
}
};
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int N;
cin >> N;
vector<int> C(N), G(N);
for (int i = 0; i < N; i++) cin >> C[i];
for (int i = 0; i < N; i++) cin >> G[i];
vector<int> X(N + 1), Y(N + 1);
for (int i = 0; i < N; i++) { X[C[i]]++; Y[G[i]]++; }
vector<ll> fact(N + 1), invfact(N + 1);
fact[0] = 1;
for (int i = 1; i <= N; i++) fact[i] = fact[i - 1] * i % MOD;
invfact[N] = modpow(fact[N], MOD - 2);
for (int i = N; i >= 1; i--) invfact[i - 1] = invfact[i] * i % MOD;
auto comb = [&](int n, int k) -> ll {
if (k < 0 || k > n) return 0;
return fact[n] * invfact[k] % MOD * invfact[n - k] % MOD;
};
priority_queue<vector<ll>, vector<vector<ll>>, Cmp> pq;
for (int k = 1; k <= N; k++) {
int m = min(X[k], Y[k]);
if (m == 0) continue; // f_k(x) = 1,跳过
vector<ll> f(m + 1);
for (int j = 0; j <= m; j++) {
ll coef = comb(X[k], j) * comb(Y[k], j) % MOD * fact[j] % MOD;
if (j & 1) coef = (MOD - coef) % MOD;
f[j] = coef;
}
pq.push(move(f));
}
if (pq.empty()) { cout << 1 << '\n'; return 0; }
while (pq.size() > 1) {
vector<ll> a = pq.top(); pq.pop();
vector<ll> b = pq.top(); pq.pop();
pq.push(multiply(a, b));
}
vector<ll> f = pq.top();
ll ans = 0;
for (int i = 0; i < (int)f.size(); i++)
ans = (ans + f[i] * fact[N - i]) % MOD;
ans = ans * modpow(fact[N], MOD - 2) % MOD;
cout << ans << '\n';
return 0;
}(本题官方题解仅给出了容斥 + 多项式乘积的算法与复杂度分析,未提供参考代码;上述代码为依据该思路实现并验证过的版本。)
复杂度分析
- 时间复杂度:(多项式乘积,采用优先队列合并最小次数多项式 + NTT)
- 空间复杂度: