AtCoder Beginner Contest 469 题解
AtCoder Beginner Contest 469 题解
| 项目 | 内容 |
|---|---|
| 比赛时间 | 2026-08-01 |
| 比赛链接 | https://atcoder.jp/contests/abc469 |
| 题目数量 | A–G 共 7 题 |
比赛概览
本场整体难度中等偏易,前四题(A–D)属于入门到普及的难度,后三题逐渐拉开差距。E 题是"二分答案 + 前缀和 + 双指针"的经典综合题;F 题考察最大生成树的 Kruskal 优化与调和级数复杂度分析;G 题则是一道结合二面体群(dihedral group)与矩阵快速幂的难题。F、G 两题都强调"把抽象变换编码为可快速计算的结构",是本次最有区分度的题目,值得重点掌握。
A - Train Car
题目链接:https://atcoder.jp/contests/abc469/tasks/abc469_a
题目大意
一列火车有 节车厢。求从车头数第 节车厢,从车尾数起是第几节。
思路
从车头给车厢依次编号 ,则第 节车厢的编号为 。它后面还有编号 共 节车厢,因此从车尾数它是第 节。
答案即 。这是最直接的算术题,注意从"从前往后第 K"到"从后往前第几"的换算关系即可。
参考代码
#include <bits/stdc++.h>
using namespace std;
int main(){
int N, K;
cin >> N >> K;
cout << N - K + 1 << '\n';
return 0;
}N, K = map(int, input().split())
print(N - K + 1)复杂度分析
- 时间复杂度:
- 空间复杂度:
B - Isolated Seats
题目链接:https://atcoder.jp/contests/abc469/tasks/abc469_b
题目大意
有 把椅子从左到右排成一排,给定长度为 的字符串 :第 个字符为 o 表示第 把椅子有人坐,为 x 表示没人坐。求满足以下全部条件的椅子数量:
- 这把椅子本身没有人坐;
- 它的左边没有椅子,或者左边的椅子没有人坐;
- 它的右边没有椅子,或者右边的椅子没有人坐。
思路
直接枚举每把椅子,检查它自身及左右两侧是否都为空即可。,暴力检查完全没有问题。
为避免对最左、最右两把椅子单独写边界判断,可以使用"哨兵"技巧:在 左右各补一个 x,得到 。这样原来首尾的椅子在 中也有了虚拟的空邻居,只需对中间 个位置统一判断 、、 是否全部为 x。
这个"补哨兵统一边界"的想法在更复杂的题目(如双指针、区间问题)中经常能显著减少实现量。
参考代码
#include <bits/stdc++.h>
using namespace std;
int main(){
int N; string S;
cin >> N >> S;
string T = "x" + S + "x"; // 左右各补一个空椅子
int ans = 0;
for(int i = 1; i <= N; i++){
if(T[i-1] == 'x' && T[i] == 'x' && T[i+1] == 'x') ans++;
}
cout << ans << '\n';
return 0;
}复杂度分析
- 时间复杂度:
- 空间复杂度:
C - Cantrip
题目链接:https://atcoder.jp/contests/abc469/tasks/abc469_c
题目大意
有 个袋子排成一排,每个袋子里有一颗糖,袋子上写着 o(hit)或 x(miss)。对每个 ,高桥君先拿走队伍最前面的 个袋子,吃掉里面的糖并保留袋子;然后只要队伍中还有袋子、且他手中还有写着 o 的袋子,就重复:丢弃一个写着 o 的袋子,拿走队伍最前面的袋子,吃掉糖并保留它。求每种 下他能吃到的糖的总数。
思路
关键观察:高桥君手中始终恰好持有 个袋子——每执行一次动作,丢弃一个、又拿进一个,数量不变。
他只会丢弃写着 o 的袋子,绝不会丢弃 x 袋子,因此他手中的 x 袋子数恰好等于"所有已拿到的袋子中 x 的总数"。设已拿到的袋子中有 个 x,则手中有 个 x 袋和 个 o 袋。只要 (手中还有 o 可丢弃)且队伍未空,他就能继续。
所以过程停止当且仅当下面两条之一成立:
- 他已拿到 个
x袋子(此时手中全是x,无可丢弃的o); - 队伍被清空。
由此得到简洁的结论:
- 若整串 中
x的总数小于 ,则永远凑不齐 个x,他会一直拿到队伍空,即拿走全部 个袋子,答案为 ; - 否则,设 为 中第 个
x所在的位置,他会一直拿到并包括第 个x为止,答案为 。
吃掉糖的数量等于收到的袋子数量(每收到一袋必吃一颗),所以答案就是上述拿到的袋数。预处理所有 x 的位置后,每个 的答案 可得。
参考代码
#include <bits/stdc++.h>
using namespace std;
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
int N; string S;
cin >> N >> S;
vector<int> xpos;
for(int i = 0; i < N; i++)
if(S[i] == 'x') xpos.push_back(i + 1); // 1-indexed
int total = (int)xpos.size();
for(int k = 1; k <= N; k++){
if(k > total) cout << N << '\n'; // x 不够 k 个,全拿完
else cout << xpos[k-1] << '\n'; // 拿到第 k 个 x
}
return 0;
}复杂度分析
- 时间复杂度:
- 空间复杂度:
D - The Big Two
题目链接:https://atcoder.jp/contests/abc469/tasks/abc469_d
题目大意
有 名玩家 ,举办了 次比赛,第 次进入决赛的两名玩家为 。求满足 ,且"每一次比赛中, 和 至少有一人进入决赛"的二元组 的数量。
思路
条件等价于:对每个 都有
关键观察:第一次比赛的两位决赛选手 、 中, 与 至少有一个必须等于 或 (否则第一场比赛就无法命中)。因此所有合法二元组必定含有 或 ,我们只需分别统计"含 的合法对"与"含 的合法对",再减去重复计数的部分。
以 为例,考虑所有能与 组成合法对的伙伴 :
- 若 出现在全部 次比赛的决赛中,那么任意 都能与 构成合法对(因为每场比赛都靠 命中),数量为 ;
- 否则,设 为 缺席的比赛集合。对这些比赛,必须依靠 来命中,即 。故 必须属于所有这些集合的交集
合法的 数量为 。
对 完全同理得到 。
记 为"含 的合法对"集合, 为"含 的合法对"集合。答案即 。注意到: 中的对必含 , 中的对必含 ,因此同时属于两者的对必然同时含 和 ,即只有可能是 这一对。于是
其中 为指示函数。判断 是否合法只需检查每一场比赛是否至少包含 、 之一。
实现要点:每个集合大小至多为 ,求交集只需线性扫描一遍比赛记录,总复杂度 。注意去重,避免 被同时计入 和 。
参考代码
#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<pair<int,int>> t(M);
vector<int> cnt(N + 1, 0);
for(int i = 0; i < M; i++){
int a, b; cin >> a >> b;
t[i] = {a, b};
cnt[a]++; cnt[b]++;
}
int A = t[0].first, B = t[0].second;
// 与 x 组成合法对的伙伴数量
auto partners = [&](int x) -> ll {
if(cnt[x] == M) return (ll)N - 1; // x 每场都进决赛
vector<int> C;
bool init = false;
for(auto [u, v] : t){
if(u != x && v != x){ // x 缺席的比赛
if(!init){ C = {u, v}; init = true; }
else{
vector<int> nxt;
for(int c : C) if(c == u || c == v) nxt.push_back(c);
C = nxt;
}
}
}
return (ll)C.size();
};
ll pa = partners(A), pb = partners(B);
bool validAB = true; // {A,B} 是否合法
for(auto [u, v] : t)
if(u != A && u != B && v != A && v != B){ validAB = false; break; }
cout << pa + pb - (validAB ? 1LL : 0LL) << '\n';
return 0;
}复杂度分析
- 时间复杂度:
- 空间复杂度:
E - Pro Exam Eligibility
题目链接:https://atcoder.jp/contests/abc469/tasks/abc469_e
题目大意
给定长度为 、由 o/x 组成的字符串 ,表示 场比赛的结果(o 为胜)。保证 中 o 的个数至少为 。高桥君选择区间 (),要求其中 o 的个数不少于 。求在此约束下,区间内胜率(胜场数除以区间长度)的最大值。
思路
最大化一个"比值"问题,典型的做法是二分答案:二分胜率 ,判断是否存在"胜场 且胜率 "的区间。
判定条件的转化
对固定的 ,构造数组
则区间 的胜率为 ,而
所以区间胜率 当且仅当 。
设前缀和 (),则该条件等价于
枚举右端点
对每个右端点 ,我们要判断是否存在合法的 。由于胜场数 随 增大而单调不增,所有满足"胜场 "的 恰好构成一个前缀区间 ,其中 是最大的合法左端点。
可以用双指针(滑动窗口) 求出:维护窗口 内的胜场数 ,每加入一个新字符 ,就不断尝试右移 :只要把 移出窗口后胜场仍 ,就右移。最终得到的 即为 (若 说明不存在合法左端点,跳过该 )。 随 单调不降,双指针的均摊复杂度为 。
判断
对每个有合法 的 ,需要判断是否存在 使 ,即
预计算前缀最小值 ,即可 判断 。
二分
胜率 的存在性关于 单调递减( 越小越容易满足),因此在 上二分,二分约 80 轮即可把精度压到 以下,远超题目要求的 。
参考代码
#include <bits/stdc++.h>
using namespace std;
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
int N, K; string S;
cin >> N >> K >> S;
const double EPS = 1e-12;
auto check = [&](double p) -> bool {
// 前缀和 P 及其前缀最小值 mn
vector<double> pref(N + 1, 0.0);
for(int i = 1; i <= N; i++){
double A = (S[i-1] == 'o') ? (1.0 - p) : (-p);
pref[i] = pref[i-1] + A;
}
vector<double> mn(N + 1);
mn[0] = pref[0];
for(int i = 1; i <= N; i++) mn[i] = min(mn[i-1], pref[i]);
// 双指针求每个 r 的最大合法左端点 l_r
int l = 1, w = 0;
for(int r = 1; r <= N; r++){
if(S[r-1] == 'o') w++;
while(l < r && w - (S[l-1] == 'o' ? 1 : 0) >= K){
if(S[l-1] == 'o') w--;
l++;
}
if(w >= K && mn[l-1] <= pref[r] + EPS) return true;
}
return false;
};
double lo = 0.0, hi = 1.0;
for(int it = 0; it < 80; it++){
double mid = (lo + hi) / 2;
if(check(mid)) lo = mid;
else hi = mid;
}
cout << fixed << setprecision(15) << lo << '\n';
return 0;
}复杂度分析
- 时间复杂度:,其中 是二分轮数(此处取 80,为常数)
- 空间复杂度:
F - GCD Maximum Spanning Tree
题目链接:https://atcoder.jp/contests/abc469/tasks/abc469_f
题目大意
给定长度为 的严格递增正整数序列 。在 个点上构造带权完全无向图,点 与点 ()之间的边权为 。求该图最大生成树的边权和。
思路
朴素做法是对 条边跑 Kruskal,显然不可行。注意到所有边权都 ,并且 有很好的倍数结构,可以按权值级别批量处理。
维护一个并查集(DSU)。从大到小枚举 ,对每个 :
- 枚举所有满足 的下标 ;
- 把这些下标依次与第一个下标合并(union)。
为什么正确? 设 且 ,则 ,所以 。
- 若 ,则这两点在处理到更大的权值级别时(即 那一轮)已经被合并进同一连通块,现在再合并是无效操作;
- 若 ,则这条边正是在权值 级别上 Kruskal 会考虑的边,合并它恰好就是 Kruskal 的动作。
因此,按此流程执行,成功的合并次数恰好为 ,每次成功合并都对应最大生成树中的一条边,其权重就是当前的 。把所有成功合并时累加的 加起来即为答案。
复杂度:预处理 记录值为 的下标( 中元素互异)。对每个 ,枚举其不超过 的倍数,总枚举次数为
(调和级数)。并查集操作共 ,足以通过 。
参考代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
struct DSU {
vector<int> p, sz;
DSU(int n) : p(n), sz(n, 1) { iota(p.begin(), p.end(), 0); }
int find(int x){ return p[x] == x ? x : p[x] = find(p[x]); }
bool unite(int a, int b){
a = find(a); b = find(b);
if(a == b) return false;
if(sz[a] < sz[b]) swap(a, b);
p[b] = a; sz[a] += sz[b];
return true;
}
};
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
int N; cin >> N;
vector<int> A(N);
int MAXV = 0;
for(int i = 0; i < N; i++){ cin >> A[i]; MAXV = max(MAXV, A[i]); }
vector<int> pos(MAXV + 1, -1);
for(int i = 0; i < N; i++) pos[A[i]] = i;
DSU dsu(N);
ll ans = 0;
for(int g = MAXV; g >= 1; g--){
int first = -1;
for(int m = g; m <= MAXV; m += g){
if(pos[m] != -1){
if(first == -1) first = pos[m];
else if(dsu.unite(first, pos[m])) ans += g;
}
}
}
cout << ans << '\n';
return 0;
}复杂度分析
- 时间复杂度:
- 空间复杂度:
G - K-nacci Operations
题目链接:https://atcoder.jp/contests/abc469/tasks/abc469_g
题目大意
给定 个由 a、b 组成的字符串 。对整数 ,定义 为 依序拼接而成的字符串。对字符串 依次执行 中每个字符对应的操作:
- 若该字符为
a,把 的第一个字符移到末尾(即循环左移一位); - 若该字符为
b,把 整体反转。
求执行完所有操作后的 。其中 ,,,。
思路
的长度呈 K-元斐波那契式的爆炸增长,根本不可能显式构造。但有一个关键洞察:
任意一串由
a、b组成的操作作用到 上,结果只可能是 的某个循环移位,或 的某个循环移位再反转。
这正是二面体群 ()的结构。于是我们可以把一串操作压缩成二元组 :
- :结果是 循环左移 位( 对 取模);
- :结果是 先循环左移 位、再整体反转。
单串效果的快速计算
对单个字符:
a:若当前 ,则 ;若 ,则 (均模 )。 不变;b:只把 翻转, 不变。
因此每个基础串 ()的效果 可在 内算出。
拼接的组合规则
若依次作用字符串 、(等价于字符串 的作用),组合后 满足:
由此,对 ,把 从左到右逐段组合,得到递推:
(组合时 的符号由"到目前为止已反转次数的奇偶性"决定,即前面各段 值之和模 2。)
周期性质与矩阵加速
先看 :对 ,
故 以 为周期。而 的递推系数只依赖 ,所以这些系数()关于下标 也以 为周期。
把 序列按每 个一组分块,令
由于块内任意元素 的递推只用得到当前块之前已算出的值,存在一个 的常矩阵 ,使得
的第 行正是 对前一块中 个值的线性组合系数,按 从小到大构造即可(某项落回当前块时,代入已经构造好的那一行)。
于是
用矩阵快速幂计算,全程对 取模。 直接用周期:。
最后根据 对 做对应变换并输出。
总复杂度 。 时矩阵乘法约 次运算,,完全可行。
参考代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
int MOD; // 模 M,即 |T|
vector<ll> matVec(const vector<vector<ll>>& A, const vector<ll>& v){
int n = A.size();
vector<ll> r(n, 0);
for(int i = 0; i < n; i++){
ll s = 0;
for(int j = 0; j < n; j++) s = (s + A[i][j] * v[j]) % MOD;
r[i] = s;
}
return r;
}
vector<vector<ll>> matMul(const vector<vector<ll>>& A, const vector<vector<ll>>& B){
int n = A.size();
vector<vector<ll>> R(n, vector<ll>(n, 0));
for(int i = 0; i < n; i++)
for(int k = 0; k < n; k++) if(A[i][k]){
ll aik = A[i][k];
for(int j = 0; j < n; j++)
R[i][j] = (R[i][j] + aik * B[k][j]) % MOD;
}
return R;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
int K; cin >> K;
vector<string> S(K);
for(int i = 0; i < K; i++) cin >> S[i];
ll N; cin >> N;
string T; cin >> T;
int M = (int)T.size();
MOD = M;
int P = K + 1;
// 基础串的效果 (a_i, b_i), i = 1..K
vector<ll> a(P + 1, 0);
vector<int> b(P + 1, 0);
for(int i = 1; i <= K; i++){
ll av = 0; int bv = 0;
for(char c : S[i-1]){
if(c == 'a'){
if(bv == 0) av = (av + 1) % M;
else av = (av - 1 + M) % M;
} else bv ^= 1;
}
a[i] = av; b[i] = bv;
}
// 求 (a_{K+1}, b_{K+1}):S_{K+1} = S_K + S_{K-1} + ... + S_1
{
ll xa = a[K]; int xb = b[K];
for(int j = K - 1; j >= 1; j--){
if(xb == 0){
xa = (xa + a[j]) % M;
xb = b[j];
} else {
xa = (xa - a[j] % M + M) % M;
xb = (1 + b[j]) % 2;
}
}
a[P] = xa; b[P] = xb;
}
// 按周期 P 取 b 值
auto getB = [&](ll x) -> int { return b[((x - 1) % P) + 1]; };
// 构造转移矩阵 A:块 -> 下一块
vector<vector<ll>> A(P, vector<ll>(P, 0));
vector<vector<ll>> rows; // 当前块内已构造好的行
for(int m = 1; m <= P; m++){
int n = P + m; // 当前块第 m 个元素的下标
vector<ll> row(P, 0);
for(int j = 1; j <= K; j++){
int c = 1;
if(j >= 2){ // 符号由前面各段 b 之和决定
int s = 0;
for(int tt = 1; tt <= j - 1; tt++) s ^= getB(n - tt);
if(s == 1) c = -1;
}
ll idx = n - j; // 递推用到的位置
if(idx >= P + 1){ // 落回当前块,用已构造的行
int rr = (int)(idx - P - 1);
for(int col = 0; col < P; col++){
row[col] = (row[col] + c * rows[rr][col]) % M;
if(row[col] < 0) row[col] += M;
}
} else { // 落回前一块,即单位向量
int col = (int)(idx - 1);
row[col] = (row[col] + c) % M;
if(row[col] < 0) row[col] += M;
}
}
rows.push_back(row);
A[m - 1] = row;
}
// v0 = 第一块
vector<ll> v(P);
for(int i = 1; i <= P; i++) v[i - 1] = a[i];
// v = A^t * v0
ll t = (N - 1) / P;
int m = (int)((N - 1) % P);
if(t > 0){
vector<vector<ll>> base = A;
ll e = t;
while(e > 0){
if(e & 1) v = matVec(base, v);
base = matMul(base, base);
e >>= 1;
}
}
ll aN = v[m];
int bN = getB(N);
// 按 (aN, bN) 变换 T
string res;
res.reserve(M);
for(int i = 0; i < M; i++) res += T[(i + aN) % M]; // 左移 aN 位
if(bN == 1) reverse(res.begin(), res.end()); // 再整体反转
cout << res << '\n';
return 0;
}复杂度分析
- 时间复杂度:
- 空间复杂度: