AtCoder Beginner Contest 466 题解
AtCoder Beginner Contest 466 题解
| 项目 | 内容 |
|---|---|
| 比赛时间 | 2026-07-11 |
| 比赛链接 | https://atcoder.jp/contests/abc466 |
| 题目数量 | A–G 共 7 题 |
比赛概览
本场整体难度中等偏高,A、B 是两道热身题;C 是一道交互题,需要利用点的顺序性做双指针滑动窗口;D 考察逆向思维(或带计数的正向模拟);E 需要先把"区间翻转"问题规约成"段划分",再做简单的 DP;F 用"区间多重集 + 优先队列"的巧妙视角统计取模结果;G 则是全场最难、最有区分度的一题,综合了带权并查集与二进制按位 DP。建议重点掌握 C 的交互式滑动窗口思想、E 的区间合并观察,以及 F 的"对区间整体取模"技巧。
A - Compromise
题目链接:https://atcoder.jp/contests/abc466/tasks/abc466_a
题目大意
有 个选项,若选择第 个选项,你会获得快乐值 。若无论选择哪个选项,你的快乐值都是负数,输出 Yes,否则输出 No。
思路
题目其实是在问:是否存在一个选择能让自己开心(快乐值 )。
- 如果存在某个 使得 ,那么选它即可,答案应为
No; - 只有当所有 都是负数(严格 )时,无论怎么选都会不开心,答案才是
Yes。
即
逐项读入、检查即可。注意 不属于负数(样例 3 中的 就说明了这一点)。
参考代码
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n;
bool allNegative = true;
for (int i = 0; i < n; i++) {
int x;
cin >> x;
if (x >= 0) allNegative = false;
}
cout << (allNegative ? "Yes" : "No") << '\n';
return 0;
}官方题解还给出了一行 Python 写法:
N = int(input())
X = list(map(int, input().split()))
print("No" if max(X) >= 0 else "Yes")复杂度分析
- 时间复杂度:
- 空间复杂度:
B - Representative Balls
题目链接:https://atcoder.jp/contests/abc466/tasks/abc466_b
题目大意
有 个球,第 个球的颜色为 、大小为 (颜色编号为 )。对每种颜色 ,输出该颜色球的最大大小;若该颜色不存在任何球,输出 -1。
思路
经典"按颜色维护最大值"问题。开一个数组 mx[1..M] 初始化为 (恰好表示"该颜色不存在"),然后依次扫描每个球,做
把初值设成 的好处是不需要再单独处理"颜色不存在"的分支——如果没有该颜色的球,mx[k] 就一直是 。
最后按 的顺序输出即可。
参考代码
#include <bits/stdc++.h>
using namespace std;
int main() {
int n, m;
cin >> n >> m;
vector<int> mx(m, -1);
for (int i = 0; i < n; i++) {
int c, s;
cin >> c >> s;
c--;
mx[c] = max(mx[c], s);
}
for (int i = 0; i < m; i++) {
if (i) cout << ' ';
cout << mx[i];
}
cout << '\n';
return 0;
}复杂度分析
- 时间复杂度:
- 空间复杂度:
C - Count Close Pairs
题目链接:https://atcoder.jp/contests/abc466/tasks/abc466_c
题目大意
这是一道交互题。数轴上从左到右依次有 个点(每个点的具体坐标未知)。你可以向评测机发起至多 次如下询问:
- 选择 ,询问点 与点 之间的距离是否不超过 ,返回
Yes/No。
求满足"距离不超过 "的点对 ()的总数,输出 ! X。
思路
设第 个点的坐标为 ,由题意 ,距离即 。
关键观察 1(连续性):对固定的 ,若 ,则对任意 都有 。因此满足 的 构成一段连续区间 ,其中 是与 的距离不超过 的最远点。
关键观察 2(单调性):若 ,则
所以 ,即
于是答案可写成
因为点 与 之间(共 个点)的距离都不超过 。
滑动窗口算法:我们按 的顺序确定 。维护两个指针 (初值 ),并保持如下不变量:已知" 到所有 的 的距离都不超过 "。
- 询问
? L R:- 若回答
Yes():由单调性 ,我们还不知道 的确切值,于是 ,继续扩大; - 若回答
No():由连续性与不变量,,累加答案 ,然后 (此时新 到 的距离都已不超过 ,不变量保持)。若 ,则把 也加一(此时 到它自身距离为 ,相当于已知答案)。
- 若回答
当 时主循环结束,此时剩余的 都有 ,逐个累加 即可。
询问次数:每次询问都会让 至少增加 ,而 最多从 增长到 ,所以总询问次数至多 (事实上可以证明至多 次),严格小于上限 。
易错点:这是交互题,每次输出后必须换行并刷新输出缓冲(C++ 中用 endl 即可),否则可能因缓冲未刷新导致超时;输出 ! X 后要立即结束程序。
参考代码
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n;
int l = 1, r = 2;
long long ans = 0;
string s;
while (r <= n) {
cout << "? " << l << " " << r << endl; // endl 自带 flush
cin >> s;
if (s == "Yes") {
r++; // M_l >= r,还不知道确切的 M_l
} else {
ans += (r - l - 1); // 确定 M_l = r-1
l++;
if (l == r) r++;
}
}
while (l < n) { // 剩余的 l: M_l = n
ans += (r - l - 1);
l++;
}
cout << "! " << ans << endl;
return 0;
}复杂度分析
- 询问次数:至多 次(满足 的限制)
- 时间复杂度:
- 空间复杂度:
D - Placing Rooks
题目链接:https://atcoder.jp/contests/abc466/tasks/abc466_d
题目大意
有一个 的棋盘,初始为空。Takahashi 按顺序进行 次操作,第 次操作:
- 移除第 行上所有棋子;
- 移除第 列上所有棋子;
- 在 格子上放一枚棋子。
求全部 次操作结束后棋盘上的棋子总数。
思路
官方题解给出两种解法,这里都介绍一下。
解法 1:倒着思考(推荐)
关键观察:第 次操作放置的棋子能活到最后,当且仅当第 到第 次操作中,没有任何一次操作在第 行或第 列上放置棋子。
理由很直接:后续每次操作都会"清除整行 + 清除整列"。若第 次操作()满足 或 ,那么第 次操作就会把 上的棋子清掉(之后放上去的新棋子当然不是原来的那枚了)。反之,如果所有后续操作的 都不等于 ,则 永远不会被清除。
因此,倒序扫描操作(从第 次到第 次),维护两个布尔数组:
rused[x]:第 行是否已被"当前已经扫描过的操作"放置过棋子;cused[y]:第 列是否已被放置过棋子。
对第 次操作,若 rused[R_i] 与 cused[C_i] 都为假,则这枚棋子存活,答案加一;然后无条件地标记 rused[R_i] = cused[C_i] = true。
正确性:当前操作放置的棋子存活与否,只取决于更靠后的操作(即已经扫描过的那些),所以倒序扫描时在"决策时刻"就能得到与正序模拟完全一致的结果。
解法 2:正向模拟
先做一个观察:任意时刻,每一行和每一列至多有一枚棋子(因为每次放棋子前都会清空它所在的行和列)。
于是可以维护两个数组:
rplace[x]:第 行的棋子是由哪次操作放置的(0 表示空);cplace[y]:第 列的棋子是由哪次操作放置的。
对第 次操作 :
- 若
rplace[R_i] > 0,说明第 行上有一枚棋子,把它移除(同时清空对应行、列记录,答案减一); - 若
cplace[C_i] > 0,同理移除第 列上的棋子(若它与第 1 步是同一枚棋子,则此时已被清掉,不会重复减); - 放置新棋子:
rplace[R_i] = cplace[C_i] = i,答案加一。
每次操作至多移除两枚棋子,均摊 。
参考代码
解法 1(倒序):
#include <bits/stdc++.h>
using namespace std;
int main() {
int n, m;
cin >> n >> m;
vector<int> r(m), c(m);
for (int i = 0; i < m; i++) cin >> r[i] >> c[i];
vector<char> rused(n + 1, 0), cused(n + 1, 0);
int ans = 0;
for (int i = m - 1; i >= 0; i--) {
if (!rused[r[i]] && !cused[c[i]]) ans++; // 该棋子存活
rused[r[i]] = 1;
cused[c[i]] = 1;
}
cout << ans << '\n';
return 0;
}解法 2(正向模拟):
#include <bits/stdc++.h>
using namespace std;
int main() {
int n, m;
cin >> n >> m;
vector<int> r(m + 1), c(m + 1);
vector<int> rplace(n + 1, 0), cplace(n + 1, 0);
int ans = 0;
for (int i = 1; i <= m; i++) {
cin >> r[i] >> c[i];
if (rplace[r[i]] > 0) {
int idx = rplace[r[i]];
rplace[r[idx]] = 0;
cplace[c[idx]] = 0;
ans--;
}
if (cplace[c[i]] > 0) {
int idx = cplace[c[i]];
rplace[r[idx]] = 0;
cplace[c[idx]] = 0;
ans--;
}
rplace[r[i]] = i;
cplace[c[i]] = i;
ans++;
}
cout << ans << '\n';
return 0;
}复杂度分析
- 时间复杂度:解法 1 为 ;解法 2 为 (读入本身是 )
- 空间复杂度:解法 1 为 ;解法 2 为
E - Range Flip
题目链接:https://atcoder.jp/contests/abc466/tasks/abc466_e
题目大意
有 张卡,第 张正面写 、背面写 ,初始全部正面朝上。你可以进行至多 次操作:选择区间 ,把区间内每张卡翻面。求操作结束后朝上一面的数字之和的最大可能值。
思路
设区间用半开区间 表示。
关键观察 1:可以把操作区间变成互不相交的
翻转的本质是对每个位置异或 ,所以最终哪些卡是背面朝上,取决于每个位置被覆盖的次数是奇数还是偶数,即这些区间的对称差。
假设存在一个最优解,其"被操作区间的总长度"在所有最优解中最小。若其中有两个区间重叠,设它们为 与 ,则用下面两个区间替换它们:
每张卡最终的朝向完全不变(对称差不变),但两个新区间的总长度严格变小(例如当 时,原总长 减去新总长 等于 )。这与最小性矛盾,所以可以假定最优解中的操作区间互不相交(替换后若出现空区间,直接丢弃该次操作即可)。
于是问题等价于:选择至多 个互不相交的区间翻转。
关键观察 2:转成"段划分"
从左到右扫过卡片,每当进入一个翻转区间时,朝向就翻转一次。因此整副牌被划分成若干交替的段:奇数段正面朝上(取 ),偶数段背面朝上(取 )。因为至多 个翻转区间,段数至多 。
DP
设 表示考虑前 张卡、且第 张卡属于第 段()时,朝上一面数字之和的最大值。第 段为奇数段时取 ,偶数段时取 ,记
转移时,第 张卡要么延续第 张卡的段( 不变),要么新起一段(第 张在第 段):
边界:(),这允许开头若干段为空,即允许第一张卡就直接属于一个翻转区间。
为什么只需要从 转移,而不是所有 ? 因为中间的空段是无意义的:若两段同朝向之间夹一个空段,它们本质上仍是同一段(相邻的两个翻转区间会合并成一个),所以最优解里中间的段不会为空,只有末尾(或开头)的段可以空。
答案取 。
正确性:任意一个"至多 个不相交区间翻转"的方案都唯一对应一个段划分(以及空段),DP 枚举了所有段划分;反之每个 DP 产生的段划分都对应一个合法的翻转方案(偶数段即翻转区间,数量不超过 )。因此两者最大值一致。
参考代码
#include <bits/stdc++.h>
using namespace std;
int main() {
int n, k;
cin >> n >> k;
vector<long long> a(n), b(n);
for (int i = 0; i < n; i++) cin >> a[i] >> b[i];
int C = 2 * k + 1;
const long long NEG = -(1LL << 62);
vector<long long> dp(C, 0); // dp[j]: 第 i 张卡属于第 j 段(0-based)
for (int i = 0; i < n; i++) {
vector<long long> ndp(C, NEG);
for (int j = 0; j < C; j++) {
long long best = dp[j];
if (j > 0) best = max(best, dp[j - 1]);
if (best == NEG) continue;
// 第 j+1 段:j 为偶数 → 奇数段 → 正面 A;j 为奇数 → 偶数段 → 背面 B
long long val = (j % 2 == 0) ? a[i] : b[i];
ndp[j] = best + val;
}
dp.swap(ndp);
}
long long ans = 0;
for (int j = 0; j < C; j++) ans = max(ans, dp[j]);
cout << ans << '\n';
return 0;
}复杂度分析
- 时间复杂度:(段数 ,)
- 空间复杂度:
F - Many Mod Calculation
题目链接:https://atcoder.jp/contests/abc466/tasks/abc466_f
题目大意
给定 和正整数序列 ,定义
对每个测试用例,求 中满足 的 的个数。
思路
把"一个个数"改成"区间多重集"
直接枚举 显然不行()。换一个视角:维护"当前所有可能取值"组成的多重集,初始为 。每应用一次 ,就相当于把多重集中每个值 替换成 。
为了利用区间结构,我们改考虑 这 个数(即区间 内的所有整数),最后再减去 的贡献——因为 , 恒满足 ,但它不在 范围内。
关键表示:把多重集表示为若干" 份 "的形式,其中 表示整数 , 表示该区间出现的份数(即该区间内每个值都被 个互不相同的原始取值映射到)。
现在考虑对" 份 "整体应用 (设 ):
- 若 :区间内每个数都小于 ,取模后不变,仍然是 份 ;
- 若 :把 按模 分成 段 和最后一段 ,取模后
若 ,第二项为空区间,直接丢弃。
用优先队列模拟
用最大堆(按区间右端 排序)维护所有区间。对每个 ,反复取出堆顶、把右端 的区间按上式分裂;相同右端的区间合并计数(保证堆内右端互不相同)。
复杂度:每次分裂产生的残余区间 满足 (因为当 时,,且 ,两者均不超过 )。所以每个区间经历 次"缩小一半",产生的区间总数是 ,再乘上堆操作 ,总复杂度 。
统计答案
处理完所有 后,堆中每个区间 ()恰好贡献一个取值为 的结果(即区间内的 ),所以 的 的个数等于各区间份数之和。答案还要减去 这一项,故
注意 ,全部用 64 位整数计算。
参考代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t;
cin >> t;
while (t--) {
int n;
ll x;
cin >> n >> x;
vector<ll> a(n);
for (ll &v : a) cin >> v;
// 最大堆,按区间右端点 x 排序,pair.second 为该区间份数
priority_queue<pair<ll, ll>> pq;
pq.push({x + 1, 1}); // 1 份 [0, X+1),即 0..X
for (ll v : a) {
while (!pq.empty() && pq.top().first > v) {
auto [val, cnt] = pq.top();
pq.pop();
while (!pq.empty() && pq.top().first == val) { // 合并同右端
cnt += pq.top().second;
pq.pop();
}
pq.push({v, val / v * cnt}); // (val/v) 份 [0, v)
if (val % v != 0) pq.push({val % v, cnt}); // 1 份 [0, val%v)
}
}
ll ans = -1; // 减去 x=0(它恒有 f(x)=0)
while (!pq.empty()) {
ans += pq.top().second;
pq.pop();
}
cout << ans << '\n';
}
return 0;
}官方题解给出的 Python 实现如下(使用最小堆存放 以模拟最大堆):
import sys
from heapq import heappush, heappop
input = sys.stdin.readline
for _ in range(int(input())):
n, x = map(int, input().split())
a = list(map(int, input().split()))
pq = [(-(x + 1), 1)]
for v in a:
while pq and -pq[0][0] > v:
val, cnt = heappop(pq)
val = -val
while pq and -pq[0][0] == val:
cnt += heappop(pq)[1]
heappush(pq, (-v, (val // v) * cnt))
if val % v:
heappush(pq, (-(val % v), cnt))
ans = -1
while pq:
ans += heappop(pq)[1]
print(ans)复杂度分析
- 时间复杂度:每个测试用例
- 空间复杂度:
G - Segment Sum Constraints
题目链接:https://atcoder.jp/contests/abc466/tasks/abc466_g
题目大意
给定 个三元组 ,考虑满足如下条件的 元正整数元组 :
若满足条件的元组有无穷多个,输出 Infinity;否则输出满足条件的元组个数 。
思路
这是全场最难的一题,分为"化简条件"与"二进制按位 DP 计数"两大步。 是核心约束,让按位 DP 可行。
第一步:转成非负整数 + 前缀和表示
条件给的是正整数。令
则 为正整数等价于 为非负整数,且约束变为 。若某 ,显然无解,输出 。
记前缀和 ,则约束写成
第二步:带偏移的并查集(找出条件之间的线性关系)
我们希望把每条约束化成""的形式。维护数组 与 ,满足恒等式 。初始 。
按输入顺序处理每条约束,设 :
- 若 :由已有关系可得 ,因此当且仅当 时约束自动满足,否则矛盾,输出 。
- 若 :由 得
于是把所有 的下标 改为 ,并令 。
- 若 :对称地,把所有 的下标 改为 ,并令 。
由于 ,每次直接 扫描更新即可。
第三步:提取化简后的独立方程
处理完全部 条约束后,对每个 ,找到最小的 使得 ,则
是一个必要且充分的独立方程(若 则无解,输出 )。这样得到至多 个方程,且每个方程左端起点 互不相同;可以证明它们等价于原约束系统。
同时用 used 数组标记哪些 变量出现在方程中。若某个 不出现在任何方程里,则它不受任何约束:
- 若其余变量存在至少一个可行解,则该变量可取任意非负整数,答案为
Infinity; - 若其余变量无解,答案为 。
第四步:二进制按位 DP 计数
所有 ,且出现在方程中的每个 都不超过它所在方程的和 ,所以 30 位二进制足够。
设 表示:考虑 的低 位,满足所有方程低 位相等、且第 个方程产生的"进位"为 时的方案数。这里
由于总和不超过 ,进位 。
初始 。对第 位(),枚举 种数字组合 (即每个 的第 位),对每个方程 ,若
则新进位
并把 累加上 。
实现技巧: 种数字组合与进位组合可以预先枚举好。按"每个方程数字和 的奇偶向量 "分组,预处理出所有"奇偶向量为 的数字组合"对应的 向量及其数量,DP 时直接查表,避免每层重复枚举。
最终答案 (进位全为 表示所有方程的和精确等于 )。注意要单独维护"该状态是否可达"(即 ),因为计数取模后可能恰为 但方案数仍为正。
参考代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll MOD = 998244353;
int n, m;
int base[9]; // k_j:代表元下标
ll offset[9]; // D_j:满足 P_j = P_{base[j]} + offset[j]
vector<int> dmask; // 每个化简方程覆盖的 B 变量集合(bitmask,bit i 表示 B_{i+1})
vector<ll> ds; // 每个化简方程的右端 S'_j
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
for (int i = 0; i <= n; i++) { base[i] = i; offset[i] = 0; }
// 处理原始约束,维护 P_j = P_{base[j]} + offset[j]
for (int i = 0; i < m; i++) {
int l, r; ll S;
cin >> l >> r >> S;
l--; // L_i - 1
S -= (r - l); // 转成非负整数版本
if (S < 0) { cout << 0 << '\n'; return 0; }
int a = l, b = r; // 约束:P_b - P_a = S
if (base[a] == base[b]) {
if (offset[b] - offset[a] != S) { cout << 0 << '\n'; return 0; }
continue; // 已被已有条件自动满足
}
int ob, oa; ll add;
if (base[a] < base[b]) {
ob = base[b]; oa = base[a];
add = offset[a] - offset[b] + S; // P_{base[b]} = P_{base[a]} + add
} else {
ob = base[a]; oa = base[b];
add = offset[b] - offset[a] - S;
}
for (int j = 0; j <= n; j++) if (base[j] == ob) {
base[j] = oa;
offset[j] += add;
}
}
// 提取化简后的独立方程
bool used[8] = {};
for (int i = 0; i < n; i++) {
for (int j = i + 1; j <= n; j++) {
if (base[i] == base[j]) {
if (offset[j] - offset[i] < 0) { cout << 0 << '\n'; return 0; }
int mask = 0;
for (int ii = i; ii < j; ii++) { mask |= 1 << ii; used[ii] = true; }
dmask.push_back(mask);
ds.push_back(offset[j] - offset[i]);
break;
}
}
}
int K = (int)dmask.size(); // M'
// 预处理:按"方程内数字和的奇偶向量"分组
// groups[p] = { (zpart, count) },其中 p[j] = (x_j mod 2),zpart[j] = x_j >> 1
vector<vector<pair<int, int>>> groups(1 << K);
{
unordered_map<int, int> tmp[1 << 8];
for (int mask = 0; mask < (1 << n); mask++) {
int x[8] = {};
for (int j = 0; j < K; j++) {
for (int ii = 0; ii < n; ii++) if (dmask[j] >> ii & 1) x[j] += (mask >> ii) & 1;
}
int parity = 0, zpart = 0;
for (int j = 0; j < K; j++) {
parity |= (x[j] & 1) << j;
zpart |= (x[j] >> 1) << (3 * j);
}
tmp[parity][zpart]++;
}
for (int p = 0; p < (1 << K); p++)
for (auto &pr : tmp[p]) groups[p].push_back(pr);
}
// 二进制按位 DP:dpkey 用每 3 位存一个进位 C_j(0..7)
vector<int> dpkey = {0};
vector<ll> dpcnt = {1};
for (int bit = 0; bit < 30; bit++) {
unordered_map<int, ll> mp;
for (size_t idx = 0; idx < dpkey.size(); idx++) {
int key = dpkey[idx];
ll cnt = dpcnt[idx];
int y[8] = {};
for (int j = 0; j < K; j++) y[j] = (key >> (3 * j)) & 1; // C_j mod 2
int pre = 0;
for (int j = 0; j < K; j++) pre |= ((key >> (3 * j + 1)) & 3) << (3 * j); // C_j >> 1
int add_key = 0;
for (int j = 0; j < K; j++) {
int sbit = (ds[j] >> bit) & 1; // S'_j 的第 bit 位
add_key |= (sbit ^ y[j]) << j; // 需要的 x_j 奇偶性
if (sbit == 0 && y[j] == 1) pre += 1 << (3 * j); // (C_j+1)/2 中的 +1
}
for (auto &pr : groups[add_key]) {
mp[pre + pr.first] = (mp[pre + pr.first] + (ll)pr.second * cnt) % MOD;
}
}
dpkey.clear(); dpcnt.clear();
for (auto &pr : mp) {
dpkey.push_back(pr.first);
dpcnt.push_back(pr.second);
}
}
bool found = false;
ll ans = 0;
for (size_t i = 0; i < dpkey.size(); i++) {
if (dpkey[i] == 0) { found = true; ans = dpcnt[i]; }
}
if (!found) {
cout << 0 << '\n'; // 约束变量无可行解
} else {
for (int i = 0; i < n; i++)
if (!used[i]) { cout << "Infinity\n"; return 0; } // 有自由变量
cout << ans << '\n';
}
return 0;
}复杂度分析
- 第一步的并查集部分:(,实际可视为 )
- 预处理部分:
- 按位 DP:进位组合数至多 ,每层枚举 种数字组合,共 层,即 (约 量级)
- 空间复杂度:
以上就是 ABC466 的全部七题题解。本场以 F、G 两题最有含金量:F 考察"区间多重集 + 优先队列"的抽象能力,G 综合了带权并查集与按位 DP,值得反复推敲。