AtCoder Beginner Contest 431 题解
AtCoder Beginner Contest 431 题解
| 项目 | 内容 |
|---|---|
| 比赛时间 | 2025-11-08 |
| 比赛链接 | https://atcoder.jp/contests/abc431 |
| 题目数量 | A–G 共 7 题 |
比赛概览
本场以"机器人"为背景串联 A–D 四题:A、B 是基础语法题(分支判断、数组状态维护),C 是排序+贪心,D 是 01 背包 DP。E 需要把"镜子反射光线"建模成图并跑 01-BFS,F 是一道插入 DP + 组合数的计数题,G 则是基于字典序排序结构的困难题。整体难度适中,区分度主要落在 E、F、G 三题上,它们分别考察图形建模、对"可插入位置只与计数有关"的洞察,以及对交换后字典序结构的分析。
A - Robot Balance
题目链接:https://atcoder.jp/contests/abc431/tasks/abc431_a
题目大意
头重为 克、身重为 克,若头重大于身重机器人会倒。问至少需要让身体增加多少克,才能保证机器人不倒。输入一行 H B,输出最小增重量。
思路
机器人不倒当且仅当头重不超过身重。设身体增重 克,则条件是 ,即 。因此最小增重量为
当 时答案为 ;否则答案为 。注意头、身相等时机器人不会倒,所以判断用 而不是 。实现上直接 if 分支即可,也可以用 max 一行求出。
参考代码
#include <bits/stdc++.h>
using namespace std;
int main() {
int H, B;
cin >> H >> B;
cout << max(H, B) - B << endl;
return 0;
}官方题解同时给出了等价的分支写法与 Python 版本:
H, B = map(int, input().split())
print(max(H, B) - B)复杂度分析
- 时间复杂度:
- 空间复杂度:
B - Robot Weight
题目链接:https://atcoder.jp/contests/abc431/tasks/abc431_b
题目大意
机器人初始重量为 ,共有 种零件,第 种零件的重量为 。初始所有零件都未安装。按顺序处理 次操作,每次给出零件编号 :若该零件当前未安装则安装它,否则拆除它,并输出操作后机器人的总重量。
思路
用一个布尔数组记录每种零件当前是否已安装。每次操作先翻转零件 的状态,再输出总重量。总重量可以直接重算为"初始重量 + 所有已安装零件的重量和",也可以维护一个当前总重变量:安装时 ,拆除时 ,这样每条查询只需 。由于 ,两种写法均可。
参考代码
#include <bits/stdc++.h>
using namespace std;
int main() {
int X, N;
cin >> X >> N;
vector<int> W(N);
for (int i = 0; i < N; ++i) cin >> W[i];
vector<bool> on(N, false);
int cur = X;
int Q;
cin >> Q;
while (Q--) {
int P;
cin >> P;
--P;
if (on[P]) {
on[P] = false;
cur -= W[P];
} else {
on[P] = true;
cur += W[P];
}
cout << cur << '\n';
}
return 0;
}复杂度分析
- 时间复杂度:
- 空间复杂度:
C - Robot Factory
题目链接:https://atcoder.jp/contests/abc431/tasks/abc431_c
题目大意
有 个头(重量 )和 个身体(重量 )。每个机器人由一个头和一个身体组成,且头重不超过身重时机器人不会倒。每个零件最多使用一次。问能否恰好组装出 个不倒的机器人。
思路
先把头、身体分别按重量升序排序:
关键结论:若存在某种合法组装方案,那么"取最轻的 个头与最重的 个身体,按序一一配对"(即第 轻的头配第 轻的选中身体,)也一定合法。因此只需检查是否对每个 都有
全部满足输出 Yes,否则输出 No。
为什么正确(交换论证):
设某个合法方案用到的 个头按重量升序为 (下标按排序后的顺序),配对的 个身体记为 。由于第 小的被用头在原序列中的位置至少是 (即 ),且 ,所以把第 个最轻的头与身体 配对仍然合法。
把 序列按重量升序重排为 ,并断言配对 仍全部合法。若不合法,存在相邻逆序对 ,我们来交换身体:因为 且原配对 合法给出 ,故 ;又 ,于是 与 均合法。反复交换即可消去所有逆序对。
最后, 元子集的第 小元素 不超过全体 个身体中第 小者 (理由:若某个 子集的第 小元素大于 ,则子集里至少有 个元素大于 ,但大于它的位置总共只有 个,矛盾)。故配对 合法。
由"合法方案存在 贪心方案合法",反过来说贪心检查失败即说明任何方案都不可能。注意 取等时也合法。
参考代码
#include <bits/stdc++.h>
using namespace std;
int main() {
int N, M, K;
cin >> N >> M >> K;
vector<long long> H(N), B(M);
for (int i = 0; i < N; ++i) cin >> H[i];
for (int i = 0; i < M; ++i) cin >> B[i];
sort(H.begin(), H.end());
sort(B.begin(), B.end());
for (int i = 0; i < K; ++i) {
if (H[i] > B[M - K + i]) {
cout << "No\n";
return 0;
}
}
cout << "Yes\n";
return 0;
}官方题解的 Python 写法同样简洁:
N, M, K = map(int, input().split())
H = sorted(map(int, input().split()))
B = sorted(map(int, input().split()))
print('Yes' if all(h <= b for h, b in zip(H[:K], B[-K:])) else 'No')复杂度分析
- 时间复杂度:(排序),检查部分
- 空间复杂度:
D - Robot Customize
题目链接:https://atcoder.jp/contests/abc431/tasks/abc431_d
题目大意
有 种零件,每种恰好一个且必须全部安装。第 种零件的重量为 ,装在头上获得幸福度 ,装在身上获得幸福度 。头的重量为所有装头零件重量之和,身的重量为所有装身零件重量之和;头重超过身重时机器人会倒。求在不倒的前提下,幸福度总和的最大值。
思路
设所有零件总重 。因为每个零件要么装头要么装身,所以 ,不倒条件 等价于
转化为 0/1 背包。 先把所有零件都放身上,总幸福度为 ;再考虑把某些零件挪到头上去。若把零件 挪到头上,幸福度变化 (可为正可为负),头重增加 。于是问题变成:
- 背包容量 ;
- 每个零件 是一个物品,重量 、价值 ;
- 在所选物品总重量不超过 的前提下,最大化价值和 ;
- 答案 。
DP 转移。 令 表示"头的总重量恰好为 时, 之和的最大值"。因为 可能为负,初始化 ,其余为 ;每个物品用倒序更新(0/1 背包避免重复选取):
由于空集始终可行(),。、,故 ,复杂度 可行。幸福度总和最大可达 ,需用 64 位整数。
参考代码
#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<int> W(N);
vector<ll> H(N), B(N);
ll sumB = 0, S = 0;
for (int i = 0; i < N; ++i) {
cin >> W[i] >> H[i] >> B[i];
sumB += B[i];
S += W[i];
}
int C = (int)(S / 2); // 头部重量上限 floor(S/2)
const ll NEG = -(1LL << 60);
vector<ll> dp(C + 1, NEG);
dp[0] = 0;
for (int i = 0; i < N; ++i) {
ll v = H[i] - B[i];
for (int j = C; j >= W[i]; --j) {
if (dp[j - W[i]] != NEG)
dp[j] = max(dp[j], dp[j - W[i]] + v);
}
}
ll best = 0;
for (int j = 0; j <= C; ++j) best = max(best, dp[j]);
cout << sumB + best << '\n';
return 0;
}复杂度分析
- 时间复杂度:,其中
- 空间复杂度:
E - Reflection on Grid
题目链接:https://atcoder.jp/contests/abc431/tasks/abc431_e
题目大意
有 的网格,每个格子摆放三种镜子之一:A(不放镜子,光直行)、B(沿主对角线摆放,即反斜杠 \)、C(沿副对角线摆放,即斜杠 /)。一束光从 左侧水平向右射入,穿过每个格子时按该格镜子类型改变方向。每次操作可以把一个格子的镜子改成另外两种之一。问最少操作多少次,才能使光最终从 的右侧射出(照到 Aoki)。多组测试。
思路
第一步:给方向编号。 用 分别表示下、右、上、左,取 、。这样光从格子 以方向 离开后进入的相邻格为 。
第二步:把镜子的反射规则用异或描述。 设光以方向 dir 进入某格子,选择方向 ndir 离开。不能 180° 折返,即必须排除 dir ^ ndir == 2。三种镜子的自然反射对应:
- 类型 A(直行):
dir ^ ndir == 0; - 类型 B(
\):右↔下、左↔上,即dir ^ ndir == 1; - 类型 C(
/):右↔上、左↔下,即dir ^ ndir == 3。
由于 dir ^ ndir 的取值只有 ,排除 之后恰好被三种镜子一一覆盖。因此:若 dir ^ ndir 恰等于当前格类型对应的异或值,光可以直接通过该格(代价 );否则需要把该格改成能实现该偏转的类型(代价 ,总能把三个类型改成需要的那个)。于是每个格子的"偏转操作"代价只可能是 或 。
第三步:建图跑 01-BFS。 定义状态 为"光离开格子 时方向为 ", 表示达到该状态所需的最小操作次数。从状态 出发,光进入相邻格 (入射方向为 ),在该格内选择出射方向 ,转移边权为 或 。所有边权只有 ,用双端队列 BFS(01-BFS)即可求出单源最短路:代价 的松弛入队首,代价 的松弛入队尾。
初始状态:光从 左侧射入,即入射方向为右(),在处理完 后得到初始 。答案取 ,即光从 向右射出。
状态数 ,每个状态至多转移 次,01-BFS 总复杂度 ;所有测试数据满足 。
参考代码
#include <bits/stdc++.h>
using namespace std;
const int INF = 1e9;
const int dx[4] = {1, 0, -1, 0};
const int dy[4] = {0, 1, 0, -1};
int mirror(char c) { // 每种镜子类型对应的 dir^ndir
if (c == 'A') return 0;
if (c == 'B') return 1;
return 3; // 'C'
}
void solve() {
int H, W;
cin >> H >> W;
vector<string> S(H);
for (auto &r : S) cin >> r;
// dp[x][y][d] = 光离开 (x,y) 方向为 d 的最小操作次数
vector<vector<array<int, 4>>> dp(H, vector<array<int, 4>>(W));
for (auto &row : dp)
for (auto &v : row) v.fill(INF);
deque<tuple<int, int, int, int>> dq; // (x, y, d, cost)
// 初始:光从 (1,1) 左侧射入,入射方向为右(1)
for (int nd = 0; nd < 4; ++nd) {
if ((1 ^ nd) == 2) continue; // 禁止折返
int c = ((1 ^ nd) == mirror(S[0][0])) ? 0 : 1;
if (c < dp[0][0][nd]) {
dp[0][0][nd] = c;
if (c == 0) dq.push_front({0, 0, nd, c});
else dq.push_back({0, 0, nd, c});
}
}
while (!dq.empty()) {
auto [x, y, d, cost] = dq.front();
dq.pop_front();
if (dp[x][y][d] != cost) continue; // 过期状态
int nx = x + dx[d], ny = y + dy[d];
if (nx < 0 || nx >= H || ny < 0 || ny >= W) continue;
for (int nd = 0; nd < 4; ++nd) {
if ((d ^ nd) == 2) continue;
int nc = cost + (((d ^ nd) == mirror(S[nx][ny])) ? 0 : 1);
if (nc < dp[nx][ny][nd]) {
dp[nx][ny][nd] = nc;
if (nc == cost) dq.push_front({nx, ny, nd, nc});
else dq.push_back({nx, ny, nd, nc});
}
}
}
cout << dp[H - 1][W - 1][1] << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T;
cin >> T;
while (T--) solve();
return 0;
}官方题解仅给出了 Python 实现(思想与本代码完全一致,此处提供可移植的 C++ 版本)。
复杂度分析
- 时间复杂度:(01-BFS,状态数 、转移数 ;多组数据总规模 )
- 空间复杂度:
F - Almost Sorted 2
题目链接:https://atcoder.jp/contests/abc431/tasks/abc431_f
题目大意
给长度为 的整数序列 和正整数 。求把 重排(重新排列后得到的不同序列 )中,满足对每个 有 的序列个数,答案对 取模。
思路
关键转化。 条件 即 :相邻两个数中,前一个不能比后一个大超过 。换句话说,一个值为 的元素前面不能紧跟着一个大于 的元素(若 放在 前面,则 ,违反条件)。
插入 DP(按值从小到大逐个插入)。 按值从小到大地把元素插进序列。假设所有值小于 的元素已经排好,现在要把 个值为 的元素插入。插入某个位置后会产生两对新的相邻关系 和 ,其中 、 是插入位置前后的元素:
- 约束 :要求 即 。由于所有已排元素都小于 ,该条件自动满足;
- 约束 :要求 。
因此插入位置合法当且仅当其后的元素 满足 。开头的空位也要看其后第一个元素是否满足;而末尾的空位没有后继元素,永远合法。
于是合法空位的个数 = "值落在 内的已排元素个数" (末尾空位)——因为每个满足 的元素正好对应其前方的一个空位。例如 ,当前序列为 ,要插入 :值在 内的元素为 共 个,它们前方各有一个合法空位,再加上末尾空位,共 个空位。
核心洞察:合法空位数只依赖计数,与当前序列的具体排列无关! 它等于
把 个不可区分的 放进这 个可区分的空位(其中 ),等价于把 个相同小球放入 个不同盒子,由隔板法知方案数为
因此答案为
正确性。 每个满足条件的序列 有唯一的构造过程(按值从小到大、每个值占据唯一的空位组合),而每一步的插入方案数只依赖计数,所以各值的选择互相独立、乘积即总数。没有出现过的值 对应 ,不改变答案,但滑动窗口仍需经过这些下标以维持窗口内容。
实现。 用滑动窗口维护 :从 扫到 ,每步先 s += cnt[v],乘上 ,然后若 就 s -= cnt[v-D]。组合数用阶乘 + 费马小定理逆元预处理(上标 )。复杂度 。
参考代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll MOD = 998244353;
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;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int N;
ll D;
cin >> N >> D;
vector<int> A(N);
int maxA = 0;
for (int i = 0; i < N; ++i) {
cin >> A[i];
maxA = max(maxA, A[i]);
}
vector<int> cnt(maxA + 1, 0);
for (int x : A) cnt[x]++;
vector<ll> fac(N + 1), ifac(N + 1);
fac[0] = 1;
for (int i = 1; i <= N; ++i) fac[i] = fac[i - 1] * i % MOD;
ifac[N] = modpow(fac[N], MOD - 2);
for (int i = N; i >= 1; --i) ifac[i - 1] = ifac[i] * i % MOD;
auto C = [&](ll n, ll k) -> ll {
if (n < 0 || k < 0 || k > n) return 0;
return fac[n] * ifac[k] % MOD * ifac[n - k] % MOD;
};
ll ans = 1, s = 0;
for (int v = 0; v <= maxA; ++v) {
s += cnt[v];
ans = ans * C(s, cnt[v]) % MOD;
if ((ll)v >= D) s -= cnt[v - (int)D];
}
cout << ans << '\n';
return 0;
}官方题解给出了相同思路的 Python 实现(滑动窗口写法一致):
MOD = 998244353
N, D = map(int, input().split())
A = list(map(int, input().split()))
lim = max(A)
cnt = [0] * (lim + 1)
for x in A:
cnt[x] += 1
fac = [1] * (N + 1)
for i in range(2, N + 1):
fac[i] = fac[i - 1] * i % MOD
ifac = [1] * (N + 1)
ifac[N] = pow(fac[N], MOD - 2, MOD)
for i in range(N, 0, -1):
ifac[i - 1] = ifac[i] * i % MOD
def C(n, k):
if n < 0 or k < 0 or k > n:
return 0
return fac[n] * ifac[k] % MOD * ifac[n - k] % MOD
ans = 1
s = 0
for v in range(lim + 1):
s += cnt[v]
ans = ans * C(s, cnt[v]) % MOD
if v >= D:
s -= cnt[v - D]
print(ans)复杂度分析
- 时间复杂度:(,)
- 空间复杂度:
G - One Time Swap 2
题目链接:https://atcoder.jp/contests/abc431/tasks/abc431_g
题目大意
给长度为 的序列 。对每一对 , 表示交换 与 后得到的序列。把所有这样的序列(共 个)按字典序排序得到 。有 次询问,每次给 ,输出任意一对满足 的 。
思路
核心观察:一次交换与 的大小关系。 对 :
- 若 ,则 (字典序更小);
- 若 ,则 ;
- 若 ,则 。
于是排序后的 被切成三段。设
则 的前 项都小于 ,接下来 项都等于 ,最后 项都大于 。其中 就是序列的逆序对数,,。根据 落在哪一段,分别处理。
小于 的一段内部的字典序排序。 对两个都满足 、 的对 、, 与 的字典序关系恰等于三元组 与 的字典序关系。证明分三类(假设 ):
- 若 :前 位相同,第 位分别是 与 ,而 ,故 更小;
- 若 且 :第 位分别是 、,顺序由两者大小决定;
- 若 且 (此时 ):前 位相同,第 位分别是 与 ,因 ,故 更大,即 较大的排前面。
大于 的一段同理,排序对应三元组 ;等于 的一段所有序列相同,输出任意一对 即可。
算法流程。 需要一个支持"求当前集合中第 小"与"统计小于某个值的元素个数"的数据结构。这里用树状数组 + 坐标压缩实现(把 压成排名,再用树状数组求前缀和与二分查找第 小),完全可移植。
- 小于 的一段:把询问按 升序排序。从左到右扫描 ,数据结构维护所有 的 。对于固定 ,其贡献的 类对按 升序排列;落在该块内的询问,取其块内 0-indexed 位置 ,直接查第 小即可得到 ,从而 也随之确定(相等的 中 大的在前,正是 小的在前)。
- 等于 的一段:预处理任意一对 。
- 大于 的一段:从右到左扫描 ,数据结构维护所有 的 。固定 时 的那些 是该块的候选,按 升序排列;注意需要跳过值不超过 的元素(它们在排名序列的最前面),即取第 小,其中 是值 的元素个数。
总复杂度 。
参考代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
struct BIT {
int n;
vector<int> bit;
BIT(int n = 0) : n(n), bit(n + 1, 0) {}
void add(int i, int v) { // i: 1-indexed
for (; i <= n; i += i & -i) bit[i] += v;
}
int sum(int i) const {
int s = 0;
for (; i > 0; i -= i & -i) s += bit[i];
return s;
}
int kth(int k) const { // 最小的 i 使得前缀和 >= k (k >= 1)
int idx = 0, step = 1;
while ((step << 1) <= n) step <<= 1;
for (; step; step >>= 1) {
int nxt = idx + step;
if (nxt <= n && bit[nxt] < k) {
idx = nxt;
k -= bit[nxt];
}
}
return idx + 1;
}
};
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int N, Q;
cin >> N >> Q;
vector<int> A(N);
for (int &x : A) cin >> x;
vector<pair<ll, int>> qs; // (k-1, 查询编号),按 k 升序处理
for (int i = 0; i < Q; ++i) {
ll k;
cin >> k;
qs.push_back({k - 1, i});
}
sort(qs.begin(), qs.end());
// cntVal[v] = v 的个数,posLt[v] = 值小于 v 的元素总个数
vector<int> cntVal(N + 1, 0);
for (int x : A) cntVal[x]++;
vector<int> posLt(N + 1, 0);
for (int v = 1; v <= N; ++v) posLt[v] = posLt[v - 1] + cntVal[v - 1];
ll D = 0, E = 0;
{ // 逆序对个数 = D
BIT fw(N);
for (int i = 0; i < N; ++i) {
D += i - fw.sum(A[i]);
fw.add(A[i], 1);
}
}
for (int v = 1; v <= N; ++v) E += 1LL * cntVal[v] * (cntVal[v] - 1) / 2;
ll DE = D + E;
vector<pair<int, int>> ans(Q);
pair<int, int> eqPair = {-1, -1};
{ // 相等段:任意一对 A_l == A_r (l < r)
vector<int> last(N + 1, -1);
for (int i = 0; i < N; ++i) {
if (last[A[i]] != -1) eqPair = {last[A[i]], i};
last[A[i]] = i;
}
}
// 两套坐标压缩:(A_j, -j) 用于"小于 A"段,(A_j, j) 用于"大于 A"段
vector<pair<int, int>> pd, pu;
for (int j = 0; j < N; ++j) pd.push_back({A[j], -j});
for (int j = 0; j < N; ++j) pu.push_back({A[j], j});
sort(pd.begin(), pd.end());
sort(pu.begin(), pu.end());
vector<int> rkD(N), rkU(N);
for (int r = 0; r < N; ++r) rkD[-pd[r].second] = r + 1; // 1-indexed
for (int r = 0; r < N; ++r) rkU[pu[r].second] = r + 1;
int ptr = 0;
// ---------- 小于 A 的一段(位置 k-1 ∈ [0, D)) ----------
{
BIT fw(N);
for (int j = 0; j < N; ++j) fw.add(rkD[j], 1);
ll cnt = 0;
for (int l = 0; l < N; ++l) {
fw.add(rkD[l], -1); // 只保留 j > l
int lim = fw.sum(posLt[A[l]]); // j>l 且 A_j < A_l 的个数
while (ptr < Q && qs[ptr].first < cnt + lim) {
int pos = (int)(qs[ptr].first - cnt); // 块内 0-indexed
int r = -pd[fw.kth(pos + 1) - 1].second;
ans[qs[ptr].second] = {l + 1, r + 1};
++ptr;
}
cnt += lim;
}
}
// ---------- 等于 A 的一段(位置 k-1 ∈ [D, DE)) ----------
while (ptr < Q && qs[ptr].first < DE) {
ans[qs[ptr].second] = {eqPair.first + 1, eqPair.second + 1};
++ptr;
}
// ---------- 大于 A 的一段(位置 k-1 ∈ [DE, M)) ----------
{
BIT fw(N);
ll cnt = 0;
for (int l = N - 1; l >= 0; --l) {
int le = fw.sum(posLt[A[l]] + cntVal[A[l]]); // j>l 且 A_j <= A_l 的个数
int lim = fw.sum(N) - le; // j>l 且 A_j > A_l 的个数
while (ptr < Q && qs[ptr].first < DE + cnt + lim) {
int pos = (int)(qs[ptr].first - DE - cnt); // 块内 0-indexed
int r = pu[fw.kth(le + pos + 1) - 1].second; // 跳过值 <= A_l 的前缀
ans[qs[ptr].second] = {l + 1, r + 1};
++ptr;
}
cnt += lim;
fw.add(rkU[l], 1); // 为更小的 l 准备
}
}
for (auto &[l, r] : ans) cout << l << ' ' << r << '\n';
return 0;
}复杂度分析
- 时间复杂度:
- 空间复杂度: