AtCoder Beginner Contest 449 题解
AtCoder Beginner Contest 449 题解
| 项目 | 内容 |
|---|---|
| 比赛时间 | 2026-03-14 |
| 比赛链接 | https://atcoder.jp/contests/abc449 |
| 题目数量 | A–G 共 7 题 |
比赛概览
本场难度梯度非常清晰:A、B 是纯粹的签到题(圆面积公式、矩形模拟);C 是经典的滑动窗口计数;D 是把"二维计数"拆成两个一维情形的分类讨论,验证基本功。从 E 开始进入需要"建模"的题目:E 把看似无穷( 次)的操作压缩成"按频率分阶段循环追加"的结构,配合离线 BIT 回答查询;F 是求矩形并面积的扫描线 + multiset 经典套路;G 是全场最难,需要先对 repunit 之和做一步巧妙的变换(化为 的幂之和),再借助生成函数与差分方程在 时间内完成计数。E、F、G 分别代表"过程压缩""扫描线""生成函数"三个值得反复练习的知识点,是本题的区分度所在。
A - π
题目链接:https://atcoder.jp/contests/abc449/tasks/abc449_a
题目大意
给定正整数 ,求直径为 的圆的面积。输出与真实值的绝对误差或相对误差不超过 即判为正确。
思路
圆面积公式为 。直径 是半径的两倍,即 ,代入得:
直接计算并输出即可。取 的方式:C++ 中用 acos(-1.0)(跨平台可移植,比 M_PI 更稳妥),Python 中用 math.pi。
易错点:精度要求宽松(),但输出时仍应保留足够多的小数位(例如 C++ 固定输出 20 位),避免浮点截断造成误差。
参考代码
#include <bits/stdc++.h>
using namespace std;
int main() {
int D;
cin >> D;
cout << fixed << setprecision(20) << acos(-1.0) * D * D / 4.0 << '\n';
return 0;
}官方题解给出的 Python 实现:
from math import pi
d = int(input())
print(d * d * pi / 4)复杂度分析
- 时间复杂度:
- 空间复杂度:
B - Deconstruct Chocolate
题目链接:https://atcoder.jp/contests/abc449/tasks/abc449_b
题目大意
有一块 行 列(每格为一块)的矩形巧克力。依次处理 个操作,每个操作输出吃掉的数量:
- 操作 1:给定 ,吃掉最下面 行,输出被吃掉的块数;
- 操作 2:给定 ,吃掉最右边 列,输出被吃掉的块数。
保证每次操作后巧克力仍然是矩形(即操作 1 前至少有 行、操作 2 前至少有 列)。
思路
题目保证操作后巧克力始终保持矩形,所以只需要用两个变量 维护"当前巧克力的行数、列数",直接模拟:
- 操作 1:最下面 行共 块,输出 ,然后令 ;
- 操作 2:最右边 列共 块,输出 ,然后令 。
为什么正确:吃掉最下面 行后,剩余部分仍是 行 列的矩形(顶部与左右边界不动);同理吃掉最右边 列后剩下 行 列。题目保证 、 恒成立,因此模拟不会出现非法的空矩形。
易错点:操作 1 的答案是 (用的是当前的列数),操作 2 的答案是 (用当前的行数),不要用错;更新要在输出之后进行。
参考代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
int main() {
int h, w, q;
cin >> h >> w >> q;
while (q--) {
int t;
cin >> t;
if (t == 1) {
int r;
cin >> r;
cout << 1LL * r * w << '\n';
h -= r;
} else {
int c;
cin >> c;
cout << 1LL * h * c << '\n';
w -= c;
}
}
return 0;
}复杂度分析
- 时间复杂度:
- 空间复杂度:
C - Comfortable Distance
题目链接:https://atcoder.jp/contests/abc449/tasks/abc449_c
题目大意
给定长度为 的小写字母字符串 。统计满足以下条件的整数对 的个数:
- ;
- ;
- 。
思路
固定右端点。对于每个 ,满足条件的 必须落在区间 内,且 。于是问题变成:对每个 ,统计窗口 中字符 的出现次数,全部累加。
滑动窗口差分法:把窗口内各字符的出现次数存在数组 cnt[26] 中。随着 每次右移一步,窗口 恰好滑出一个元素( 处)并滑入一个元素( 处),可以在 内差分更新。把 先自增 可以避免边界判断,具体如下(均用 0-indexed):
对 :
- 若 ,加入 :
cnt[s[i-L]]++; - 若 ,移出 :
cnt[s[i-R-1]]--; - 累加
cnt[s[i]]。
为什么正确:每个满足条件的无序配对由它的右端点 唯一决定,且当 恰好取到该右端点时, 恰好落在当前维护的窗口内,因此每个配对被且仅被统计一次。 保证了 ,窗口内不包含 自身,不会把自己算进去。
易错点:窗口是闭区间 ,长度为 ;先加入再移出、或先移出再加入都不影响正确性,但要注意下标越界判断。答案最大约为 ,必须用 64 位整数。
参考代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
int main() {
int n, l, r;
cin >> n >> l >> r;
r++; // 提前 +1,简化滑动窗口的边界判断
string s;
cin >> s;
ll ans = 0;
vector<int> cnt(26);
for (int i = 0; i < n; i++) {
if (i >= l) cnt[s[i - l] - 'a']++; // 窗口右端新进入 S[i-l]
if (i >= r) cnt[s[i - r] - 'a']--; // 窗口左端滑出 S[i-r]
ans += cnt[s[i] - 'a']; // 统计以 i 为右端点的配对
}
cout << ans << '\n';
return 0;
}复杂度分析
- 时间复杂度:(每个字符至多进出窗口一次)
- 空间复杂度:
D - Make Target 2
题目链接:https://atcoder.jp/contests/abc449/tasks/abc449_d
题目大意
二维坐标平面上,格点 当 为偶数时涂黑,为奇数时涂白。给定矩形 ,求其中黑色格点的个数。
思路
关键是处理 。对任意格点,它恰好属于以下两种情形之一:
情形一:,此时 ,要求 为偶数。对每个偶数 , 需要满足 ,即 ,等价于
再与区间 求交,个数为
情形二:,此时 ,要求 为偶数。对每个偶数 , 满足 ,即
与 求交,个数为
两种情形互斥且并集覆盖所有格点,因此答案就是两部分的简单求和。坐标范围 ,直接枚举所有偶数 与偶数 即可。
易错点:情形一的不等式是严格小于(),所以下界是 ;情形二是不等号(),下界是 。二者边界差 1,写反会导致答案相差一行(列)的计数。另外用 处理区间为空的情形。
参考代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
int main() {
int L, R, D, U;
cin >> L >> R >> D >> U;
ll ans = 0;
// |x| > |y|:max = |x|,需要 |x| 为偶数
for (int x = L; x <= R; x++) {
if (x % 2 == 0) {
int lo = max(D, -abs(x) + 1);
int hi = min(U, abs(x) - 1);
ans += max(0, hi - lo + 1);
}
}
// |x| <= |y|:max = |y|,需要 |y| 为偶数
for (int y = D; y <= U; y++) {
if (y % 2 == 0) {
int lo = max(L, -abs(y));
int hi = min(R, abs(y));
ans += max(0, hi - lo + 1);
}
}
cout << ans << '\n';
return 0;
}复杂度分析
- 时间复杂度:,坐标范围 ,完全可行
- 空间复杂度:
E - A += v
题目链接:https://atcoder.jp/contests/abc449/tasks/abc449_e
题目大意
给定整数 与长度 的序列 ()。对这个序列执行 次操作:设 为 到 之间在 中出现次数最少的整数,若并列取最小的那个,把 追加到 末尾。给定 个询问 ,求操作 次后 的值。
思路
操作次数 巨大,不可能直接模拟,需要"把同频追加的过程打包"。以下记 为值 当前出现次数。
关键观察:按频率排序后的"分阶段循环"结构。
把 按 升序排序,得到序列 ,并令 ,则 。再定义 。
整个过程可以等价描述为:对 ,依次执行 轮"把 按数值升序依次各追加一次"。
为什么这样是对的:在第 阶段开始时, 的次数都等于 ,且是全局最小;其余值的次数都大于 。于是每次追加都在这些最小值中选,平局按数值取最小——恰好就是"升序轮转"。每轮把 各加一次,它们的次数同步 +1;重复 轮后它们都到达 ,阶段结束,进入 阶段。(当 时该阶段是空转。)
回答询问。记 ,第 阶段结束后序列长度为
这是因为第 阶段每轮追加 个元素。 关于 单调不减。注意到 ,所以设 即可覆盖所有 。
对询问 :
- 若 ,答案为初始序列的 ;
- 否则二分出最小的 使得 (即 落在第 阶段),令
则 是该阶段第 个追加元素。由于第 阶段每轮都是" 升序"的循环,答案就是 中第 小的值。
离线回答"前 个中的第 小":把询问按 分组并排序,用 BIT 维护值域(值 每个位置放 0/1)。当 从小到大增加时,把 加入 BIT;此时 都已在 BIT 中,用 lower_bound 求出前缀和 的最小位置,即为第 小的值。总复杂度 。
验证:以样例 1 为例,,,排序得 ,。,,。前三个阶段追加的分别是 、、然后 循环,得到序列 ,与样例输出完全一致。
参考代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
struct BIT {
int n;
vector<int> bit;
BIT(int n) : n(n), bit(n + 1, 0) {}
void add(int i, int x) { // 1-indexed
for (; i <= n; i += i & -i) bit[i] += x;
}
// 返回前缀和 >= w 的最小下标(1-indexed)
int lower_bound(int w) {
int x = 0, k = 1;
while (k << 1 <= n) k <<= 1; // 不大于 n 的最大 2 的幂
for (; k; k >>= 1) {
if (x + k <= n && bit[x + k] < w) {
w -= bit[x + k];
x += k;
}
}
return x + 1;
}
};
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
cin >> n >> m;
vector<int> a(n), c(m + 1, 0);
for (int i = 0; i < n; i++) {
cin >> a[i];
c[a[i]]++;
}
// 按 (当前次数, 值) 升序排序
vector<pair<int, int>> p; // (V_i, P_i)
for (int v = 1; v <= m; v++) p.push_back({c[v], v});
sort(p.begin(), p.end());
vector<ll> V(m + 1), r(m + 1);
for (int i = 0; i < m; i++) V[i + 1] = p[i].first;
r[0] = n;
for (int k = 1; k < m; k++)
r[k] = r[k - 1] + 1LL * k * (V[k + 1] - V[k]);
r[m] = (ll)4e18; // 视为 +inf
int q;
cin >> q;
vector<vector<pair<ll, int>>> byPhase(m + 1);
vector<ll> ans(q);
for (int i = 0; i < q; i++) {
ll x;
cin >> x;
if (x <= n) { ans[i] = a[x - 1]; continue; }
int ok = m, ng = 0; // 最小 ok 使得 r[ok] >= x
while (ok - ng > 1) {
int mid = (ok + ng) / 2;
if (r[mid] >= x) ok = mid;
else ng = mid;
}
ll off = (x - r[ng] - 1) % ok; // 阶段内第 off+1 个
byPhase[ok].push_back({off, i});
}
BIT bit(m);
for (int k = 1; k <= m; k++) {
bit.add(p[k - 1].second, 1); // 值 P_k 进入 BIT
for (auto [off, idx] : byPhase[k])
ans[idx] = bit.lower_bound((int)off + 1);
}
for (int i = 0; i < q; i++) cout << ans[i] << '\n';
return 0;
}官方题解还提供了 Python 实现(基于相同的离线 BIT 思路),供参考:
import sys
input = sys.stdin.readline
# 返回前缀和 >= w 的最小下标(1-indexed)
def lower_bound(bit, w):
n = len(bit) - 1
x = 0
k = 1 << (n.bit_length() - 1)
while k:
if x + k <= n and bit[x + k] < w:
w -= bit[x + k]
x += k
k >>= 1
return x + 1
def main():
n, m = map(int, input().split())
a = list(map(int, input().split()))
c = [0] * (m + 1)
for v in a:
c[v] += 1
p = sorted((c[v], v) for v in range(1, m + 1))
V = [0] * (m + 1)
for i in range(1, m + 1):
V[i] = p[i - 1][0]
r = [0] * (m + 1)
r[0] = n
for k in range(1, m):
r[k] = r[k - 1] + k * (V[k + 1] - V[k])
r[m] = 10**18 + 1
q = int(input())
ans = [-1] * q
by = [[] for _ in range(m + 1)]
for i in range(q):
x = int(input())
if x <= n:
ans[i] = a[x - 1]
continue
ok, ng = m, 0
while ok - ng > 1:
mid = (ok + ng) // 2
if r[mid] >= x:
ok = mid
else:
ng = mid
off = (x - r[ng] - 1) % ok
by[ok].append((off, i))
bit = [0] * (m + 1)
for k in range(1, m + 1):
v = p[k - 1][1]
i = v
while i <= m:
bit[i] += 1
i += i & -i
for off, idx in by[k]:
ans[idx] = lower_bound(bit, off + 1)
print('\n'.join(map(str, ans)))
main()复杂度分析
- 时间复杂度:(排序 + 二分 + 离线 BIT)
- 空间复杂度:
F - Grid Clipping
题目链接:https://atcoder.jp/contests/abc449/tasks/abc449_f
题目大意
网格中有 个黑色格子 ,其余为白色。求所有全由白色格子组成的 矩形区域的个数,即满足 、、且该 区域不含任何黑格的 的个数。
思路
补集转化。合法子矩形总数是 。数出"至少含一个黑格"的坏矩形个数 ,则答案为 。
一个黑格 使得哪些左上角 对应的 矩形变坏?矩形需要盖住 ,即
所以每个黑格对应一个轴平行矩形(注意坐标范围可能越界,扫过时自然截断)。 就是这些矩形的并集面积。由于 高达 而 ,需要对稀疏的矩形做扫描线。
扫描线 + multiset 维护区间并。沿 方向扫描:
- 黑格 的矩形在 处"进入"(带来 方向区间 ),在 处"离开";
- 把事件按 坐标排序,相邻两个事件行之间的所有行,其被覆盖的 集合都相同,可以整段计算。
维护"当前行被覆盖的 长度"需要高效支持插入/删除区间并。做法是:用一个有序集合(C++ multiset)存放所有活动区间的左端点 ,并插入两个哨兵 与 。对集合中相邻的两个元素 ,它们之间未被覆盖的 个数为
理由:左端点为 的区间覆盖 ,因此在 段内覆盖了前 个位置,剩下 个未被覆盖。对集合中所有相邻对求和,即得当前行的未覆盖数 good,被覆盖数为 ( 为合法 总数)。
插入或删除左端点 时,只影响它与前驱 、后继 构成的局部,令
即可 更新。
累加答案:设事件按行排序为 。扫描时维护 pre 为已处理到的行,遇到事件行 时,先累加行区间 的坏矩形数 ,再处理该行的事件(先加后删按事件类型)。最后处理完所有事件后,把剩余的行也累加进去。
易错点:事件行可能为负()或超过 ,处理时用 截断;在负行上只更新集合不累加答案即可。复杂度 。
参考代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
int main() {
cin.tie(nullptr);
ios::sync_with_stdio(false);
ll H, W, h, w, n;
cin >> H >> W >> h >> w >> n;
vector<tuple<ll, ll, bool>> g; // (事件行, 区间左端点, 是否加入)
g.reserve(2 * n);
for (int i = 0; i < n; i++) {
ll r, c;
cin >> r >> c;
r--; c--; // 转 0-indexed
g.push_back({r - h + 1, c - w + 1, true}); // 在行 r-h+1 进入
g.push_back({r + 1, c - w + 1, false}); // 在行 r+1 离开
}
sort(g.begin(), g.end());
multiset<ll> s;
s.insert(-w);
s.insert(W - w + 1); // 两个哨兵
ll ans = (H - h + 1) * (W - w + 1); // 总矩形数
auto gap = [&](ll l, ll r) { return max(0LL, r - l - w); };
const ll X = H - h + 1; // 合法 r0 个数
const ll Y = W - w + 1; // 合法 c0 个数
ll good = Y; // 当前行未覆盖的 c0 数
auto add = [&](ll x) {
auto it = s.lower_bound(x);
ll r = *it, l = *prev(it);
good += gap(l, x) + gap(x, r) - gap(l, r);
s.insert(x);
};
auto del = [&](ll x) {
auto it = s.find(x);
ll r = *next(it), l = *prev(it);
good -= gap(l, x) + gap(x, r) - gap(l, r);
s.erase(it);
};
ll pre = 0;
int i = 0;
while (i < 2 * n) {
ll x = get<0>(g[i]);
ll nx = min(x, X);
if (pre < nx) { // 先累加 [pre, nx) 这些行的坏矩形
ans -= (nx - pre) * (Y - good);
pre = nx;
}
while (i < 2 * n && get<0>(g[i]) == x) { // 再统一处理本行的所有事件
auto [r, c, in] = g[i];
if (in) add(c);
else del(c);
i++;
}
}
if (pre < X) ans -= (X - pre) * (Y - good);
cout << ans << '\n';
return 0;
}复杂度分析
- 时间复杂度:(排序 + 每个事件一次 multiset 操作)
- 空间复杂度:
G - Many Repunit Sum 2
题目链接:https://atcoder.jp/contests/abc449/tasks/abc449_g
题目大意
给定正整数 。对正整数 , 位 repunit(全 1 数)定义为 。求" 个位数在 到 之间的 repunit 之和"所能表示的不同整数的个数,对 取模。
思路
本题的核心是两步:先把问题变换为" 个 的幂之和",再用生成函数 + 差分方程计数。
第一步:问题变换。 设 位 repunit 为 。若 ,则
反之同理(两边同乘 并加 即可反推)。因此 可表示 ⟺ 可表示为 个"位数不超过 的 的幂"(即 )之和,且 是表示值之间的双射。问题转化为:计数可表示为 个 的幂(指数 )之和的不同 的个数。
第二步:判断 是否可表示。 设 为用 的幂表示 所需的最少项数(,指数 )。 可表示为 项之和当且仅当:
- (每项至少为 );
- (由 的定义);
- (因为 ,任何 项之和 ;而 ,故 )。
充分性:取 项的最优表示。把一项 ()拆成十个 ,项数恰好增加 。如此重复 次即可得到 项表示; 保证过程中不会出现"所有指数都为 却还需要再拆"的情况(若全为 ,则 项数 ,矛盾)。
的计算(贪心):从高位到低位贪心。设 ,,则
因为 个 无法用少于 项凑出;而 时,用 的幂表示 的最少项数就是 的数位和(任何表示的数位和 项数,且直接用各位数字即可达到)。
第三步:计数。 把 写成"数字分解":,而 是 的十进制数字。则 。先忽略 条件,对所有 计数满足
的元组个数;最后再减去误计为可表示、但 的元组。当 时 ,条件 2 自动满足,且 ,故 且被计数的 恰是满足 的 ,个数为 。于是
其中 是满足 的元组个数。前 个数字各 ,最后一个无上界,因此 是 系数 的前缀和:
第四步: 递推系数。 令 ,,(这里的 即上面的 )。由 两边比较 的系数,可整理出(推导要点:,将 重标号为 ):
因为 的次数只有 ,求和范围至多 项,可在 内算出 。由于 ,模 下做逆元、加减乘即可( 为素数, 均与其互素,可直接用费马小定理求逆)。
为什么正确:整个推导链条是等价的——repunit 和 ⟺ 的幂之和 ⟺ 满足三条条件的 ⟺ 满足同余条件的"数字分解"元组。每一步都是双射或无损的计数(最后一步减去 恰好剔除 的部分),因此结果即原问题的不同整数个数。
补充:官方题解还指出可以用 FPS 的 log/exp 或 NTT 快速幂在 内求解,也可直接利用闭式
同样 可做,但差分方程法实现最简洁。
参考代码
#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);
ll N, M;
cin >> N >> M;
int n = (int)N;
ll e = (M - 1) % MOD;
vector<ll> f(n + 1), inv(n + 2);
for (int i = 1; i <= n + 1; i++) inv[i] = modpow(i, MOD - 2);
// f = (1 + x + ... + x^9)^(M-1) 的前 n+1 个系数,差分方程递推
f[0] = 1;
for (int i = 0; i < n; i++) {
ll sum = 0;
for (int k = max(0, i - 8); k <= i; k++) {
ll coef = (e * (i - k + 1) - k) % MOD;
if (coef < 0) coef += MOD;
sum = (sum + coef * f[k]) % MOD;
}
f[i + 1] = sum * inv[i + 1] % MOD;
}
// c_S = sum_{s<=S} f_s;累加所有 S ≡ N (mod 9), S <= N 的 c_S
ll pre = 0, ans = 0;
for (int S = 0; S <= n; S++) {
pre = (pre + f[S]) % MOD;
if (S % 9 == n % 9) ans = (ans + pre) % MOD;
}
ans = (ans - n / 9) % MOD; // 剔除 m < N 的部分
if (ans < 0) ans += MOD;
cout << ans << '\n';
return 0;
}复杂度分析
- 时间复杂度:
- 空间复杂度: