AtCoder Beginner Contest 435 题解
AtCoder Beginner Contest 435 题解
| 项目 | 内容 |
|---|---|
| 比赛时间 | 2025-12-06 |
| 比赛链接 | https://atcoder.jp/contests/abc435 |
| 题目数量 | A–G 共 7 题 |
比赛概览
本场整体难度平稳,A、B 为签到题;C 是经典的单向连锁反应扫描,注意 高达 ;D 考察"反图 + 在线传播"的均摊思想;E 是区间覆盖的经典题,用有序集合维护白色连续段即可。F 是本场第一个有明显区分度的题,需要把"拆塔运动"归约到笛卡尔树上的树形 DP;G 是压轴计数题,核心是把递推 用"懒标记 + 进入/离开事件"批量加速到 。建议重点掌握 E、F、G 的处理手法。
A - Triangular Number
题目链接:https://atcoder.jp/contests/abc435/tasks/abc435_a
题目大意
给定正整数 (),输出 。
思路
这是最直接的求和问题,两种做法均可:
- 用循环累加 到 ;
- 用等差数列求和公式
由于 ,两种做法都轻而易举;用公式时注意 最大为 ,用 int 即可,但写成 long long 更稳妥。
参考代码
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n;
cout << 1LL * n * (n + 1) / 2 << '\n';
return 0;
}官方题解给出了循环写法,也一并附上(Python):
n = int(input())
ans = 0
for i in range(1, n + 1):
ans += i
print(ans)复杂度分析
- 时间复杂度:(循环)或 (公式)
- 空间复杂度:
B - No-Divisible Range
题目链接:https://atcoder.jp/contests/abc435/tasks/abc435_b
题目大意
给定长度为 ()的正整数序列 ,统计满足下列条件的整数对 ()的个数:
对区间内每一个 (), 不是 的约数。
思路
关键观察:对于正整数 ," 是 的约数"等价于" 除以 的余数为 "。因此对一个固定的 :
- 先求出区间和 ;
- 再逐个检查 :若存在某个 使得 ,则 不合法;否则合法。
,候选区间总数只有 个,对每个区间求和与检查都是 ,总复杂度 ,绰绰有余。
易错点:当 时, 是任何正整数的约数,所以任何包含 的区间必然不合法(如样例 2)。这是自然被余数判据覆盖的,无需特判,但要意识到这一点以防手算时出错。
参考代码
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n;
vector<int> a(n);
for (int i = 0; i < n; i++) cin >> a[i];
int ans = 0;
for (int l = 0; l < n; l++) {
for (int r = l; r < n; r++) {
int s = 0;
for (int i = l; i <= r; i++) s += a[i];
bool ok = true;
for (int i = l; i <= r; i++) {
if (s % a[i] == 0) { ok = false; break; }
}
if (ok) ans++;
}
}
cout << ans << '\n';
return 0;
}官方题解 Python 版:
n = int(input())
a = list(map(int, input().split()))
ans = 0
for i in range(n):
for j in range(i + 1):
s = 0
for k in range(j, i + 1):
s += a[k]
flag = True
for k in range(j, i + 1):
if s % a[k] == 0:
flag = False
if flag:
ans += 1
print(ans)复杂度分析
- 时间复杂度:
- 空间复杂度:(存序列)
C - Domino
题目链接:https://atcoder.jp/contests/abc435/tasks/abc435_c
题目大意
数轴上有 个多米诺骨牌,第 个骨牌位于坐标 、高度 。当第 个骨牌向右倒下时,区间 内的所有骨牌都会向右倒。若第 个骨牌向右倒,最终一共有多少个骨牌倒下?
(,)
思路
关键观察:连锁反应只会从左向右单调推进。因为每个骨牌只能使右侧的骨牌倒下,所以一旦某个骨牌 没有倒下,那么它右侧的所有骨牌都不可能倒下——连锁已经终止。
算法:维护变量 表示"目前最右倒下的坐标"。初始时第 个骨牌自己倒下,故 。从左到右扫描 :
- 若 ,说明第 个骨牌未倒下,连锁到此结束,答案为 ;
- 否则第 个骨牌已经倒下,它倒下会波及到坐标 ,更新 。
为什么正确:任意时刻所有已倒下的骨牌都落在 内(归纳:第 个骨牌倒下时它在 内,且它的波及范围全部纳入 )。由于倒下传播是单调的, 达到某个值后不再增长,扫描到 时即可确认不会再有任何骨牌倒下。
边界情况: 可能超过 (因为 但 也可以较大),需要与 取 ,防止越界。
参考代码
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n;
vector<int> a(n + 1);
for (int i = 1; i <= n; i++) cin >> a[i];
int R = 1; // 最右倒下的坐标,初始为第 1 个
for (int i = 1; i <= n; i++) {
if (i > R) break; // 第 i 个未倒下,连锁终止
R = max(R, min(n, i + a[i] - 1));
}
cout << R << '\n';
return 0;
}官方题解 Python 版(注意其中坐标从 开始):
N = int(input())
A = list(map(int, input().split()))
crr = 1 # 坐标小于 crr 的骨牌都已倒下
for i in range(N):
if i >= crr: # 骨牌 i 未倒下,连锁结束
print(i)
exit()
crr = max(crr, i + A[i])
print(N)复杂度分析
- 时间复杂度:
- 空间复杂度:
D - Reachability Query 2
题目链接:https://atcoder.jp/contests/abc435/tasks/abc435_d
题目大意
给定一个 个点、 条边的有向图,第 条边为 。初始所有点为白色。按顺序处理 个查询:
1 v:把点 染黑;2 v:询问从点 出发,沿有向边能否到达某个黑点。
()
思路
关键转化:把图中所有边反向,得到反图 。原图中"从 能到达黑点 "等价于反图中"从黑点 能到达 "。因此查询 2 v 就变成:在反图中, 是否"从某个黑点可达"。
维护标记:设 表示"反图中 是否从某个黑点可达"。查询 2 v 直接输出 , 完成。
查询 1 v(把 染黑):在反图中从 出发做 BFS/DFS,把经过的所有点标记为"可达"()。关键是只在 从 false 变成 true 时才继续扩展它的出边。
为什么均摊 :若 已经为 true,说明从之前的黑点已经能到达 ,那么 能到达(反图中)的所有点必然也已经被标记过,此时染色 不会带来任何新信息,直接跳过 BFS。每个点至多被"首次标为 true"一次,而 BFS 只会在首次标 true 时扫描它的出边,因此每条边在整个过程中至多被遍历一次。总的复杂度为 。
易错点:注意 BFS 是在反图上进行的,即从 出发访问的是所有"在原图中指向 的点"。
参考代码
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
cin >> n >> m;
vector<vector<int>> radj(n + 1); // 反图
for (int i = 0; i < m; i++) {
int x, y;
cin >> x >> y;
radj[y].push_back(x); // 原图 x->y,反图中 y->x
}
int q;
cin >> q;
vector<char> reach(n + 1, 0); // 反图中是否从黑点可达
queue<int> que;
while (q--) {
int t, v;
cin >> t >> v;
if (t == 1) {
if (reach[v]) continue; // 已可达,染色无新信息
reach[v] = 1;
que.push(v);
while (!que.empty()) {
int u = que.front();
que.pop();
for (int w : radj[u]) {
if (!reach[w]) {
reach[w] = 1;
que.push(w);
}
}
}
} else {
cout << (reach[v] ? "Yes" : "No") << '\n';
}
}
return 0;
}复杂度分析
- 时间复杂度:(每个点、每条边至多被处理一次)
- 空间复杂度:
E - Cover query
题目链接:https://atcoder.jp/contests/abc435/tasks/abc435_e
题目大意
有 个格子排成一行(),初始全部为白色。依次处理 个查询(),第 个查询给出 ,把 内所有格子染黑(白变黑,黑保持不变),并输出当前白色格子的总数。
思路
高达 ,逐格处理不可行,但 ,因此只需要维护白色格子的连续段(区间)。
状态表示:用一个有序集合维护当前所有白色连续段 ,初始为 ;另外用变量 维护白色格子的总数。
处理查询 :
- 找到第一个满足 的白色段(用有序集合的
lower_bound定位); - 依次处理所有与 相交的白色段(即 的段):
- 整段被覆盖( 且 ):直接删除该段;
- 左侧露出():删除原段,插入残段 ;
- 右侧露出():删除原段,插入残段 ;
- 两侧都露出:删除原段,插入两个残段;
- 每删除一个段,就从 中减去它与 的交集长度 ;
- 输出 。
为什么均摊高效:每个查询至多新增 个残段(左侧露出 + 右侧露出),而被整段删除的白色段数量在全部查询中总和为 (因为每新增一个段最多被删除一次)。因此总操作次数为 ,每次 lower_bound/插入/删除都是 ,总复杂度 。
实现细节:集合中存二元组 (右端点在前面),这样 lower_bound({L, -1}) 就能直接找到第一个 的段;从该段起向后扫描,直到 停止。
参考代码
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, q;
cin >> n >> q;
set<pair<int, int>> st; // 存 (r, l) 表示白色段 [l, r]
st.insert({n, 1});
long long white = n;
while (q--) {
int L, R;
cin >> L >> R;
auto it = st.lower_bound({L, -1}); // 第一个 r >= L 的段
vector<pair<int, int>> add;
while (it != st.end()) {
int l = it->second, r = it->first;
if (R < l) break; // 之后的段都与 [L,R] 不相交
// 交叠部分被染黑
white -= min(r, R) - max(l, L) + 1;
if (l < L) add.push_back({L - 1, l}); // 左侧残段 [l, L-1]
if (R < r) add.push_back({r, R + 1}); // 右侧残段 [R+1, r]
it = st.erase(it);
}
for (auto &p : add) st.insert(p);
cout << white << '\n';
}
return 0;
}复杂度分析
- 时间复杂度:(均摊)
- 空间复杂度:(白色段数量有界)
F - Cat exercise
题目链接:https://atcoder.jp/contests/abc435/tasks/abc435_f
题目大意
有 个猫塔从左到右排成一行,第 座塔高 ,且 是 的一个排列。两塔之间距离定义为坐标之差的绝对值 。猫初始在高度 的塔顶。
可以反复选择并移除塔。移除塔时猫的行为如下:
- 若移除的不是猫所在塔,猫不动;
- 若移除的是猫所在塔,且该塔左/右侧还有相邻塔,则猫跳到"从当前位置只沿相邻塔移动所能到达的塔中最高的一座"(去掉被移除的那座),移动距离为两塔坐标差;
- 若移除猫所在塔时其左右都没有相邻塔,猫跳进怀抱,游戏结束。
注意:被移走的塔留下的空隙不会被填补,即塔的相邻关系始终是初始时的左右邻居。求在游戏结束前,猫移动总距离的最大值。
思路
这是本场最有难度的一道题,核心是把过程归约到笛卡尔树上的树形 DP。
第 1 步:状态与关键不变式。 记状态 表示"猫在塔 上,且当前仍立着、从 出发沿相邻关系可达的塔恰好是连续区间 "()。关键不变式是:猫在塔 上时, 内所有塔的高度都不超过 。理由:猫最初在全局最高的塔上,而它每次落脚的都是"当前可达区间中最高的塔",之后区间的收缩(移除塔)不会破坏这一性质。
第 2 步:定义区间 。 对每个塔 ,定义 为包含 的最大区间,使得其中所有塔高 ,即左右分别被最近的更高塔挡住:
- :若左侧存在 且 ,则 等于最大的这样的 加 ,否则 ;
- :对称地,若右侧存在 且 ,则 等于最小的这样的 减 ,否则 。
由不变式可知,任何从初始状态可达的状态 都满足 。
第 3 步:归约到状态 。 从 出发,我们可以先把 中的塔全部移除(猫不动,移动距离不增加),从而到达 。因此
其中 表示从该状态到游戏结束的最大移动距离。于是只需考虑状态 。
第 4 步:只保留两种转移。 在状态 中,若要猫移动,必须移除塔 ,猫会跳到"剩余可达塔中最高的那座" 。可行的 必须满足:,且 与 之间的塔(不含 本身)高度都不超过 (否则 不是剩余塔中最高的)。
可以证明:只需考虑每侧最高的塔:
理由如下(以左侧为例)。先验证两个事实:
- 若 是可行的转移目标,则 。这是因为 是 中的最高塔,而 内所有塔都不高于 ,且 ,从而 、。
- 从 出发,可以"收缩区间"到达 (不断移除不包含 那一侧的塔、移走当前塔,沿笛卡尔树一步步逼近 ),于是 ,其中 为移动距离之和,由三角不等式知 。
于是对任意左侧可行目标 有
即 的方案不会优于 的方案;右侧同理。所以 DP 转移只需考虑 与 两个方向:
当 或 不存在时忽略对应项;若 (没有任何可达的其他塔),则 。
第 5 步:这就是笛卡尔树。 如果对每个 连边 、(存在时),得到的图恰好是一棵最大笛卡尔树:每个节点的高度大于其子节点高度,根是高度 的塔 ,且 的左子/右子分别是其区间内左侧/右侧的最高塔。可以借助单调栈在 时间内构建:
从左到右扫描每个 i:
当栈顶元素高度小于 P_i 时:栈顶出栈,最后一个出栈的成为 i 的左子;
若栈仍非空:栈顶成为……(i 成为栈顶的右子);
把 i 入栈。第 6 步:树形 DP。 设 表示 ,则
其中 为 的左、右子(不存在时忽略)。答案为 。由于边总是从较高塔指向较低塔,可以按高度从小到大处理,从而避免递归爆栈。
样例 1 演算:,根为塔 。区间 与笛卡尔树如下:塔 (高 )的右子是塔 (高 , 中最高);塔 的左子是塔 (高 )、右子是塔 (高 );塔 的左子是塔 (高 )。叶子 ,,,,答案 ,与样例一致。
参考代码
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
vector<int> a(n), pos(n + 1);
for (int i = 0; i < n; i++) {
cin >> a[i];
pos[a[i]] = i; // 高度 -> 位置
}
// 构建最大笛卡尔树(高度更大的节点在更上层)
vector<int> lc(n, -1), rc(n, -1);
stack<int> st;
for (int i = 0; i < n; i++) {
while (!st.empty() && a[st.top()] < a[i]) {
lc[i] = st.top(); // 最后弹出的成为 i 的左子
st.pop();
}
if (!st.empty()) rc[st.top()] = i; // i 成为栈顶的右子
st.push(i);
}
// 树形 DP:子节点高度更小,按高度从小到大处理即可避免递归
vector<long long> dp(n, 0);
for (int h = 1; h <= n; h++) {
int i = pos[h];
if (lc[i] != -1) dp[i] = max(dp[i], dp[lc[i]] + (long long)(i - lc[i]));
if (rc[i] != -1) dp[i] = max(dp[i], dp[rc[i]] + (long long)(rc[i] - i));
}
cout << dp[pos[n]] << '\n';
return 0;
}复杂度分析
- 时间复杂度:(单调栈建树 + 按高度 DP)
- 空间复杂度:
G - Domino Arrangement
题目链接:https://atcoder.jp/contests/abc435/tasks/abc435_g
题目大意
有 个格子,编号 ,初始都没有涂色。有 种颜色,第 种颜色可以给区间 内的任意多个格子涂色。求满足以下条件的涂色方案数,对 取模:
对每个涂了色的格子 , 与 中恰好有一个与它颜色相同( 和 视为无色)。
()
思路
这是本场最难的计数题。核心分三步:把条件翻译成"多米诺覆盖",建立 DP 递推,再用懒标记把递推批量加速。
第 1 步:条件转化。 考察某一种颜色 的任意极大连续段(连续若干个涂成 的格子):
- 长度为 :该格左右邻居都不是 ,不满足"恰好一个同色";
- 长度 :段中间的格子左右邻居都是 ,有 个同色邻居,不满足;
- 长度恰好为 :段两端的格子恰好各有一个同色邻居(另一个邻居不是 或不存在),满足。
因此,合法涂色方案 把某些格子配对成互不重叠的长度为 的块(多米诺),每个块 被赋予一种能同时覆盖这两格的颜色 (即 且 ),且相邻两个块不能同色(否则会合并成长度为 的段)。记
为能覆盖格子对 的颜色集合。
第 2 步:DP 递推。 令 表示"涂色方案中,最右被涂的格子是 且颜色为 的方案数"。由于 是最右被涂格, 必为无色,条件要求 与 同色,故 必须是 色块,从而 ;且前 个格子构成合法涂色、同时 不能是 色(否则段会向左延伸)。于是
其中 表示前 个格子的合法涂色总数。求和得
边界:(空涂色)、(只有空涂色)。答案即 。
第 3 步:快速计算 。 定义数组 :对 ,;否则为"未定义"。那么 ,其中 ,。
来看 与 的关系(逐颜色):
- :,即整体做"取反再平移";
- :由于 (),;
- :删除该颜色;
- 其余:未定义。
而每个颜色 满足 ,所以它在 轴上是一段连续区间:固定奇偶性后,进入/离开事件各至多一次。全部颜色的进入/离开事件总数是 。
懒标记实现:对每个奇偶性分别维护一个结构,其中每个活跃颜色存一个"存储值" ,配合全局标记 ,使得实际值
"取反再平移" 只需更新标记:
完成。删除颜色时把它的存储值从 中减去、计数减一;加入颜色时令实际值为 (即 )、计数加一。这样每步 都能以 (事件数摊销)得到 与 ,从而算出 。总复杂度 。
参考代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll MOD = 998244353;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
cin >> n >> m;
// 颜色 c 活跃于 k 的区间为 [L_c+1, R_c]。
// 对两种奇偶性分别记录进入/离开事件:enter[p][k]、leave[p][k]
vector<vector<vector<int>>> enter(2, vector<vector<int>>(n + 3));
vector<vector<vector<int>>> leave(2, vector<vector<int>>(n + 3));
for (int c = 0; c < m; c++) {
int L, R;
cin >> L >> R;
int lo = L + 1;
for (int p = 0; p < 2; p++) {
int first = lo;
if ((first & 1) != p) first++; // 奇偶性为 p 的最小步
if (first > R) continue;
int last = R;
if ((last & 1) != p) last--; // 奇偶性为 p 的最大步
enter[p][first].push_back(c);
leave[p][last + 2].push_back(c); // 该奇偶性下离开的步
}
}
vector<ll> S(n + 2, 0);
S[0] = 1; // 空涂色
if (n >= 1) S[1] = 1;
// 懒标记:实际值 = mul * val + add (mul 为 1 或 -1)
// 注意 val 按 (颜色, 奇偶性) 分开存:同一个颜色可以同时在两个奇偶性的结构中活跃,
// 且两处的存储值可能不同,必须独立保存。
vector<ll> mul(2, 1), add(2, 0);
vector<ll> sumv(2, 0); // 活跃颜色存储值之和
vector<ll> cnt(2, 0); // 活跃颜色个数 = |C_k|
vector<array<ll, 2>> val(m); // val[c][p]:颜色 c 在奇偶性 p 结构中的存储值
vector<array<char, 2>> on(m); // on[c][p]:颜色 c 在奇偶性 p 的结构中活跃
for (int k = 2; k <= n; k++) {
int p = k & 1;
if (k >= 4) {
// 全局变换 x -> S[k-4] - x
mul[p] = (MOD - mul[p]) % MOD;
add[p] = (S[k - 4] - add[p] + MOD) % MOD;
}
// 离开的颜色
for (int c : leave[p][k]) {
if (on[c][p]) {
on[c][p] = 0;
cnt[p]--;
sumv[p] = (sumv[p] - val[c][p] + MOD) % MOD;
}
}
// 进入的颜色:实际值置 0
for (int c : enter[p][k]) {
if (!on[c][p]) {
on[c][p] = 1;
cnt[p]++;
val[c][p] = (mul[p] == 1) ? (MOD - add[p]) % MOD : add[p];
sumv[p] = (sumv[p] + val[c][p]) % MOD;
}
}
ll sumT = (mul[p] * sumv[p] + add[p] * cnt[p]) % MOD;
// S_k = S_{k-1} + |C_k| * S_{k-2} - sum(T_k)
S[k] = (S[k - 1] + cnt[p] * S[k - 2] % MOD - sumT) % MOD;
if (S[k] < 0) S[k] += MOD;
}
cout << S[n] << '\n';
return 0;
}复杂度分析
- 时间复杂度:
- 空间复杂度: