AtCoder Beginner Contest 443 题解
AtCoder Beginner Contest 443 题解
| 项目 | 内容 |
|---|---|
| 比赛时间 | 2026-01-31 |
| 比赛链接 | https://atcoder.jp/contests/abc443 |
| 题目数量 | A–G 共 7 题 |
比赛概览
本场整体难度较友好:A、B 为纯签到题;C 是一道按题意直接模拟的区间题;D 考察"相邻列行差不超过 1"这一约束的转化,用两遍扫描求下包络;E 是网格 DP,难点在于拆墙条件的判定;F 把"按位构造数字"转化为图上的最短路(BFS),并需要严谨论证"先到先得"能得到最小值;G 是经典的 floor sum(AtCoder Library 板子)计数题。其中 D、F、G 三道题较有区分度,分别考察约束传播、图论建模与数论板子,值得重点掌握。
A - Append s
题目链接:https://atcoder.jp/contests/abc443/tasks/abc443_a
题目大意
给定一个由小写英文字母组成的字符串 (长度 ),在其末尾追加一个字符 s 并输出。
思路
直接在字符串末尾追加字符即可。这是最基础的签到题,C++ 里 s += 's' 一行完成,注意 +='s'(字符)与 +="s"(字符串)均可。
参考代码
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
string S;
cin >> S;
S += 's';
cout << S << '\n';
return 0;
}官方题解给出的 Python 参考实现:
s = input()
s += 's'
print(s)复杂度分析
- 时间复杂度:
- 空间复杂度:
B - Setsubun
题目链接:https://atcoder.jp/contests/abc443/tasks/abc443_b
题目大意
节分这天,人们要吃掉与年龄相同数量的豆子。Takahashi 今年(第 年后) 岁,每年节分他吃掉 颗豆子( 年后)。他从今年起(含今年)最早到第几年之后,累计吃掉的豆子数达到 颗或以上?。
思路
第 年他吃掉 颗豆子,因此前 年的累计为
直接按 逐年累加模拟即可。为什么不会超时? 最坏情况是 (每年吃得最少)、(目标最大),此时需要满足
即最多约 次迭代,远小于时间限制。官方题解给出更宽松的估计:从 岁开始吃 年累计已超过 颗。
进阶(可选):也可以直接解二次不等式 ,即
其正根为
答案为 (用整数运算实现时注意浮点误差,可先取整再微调验证)。不过模拟循环已足够简单且高效。
参考代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
ll N, K;
cin >> N >> K;
ll total = 0;
for (ll i = 0; ; i++) {
total += N + i;
if (total >= K) {
cout << i << '\n';
return 0;
}
}
}复杂度分析
- 时间复杂度:(约 次迭代)
- 空间复杂度:
C - Chokutter Addiction
题目链接:https://atcoder.jp/contests/abc443/tasks/abc443_c
题目大意
AtCoder 公司从时刻 开始上班、时刻 结束。Takahashi 按照如下规则看 SNS chokutter:
- 上班开始时(时刻 )打开 chokutter;
- 若 Aoki 经过他身后时 chokutter 正开着,他立刻关闭;
- 若他在时刻 关闭,则一定在时刻 重新打开。
上班期间 Aoki 共经过 次,第 次在时刻 (满足 ,,且保证 Aoki 不会在 Takahashi 打开 chokutter 的那一刻经过)。求 Takahashi 总共看了多少秒 chokutter。
思路
关键观察:Takahashi 的状态只可能是"开着"或"关着",而"开着"的时间段总是从某个 open 时刻开始、到某次 Aoki 经过(或下班 )结束;每次关闭后一定相隔 秒重新打开,所以 open 的更新完全确定。
维护变量 表示当前这轮打开 chokutter 的时刻。由于 已经有序,按顺序扫描每次 Aoki 经过的时刻 :
- 若 :说明在 时刻 chokutter 正开着,从 到 这一整段都在观看,累加 ,然后 Takahashi 立刻关闭,并把下一次打开时刻设为 ;
- 否则():说明 时刻 chokutter 没开着( 落在两轮打开之间),Aoki 的经过不影响任何行为,什么都不做。
最后,如果 ,说明最后一轮从 一直看到下班,累加 。
正确性:每轮"打开 被 Aoki 关闭"的时间段互不重叠且完全覆盖所有观看时间; 精确记录了当前观看区间的左端点。易错点: 可能超过 (例如样例 1 最后 ),此时收尾判断 自然不成立,不会多加; 时答案为 。
参考代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int N, T;
cin >> N >> T;
ll ans = 0;
int open = 0; // 最近一次打开 chokutter 的时刻
for (int i = 0; i < N; i++) {
int a;
cin >> a;
if (open < a) { // a 时刻正开着:从 open 看到 a
ans += a - open;
open = a + 100; // 100 秒后重新打开
}
// 否则 a 时刻没开,什么都不发生
}
if (open < T) ans += T - open; // 收尾:最后一轮看到下班
cout << ans << '\n';
return 0;
}复杂度分析
- 时间复杂度:
- 空间复杂度:
D - Pawn Line
题目链接:https://atcoder.jp/contests/abc443/tasks/abc443_d
题目大意
有一个 的网格,每列恰好有一个棋子,列 的棋子在从上往下第 行。每次操作可以把一个不在最顶行的棋子向上移动一格(行号减 )。求最少的操作次数,使得对所有 ,相邻两列棋子的行号满足 。共 组数据,。
思路
记列 棋子的最终行号为 。因为棋子只能向上移动,所以 。相邻约束 逐段传递后给出任意两列之间的约束
于是对任意两列 :
这告诉我们:列 的最终行号不能超过 (其中 时给出 本身)。操作数等于 ,要让操作数最小就要让每个 尽量大。若记
那么 ,并且(下面证明)向量 本身满足相邻差不超过 ,即把每个棋子都移到 行是可行的。因此最优方案就是 ,答案 。
如何高效计算 ? 这是经典的一维"下包络/距离变换",用两遍扫描:
- 先从左到右:,得到所有 的贡献 ;
- 再从右到左:,把 的贡献也纳入。
两遍后 恰为 。
为什么结果向量满足相邻差 ? 第一遍结束后满足 (即 )。第二遍考虑操作 :若 被更新,则 ,相邻差恰好为 ;若未被更新,则 ,而第二遍只会让 变小,第一遍保证的 不会被破坏,两者合起来得 。因此 满足题目条件,且它是所有合法方案中每列行号的最大值,故为最优。
易错点: 之和可能达到 ,要用 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;
cin >> N;
vector<ll> R(N);
ll sum = 0;
for (int i = 0; i < N; i++) {
cin >> R[i];
sum += R[i];
}
// 从左到右:传播"左边棋子"的约束
for (int i = 1; i < N; i++) R[i] = min(R[i], R[i - 1] + 1);
// 从右到左:传播"右边棋子"的约束
for (int i = N - 2; i >= 0; i--) R[i] = min(R[i], R[i + 1] + 1);
for (int i = 0; i < N; i++) sum -= R[i];
cout << sum << '\n';
}
return 0;
}复杂度分析
- 时间复杂度:(每组数据)
- 空间复杂度:
E - Climbing Silver
题目链接:https://atcoder.jp/contests/abc443/tasks/abc443_e
题目大意
有一个 网格, 为墙(#)或空地(.)。Takahashi 初始在空地 ,然后重复 次移动:从 出发,目的地取 之一(不能出界)。若目的地是墙:
- 当该列在目的地下方的所有格子 当前都是空地时,可以拆掉这面墙并移动过去;
- 否则移动失败,立刻终止(即使还没走满 步)。
输出一个长度为 的 串 :若他能无失败地到达 ,则第 位为 1,否则为 0。共 组数据,,。
思路
用 DP 记录可达性。定义 表示 Takahashi 能无失败地到达格子 。
初始化:对所有 置 。因为起点 是空地,他可以沿列 自下而上逐格移动:遇到墙时,其下方的格子都已经被他走过/拆过成为空地,故总能满足拆墙条件。
转移:从下往上处理行 。若 三者中至少一个为 ,说明"不考虑墙"时 可达:
- 若 是空地,直接 ;
- 若 是墙,则只有当它是该列最下方的墙时才能拆。为此预处理 表示第 列中最靠下(行号最大)的墙所在行。拆墙条件" 下方全空"等价于 ( 行以下是该列最下方的墙,下面全是空地)。一旦拆掉 并站上去,他可以继续沿列向上推进、把上方的墙逐块拆掉,因此整列从第 行到顶全都可达:令 对所有 。
正确性:拆墙条件只与原始网格中"该列 行以下是否有墙"有关——若下方原本有空地被走过、或有墙被拆掉,那么被拆掉的墙在拆的时候已经把其上方的格子标为可达(即 早已为 ),不会等到现在才需要处理。因此对墙的处理只需判断 这一次;每列至多被"整列标记"一次,总工作量 。
易错点:、 越界判断; 可能已被更下方的拆墙标记为 ,此时要直接跳过(continue),否则会重复处理。
参考代码
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T;
cin >> T;
while (T--) {
int N, C;
cin >> N >> C;
C--;
vector<string> S(N);
vector<int> low(N, -1);
for (int i = 0; i < N; i++) {
cin >> S[i];
for (int j = 0; j < N; j++)
if (S[i][j] == '#') low[j] = i; // 列 j 中最靠下的墙的行号
}
vector<vector<int>> dp(N, vector<int>(N, 0));
for (int i = 0; i < N; i++) dp[i][C] = 1; // 沿列 C 直上一定可行
for (int i = N - 2; i >= 0; i--) {
for (int j = 0; j < N; j++) {
if (dp[i][j]) continue; // 已被下方拆墙标为可达
bool ok = dp[i + 1][j];
if (j > 0 && dp[i + 1][j - 1]) ok = true;
if (j + 1 < N && dp[i + 1][j + 1]) ok = true;
if (!ok) continue;
if (S[i][j] == '.') {
dp[i][j] = 1;
} else if (low[j] == i) { // 是该列最下方的墙,可拆
for (int k = 0; k <= i; k++) dp[k][j] = 1;
}
}
}
for (int j = 0; j < N; j++) cout << dp[0][j];
cout << '\n';
}
return 0;
}复杂度分析
- 时间复杂度:(每组数据;每列至多整列标记一次,均摊后仍为 )
- 空间复杂度:
F - Non-Increasing Number
题目链接:https://atcoder.jp/contests/abc443/tasks/abc443_f
题目大意
称正整数 是好整数,当且仅当它写成十进制后,从个位到最高位的数字序列 (满足 )是非递增的,即 。例如 、、 是好整数,、 不是。
给定正整数 (),判断是否存在 的好整数倍,若存在输出最小值,否则输出 。答案可能非常大(样例 4 给出了 22 位的答案)。
思路
第一步:重述题意。 条件 (个位最大)等价于从左往右读数字是非递减的(最高位最小)。例如 :。另一个重要事实:好整数的每一位都在 中——若某一位出现 ,则比它更低的所有位也都必须是 ,整个数就是 ,不可能是正整数。
第二步:转化为图上最短路。 把数字从高位到低位一位一位地构造。状态设为二元组 :
- :当前已构造的数模 的余数;
- :当前构造出的数的最低位(最后追加的那一位)。
由于数字从左到右非递减,接下来追加的位 必须满足 ;追加后新的余数为 ,新的最低位为 。于是得到一个有向图:
起点是 ( 表示还没有任何数字,因此第一位必须 ,即 )。每条边代表"多一位数字",花费均为 。
第三步:为什么 BFS 能给出最小值。 我们要求的是能被 整除的最小好整数,即从起点到达某个 的状态 的"位数最少、且数值最小"的路径。
- 位数最少:更多位数一定数值更大,所以先做 BFS(边权全为 )保证找到的是位数最少的答案;
- 数值最小:对固定位数的数,数值大小等于从左到右的字典序大小。BFS 从每个状态出发按 升序尝试,且队列先进先出,因此同位数的好整数是按从小到大(字典序)依次被发现的。于是第一个到达 的状态对应的数字就是全局最小答案。
用数组 记录每个状态的转移来源(父状态),找到答案后沿着 一路回溯,把路径上每个状态的编号 (即当时追加的那一位)收集起来再反转即可得到答案串。若队列耗尽仍未到达 ,输出 。
关于 的说明:最显然的无解情形是 为 的倍数——此时任何倍数都以 结尾,而好整数不含数字 。但无解并不只限于此(例如 也无解,可以用模 、 的条件推出来),BFS 会自然检测到:状态空间有限( 个状态),全部可达状态遍历完后仍无 ,即为 。
一个优化(官方题解的写法):扩展状态 时按 升序尝试,一旦发现目标状态 已经访问过,就立刻 break,不再尝试更大的 。正确性在于:若追加 的目标已访问,说明它是由某个更早被处理的状态 通过追加数字 到达的(由同余结构, 与当前状态满足 ),而 被处理得更早,它当时继续尝试更大数字产生的后继——与我们想跳过的后继完全一致(因为 )——要么已被 入队,要么在更早时刻就已命中"目标已访问"的情形而继续归纳成立。因此跳过这些后继不会遗漏任何可达状态。若不想依赖这个证明,把 break 改成 continue 也同样正确(此时每个状态至多尝试 9 个数字,总复杂度 , 是常数)。
参考代码
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int N;
cin >> N;
// 状态编号 10*x + c:当前数值模 N 为 x,最低位数字为 c
// to[v] = 状态 v 的转移来源,-1 表示未访问
vector<int> to(10LL * N + 10, -1);
deque<int> q;
q.push_back(0); // 初始状态 (0, 0)
while (!q.empty()) {
int idx = q.front();
q.pop_front();
int c = idx % 10, x = idx / 10;
// 下一位 cc >= 当前最低位 c(数字从左到右非递减)
for (int cc = max(1, c); cc <= 9; cc++) {
int xx = (int)((10LL * x + cc) % N);
int idx2 = 10 * xx + cc;
if (to[idx2] != -1) break; // 已访问:更大的数字后继也已访问,剪枝
to[idx2] = idx;
q.push_back(idx2);
if (xx == 0) { // 第一个到达 x=0 的状态即最小答案
string ans;
int cur = idx2;
while (cur != 0) {
ans.push_back(char('0' + cur % 10));
cur = to[cur];
}
reverse(ans.begin(), ans.end());
cout << ans << '\n';
return 0;
}
}
}
cout << -1 << '\n';
return 0;
}官方题解给出的 Python 参考实现(用 同时承担"是否访问过"与"父状态"两个角色):
from collections import deque
n = int(input())
to = [-1] * (10 * n)
q = deque([0])
while q:
idx = q.popleft()
c, x = idx % 10, idx // 10
for cc in range(max(1, c), 10):
xx = (10 * x + cc) % n
idx2 = 10 * xx + cc
if to[idx2] != -1:
break
q.append(idx2)
to[idx2] = idx
if xx == 0:
ans = []
cur = idx2
while cur != 0:
ans.append(str(cur % 10))
cur = to[cur]
print("".join(ans[::-1]))
exit()
print(-1)复杂度分析
- 时间复杂度:,其中 (状态数为 ,每个状态至多尝试 个数字;不采用
break时也为 量级) - 空间复杂度:( 数组与队列)
G - Another Mod of Linear Problem
题目链接:https://atcoder.jp/contests/abc443/tasks/abc443_g
题目大意
给定整数 ,定义数列 。求满足 且 的 的个数。共 组数据,,。
思路
设 。条件 可作如下等价变形:
其中第一步是因为两侧都是整数, 等价于 ;最后一步用到了"对实数 和整数 , 与 等价"。
记
注意到两个分数的差为
(因为 ),所以两个整数取整后至多相差 ,即 。于是指示量
对 求和:
这两个求和正是经典的 floor sum:,可以用欧几里得式的递归/迭代在 内求出(AtCoder Library 的 floor_sum 板子)。其核心思想是当 或 时先取出整除部分,再通过"交换 与 "把问题规模按欧几里得算法缩小。
注意:当 或 时, 或 是负数。实现时要先把 用带余除法归一化到 :,则
对 同理;把 与 累加进答案后再调用非负版 floor_sum 即可。
参考代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
// sum_{i=0}^{n-1} floor((a*i + b) / m),要求 a, b >= 0(ACL 风格的 floor_sum,O(log m))
ll floor_sum(ll n, ll m, ll a, ll b) {
ll ans = 0;
while (true) {
if (a >= m) {
ans += (n - 1) * n * (a / m) / 2;
a %= m;
}
if (b >= m) {
ans += n * (b / m);
b %= m;
}
ll y_max = a * n + b;
if (y_max < m) break;
n = y_max / m;
b = y_max % m;
swap(m, a);
}
return ans;
}
// 允许 a, b 为负数的封装(本题 A-1、B-1 可能为 -1)
ll floor_sum_signed(ll n, ll m, ll a, ll b) {
ll extra = 0;
ll qa = a / m, ra = a % m;
if (ra < 0) { ra += m; qa--; } // 归一化 a 到 [0, m)
extra += qa * (n * (n - 1) / 2);
ll qb = b / m, rb = b % m;
if (rb < 0) { rb += m; qb--; } // 归一化 b 到 [0, m)
extra += qb * n;
return extra + floor_sum(n, m, ra, rb);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T;
cin >> T;
while (T--) {
ll N, M, A, B;
cin >> N >> M >> A >> B;
ll ans = N - floor_sum_signed(N, M, A, B)
+ floor_sum_signed(N, M, A - 1, B - 1);
cout << ans << '\n';
}
return 0;
}官方题解给出的 Python 参考实现(递归版 floor_sum,Python 整除自带向下取整语义,天然支持负数):
import sys
input = sys.stdin.readline
def floor_sum(n, m, a, b):
a1, a2 = a // m, a % m
s = n * (n - 1) // 2 * a1
b1, b2 = b // m, b % m
if a2 == 0:
return s + b1 * n
k = (a2 * (n - 1) + b2) // m
return s + n * (k + b1) - floor_sum(k, a2, m, m + a2 - b2 - 1)
for _ in range(int(input())):
n, m, a, b = map(int, input().split())
print(n - floor_sum(n, m, a, b) + floor_sum(n, m, a - 1, b - 1))复杂度分析
- 时间复杂度:(每组数据,floor sum 按欧几里得算法每次把规模减小约一半)
- 空间复杂度:(迭代版)/ (递归版栈深)