AtCoder ABC #460 题解
A - Mod While Positive
题目描述
给定两个正整数 N N N 和 M M M。
重复执行以下操作,直到 M M M 的值变为 0 0 0,求执行操作的次数:
- 设 x x x 为 N N N 除以 M M M 的余数,将 M M M 的值替换为 x x x。
可以证明,经过有限次操作后, M M M 最终会变为 0 0 0。
解题思路
直接模拟即可。不断进行取模操作,直到 M M M 变为 0 0 0。
由于每次操作后 M M M 都会变成 N % M N \% M N%M,而余数一定小于除数,所以 M M M 是严格递减的。又因为 M M M 是非负整数,所以必然会递减到 0 0 0,不会无限循环。
每次操作让 M M M 变小,最坏情况下操作次数为 O ( log min ( N , M ) ) O(\log \min(N, M)) O(logmin(N,M)) 级别。
代码
cpp
#include <bits/stdc++.h>
using namespace std;
int main()
{
int n, m, ans = 0; // n, m 为输入的两个正整数,ans 计数操作次数
cin >> n >> m; // 读取输入
while (m) // 当 m 不为 0 时循环
{
ans++; // 操作次数 +1
m = n % m; // 将 m 替换为 n % m(取余操作)
}
cout << ans; // 输出总共执行的操作次数
return 0;
}
B - Two Rings
题目描述
在 x y xy xy 平面上有两个圆 C 1 C_1 C1 和 C 2 C_2 C2。(在本题中,圆指的是圆周,即只有周长上的点,不包括内部)
圆 C 1 C_1 C1 的圆心为 ( X 1 , Y 1 ) (X_1, Y_1) (X1,Y1),半径为 R 1 R_1 R1。
圆 C 2 C_2 C2 的圆心为 ( X 2 , Y 2 ) (X_2, Y_2) (X2,Y2),半径为 R 2 R_2 R2。
判断圆 C 1 C_1 C1 和圆 C 2 C_2 C2 是否有公共点。也就是说,判断是否存在至少一个点,使得该点到 ( X 1 , Y 1 ) (X_1, Y_1) (X1,Y1) 的距离恰好为 R 1 R_1 R1,同时到 ( X 2 , Y 2 ) (X_2, Y_2) (X2,Y2) 的距离也恰好为 R 2 R_2 R2。
给定 T T T 组测试数据,请分别求解。
解题思路
计算几何。两个圆的圆心以及符合题目条件的公共点,三者构成一个三角形。
三条边长分别为: R 1 R_1 R1、 R 2 R_2 R2、两个圆心之间的距离 D D D。
要使三角形存在(退化三角形也算),需要满足三角形的边长条件:
- D ≥ ∣ R 1 − R 2 ∣ D \ge |R_1 - R_2| D≥∣R1−R2∣(保证圆不能一个完全包含另一个而没有交点)
- D ≤ R 1 + R 2 D \le R_1 + R_2 D≤R1+R2(保证两个圆不会相离太远)
代码
cpp
#include <bits/stdc++.h>
#define int long long
using namespace std;
signed main()
{
int T; // T 为测试数据组数
cin >> T; // 读取测试数据组数
while (T--) // 循环处理每一组数据
{
int x1, x2, y1, y2, r1, r2; // 定义两个圆的参数:圆心坐标和半径
cin >> x1 >> y1 >> r1; // 读取第一个圆的圆心坐标和半径
cin >> x2 >> y2 >> r2; // 读取第二个圆的圆心坐标和半径
// 计算两个圆心之间距离的平方(避免开方带来的精度问题)
int l2 = (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1);
// 判断两圆是否有公共点
// 条件:圆心距 D 需满足 |r1 - r2| <= D <= r1 + r2
// 平方形式:d^2 >= (r1 - r2)^2 且 d^2 <= (r1 + r2)^2
if (l2 > (r1 + r2) * (r1 + r2) || l2 < (r1 - r2) * (r1 - r2))
cout << "No\n"; // 无公共点(相离或内含)
else
cout << "Yes\n"; // 有公共点(相切或相交)
}
return 0;
}
C - Sushi
题目描述
做寿司需要两种原料: N N N 块醋饭(shari)和 M M M 块配料(neta)。
第 i i i 块醋饭的重量为 A i A_i Ai,第 j j j 块配料的重量为 B j B_j Bj。
制作一块寿司需要将一块醋饭和一块配料组合在一起。组合时,配料的重量不能超过醋饭重量的两倍。另外,每块醋饭和配料只能使用一次(不能用于多块寿司)。
求最多能制作多少块寿司。
解题思路
贪心。将醋饭和配料分别按重量从小到大排序。
容易发现,重量越小的配料越容易满足"不超过醋饭两倍"的条件,所以应该让较小的配料尽可能去匹配较小的醋饭。
代码
cpp
#include <bits/stdc++.h>
#define int long long
using namespace std;
signed main()
{
int n, m; // n 为醋饭数量,m 为配料数量
cin >> n >> m; // 读取醋饭和配料的数量
vector<int> a(n + 1), b(m + 1); // a 存储醋饭重量,b 存储配料重量(从 1 开始方便处理)
for (int i = 1; i <= n; i++) // 读取 n 块醋饭的重量
cin >> a[i];
for (int i = 1; i <= m; i++) // 读取 m 块配料的重量
cin >> b[i];
sort(a.begin(), a.end()); // 将醋饭按重量从小到大排序
sort(b.begin(), b.end()); // 将配料按重量从小到大排序
int i = 1, j = 1, ans = 0; // i 指向当前醋饭,j 指向当前配料,ans 计数成功配对数
while (i <= n && j <= m) // 当还有未使用的醋饭和配料时循环
{
if (b[j] <= a[i] * 2) // 如果当前配料重量 <= 当前醋饭重量的两倍,则可以配对
i++, j++, ans++; // 配对成功,两个指针后移,答案 +1
else
i++; // 配对失败,说明这块醋饭太小,换下一块更大的醋饭尝试
}
cout << ans; // 输出最多能制作的寿司数量
return 0;
}
D - Repeatedly Repainting
题目描述
有一个 H H H 行 W W W 列的网格。位于从上往下第 i i i 行、从左往右第 j j j 列的单元格记为 ( i , j ) (i, j) (i,j)。
每个单元格被染成白色或黑色。网格由 H H H 个长度为 W W W 的字符串 S 1 , S 2 , ... , S H S_1, S_2, \ldots, S_H S1,S2,...,SH 描述:如果 S i S_i Si 的第 j j j 个字符是 .,则单元格 ( i , j ) (i, j) (i,j) 为白色;如果是 #,则为黑色。
执行以下操作 10 100 10^{100} 10100 次(次数足够多,可以认为操作会无限进行下去):
- 同时对所有单元格应用以下规则:
- 操作前为白色的单元格,当且仅当其至少有一个相邻的黑色单元格时,操作后变为黑色。(相邻的定义:两个单元格 ( x , y ) (x, y) (x,y) 和 ( x ′ , y ′ ) (x', y') (x′,y′)相邻当且仅当 max ( ∣ x − x ′ ∣ , ∣ y − y ′ ∣ ) = 1 \max(|x-x'|, |y-y'|) = 1 max(∣x−x′∣,∣y−y′∣)=1,即位于 8 邻域内)
- 操作前为黑色的单元格,操作后变为白色。
求操作完成后每个单元格的颜色。
解题思路
建议手玩几组数据观察规律。如果一个格子变成黑色,那么它之后会按照 白 -> 黑 -> 白 ... 的顺序交替变化。
因此,每个格子最终的颜色取决于离它最近的黑色格子的距离(奇偶性)。可以用 BFS 求出每个格子到最近黑格的距离,然后根据距离的奇偶性判断最终颜色。
代码
cpp
#include <bits/stdc++.h>
using namespace std;
int main()
{
int h, w; // h 为行数,w 为列数
cin >> h >> w; // 读取网格尺寸
vector<string> grid(h); // grid 存储初始网格状态
for (int i = 0; i < h; i++) // 读取 h 行网格数据
cin >> grid[i];
vector<string> v = grid; // v 用于存储操作一次后的网格状态
const int dx[8] = {-1, -1, -1, 0, 0, 1, 1, 1}; // 8 个方向的横坐标偏移量
const int dy[8] = {-1, 0, 1, -1, 1, -1, 0, 1}; // 8 个方向的纵坐标偏移量
// 第一步:根据初始状态计算操作一次后每个单元格的颜色
for (int i = 0; i < h; i++)
{
for (int j = 0; j < w; j++)
{
if (grid[i][j] == '#') // 初始为黑色,操作后变白色
v[i][j] = '.';
else // 初始为白色
{
// 检查 8 邻域是否有黑色单元格
for (int k = 0; k < 8; k++)
{
int ni = i + dx[k], nj = j + dy[k]; // 邻居坐标
if (ni >= 0 && ni < h && nj >= 0 && nj < w && grid[ni][nj] == '#')
{
v[i][j] = '#'; // 有相邻黑格,操作后变黑色
break;
}
}
}
}
}
// 第二步:对操作一次后的网格进行 BFS,计算每个单元格到最近黑格的距离
vector<vector<int>> dist(h, vector<int>(w, -1)); // dist 存储到最近黑格的距离
queue<pair<int, int>> q; // BFS 队列
// 将所有黑格加入队列作为 BFS 起点,距离设为 0
for (int i = 0; i < h; i++)
{
for (int j = 0; j < w; j++)
{
if (v[i][j] == '#')
{
dist[i][j] = 0; // 自身为黑格,距离为 0
q.push({i, j});
}
}
}
// BFS 遍历,计算每个单元格到最近黑格的距离
while (!q.empty())
{
auto [x, y] = q.front(); // 取出当前单元格
q.pop();
for (int k = 0; k < 8; k++) // 遍历 8 个方向
{
int nx = x + dx[k], ny = y + dy[k];
if (nx >= 0 && nx < h && ny >= 0 && ny < w && dist[nx][ny] == -1)
{
dist[nx][ny] = dist[x][y] + 1; // 更新距离
q.push({nx, ny});
}
}
}
// 第三步:根据距离奇偶性确定最终颜色
for (int i = 0; i < h; i++)
{
for (int j = 0; j < w; j++)
{
// 距离为奇数则最终为黑色,偶数或 -1(无黑格)则为白色
if (dist[i][j] != -1 && dist[i][j] % 2 == 1)
cout << '#';
else
cout << '.';
}
cout << '\n';
}
return 0;
}
E - x + y ≡ x + y
题目描述
对于正整数 a a a 和 b b b,定义 c o n c a t ( a , b ) \mathrm{concat}(a, b) concat(a,b) 为将 a a a 和 b b b 拼接起来得到的整数。更正式地, c o n c a t ( a , b ) \mathrm{concat}(a, b) concat(a,b) 定义如下:
- 设 A A A 和 B B B 分别是将 a a a 和 b b b 写成十进制得到的字符串。设 C C C 是将 A A A 和 B B B 按顺序拼接得到的字符串。将 C C C 作为十进制整数解释得到的值就是 c o n c a t ( a , b ) \mathrm{concat}(a, b) concat(a,b)。
例如,若 a = 123 a = 123 a=123, b = 45 b = 45 b=45,则 c o n c a t ( a , b ) = 12345 \mathrm{concat}(a, b) = 12345 concat(a,b)=12345。
给定正整数 N N N 和 M M M。求满足以下条件的正整数对 ( x , y ) (x, y) (x,y) 的数量,对 998244353 998244353 998244353 取模:
- x ≤ N x \le N x≤N, y ≤ N y \le N y≤N
- c o n c a t ( x , y ) ≡ x + y ( m o d M ) \mathrm{concat}(x, y) \equiv x + y \pmod{M} concat(x,y)≡x+y(modM)
给定 T T T 组测试数据,请分别求解。
解题思路
首先处理 c o n c a t ( x , y ) ≡ x + y ( m o d M ) \mathrm{concat}(x, y) \equiv x + y \pmod{M} concat(x,y)≡x+y(modM) 这个式子。
设 y y y 的十进制长度为 l e n ( y ) len(y) len(y),则 c o n c a t ( x , y ) = x × 10 l e n ( y ) + y \mathrm{concat}(x, y) = x \times 10^{len(y)} + y concat(x,y)=x×10len(y)+y。
代入得: x × 10 l e n ( y ) + y ≡ x + y ( m o d M ) x \times 10^{len(y)} + y \equiv x + y \pmod{M} x×10len(y)+y≡x+y(modM)
两边减去 y y y: x × 10 l e n ( y ) ≡ x ( m o d M ) x \times 10^{len(y)} \equiv x \pmod{M} x×10len(y)≡x(modM)
移项: x × ( 10 l e n ( y ) − 1 ) ≡ 0 ( m o d M ) x \times (10^{len(y)} - 1) \equiv 0 \pmod{M} x×(10len(y)−1)≡0(modM)
即 x × ( 10 l e n ( y ) − 1 ) x \times (10^{len(y)} - 1) x×(10len(y)−1) 是 M M M 的倍数。
设 g = gcd ( 10 l e n ( y ) − 1 , M ) g = \gcd(10^{len(y)} - 1, M) g=gcd(10len(y)−1,M),则 x x x 必须是 M / g M / g M/g 的倍数。
统计每个长度对应的 x x x 的个数和 y y y 的个数,相乘后求和即可。
代码
cpp
#include <bits/stdc++.h>
using namespace std;
using ll = unsigned long long;
const int p = 998244353; // 模数
ll f[20]; // f[i] 存储 10^i 的值
ll gcd(ll a, ll b) // 欧几里得算法求最大公约数
{
return (!b ? a : gcd(b, a % b));
}
void init() // 预处理 10 的幂
{
f[0] = 1;
for (int i = 1; i <= 19; i++) // 10^19 足够覆盖 10^18 范围的数
f[i] = f[i - 1] * 10;
}
void solve()
{
ll n, m; // n 为数值上限,m 为模数
cin >> n >> m;
ll val = n, len = 0; // 计算 n 的十进制长度
while (val)
val /= 10, len++;
ll res = 0; // 结果,存储满足条件的 (x, y) 对数
// 枚举 y 的长度,从 1 到 len-1(即 y 的位数小于 n 的位数)
for (int i = 1; i < len; i++)
{
// 设 g = gcd(10^i - 1, m),则 x 必须是 m/g 的倍数
ll k = m / gcd(f[i] - 1, m); // x 需要是 k 的倍数
ll cntx = n / k, cnty = f[i] - f[i - 1]; // cntx: 满足条件的 x 的个数
// cnty: 长度为 i 的 y 的个数(即 10^(i-1) 到 10^i - 1)
cntx %= p, cnty %= p; // 取模防止溢出
res = (res + (cntx * cnty % p)) % p; // 累加答案
}
// 处理 y 的长度等于 len 的情况(即 y 和 n 位数相同)
ll k = m / gcd(f[len] - 1, m); // x 需要是 k 的倍数
ll cntx = n / k, cnty = n - f[len - 1] + 1; // cnty: 长度为 len 且不超过 n 的 y 的个数
cntx %= p, cnty %= p;
res = (res + (cntx * cnty % p)) % p;
cout << res << '\n';
}
int main()
{
init(); // 预处理 10 的幂
int T; // 测试数据组数
cin >> T;
while (T--)
solve();
return 0;
}
F - Farthest Pair Query(最远点对查询)
题目描述
有一棵包含 N N N 个节点的树。节点编号为 1 , 2 , ... , N 1, 2, \ldots, N 1,2,...,N,第 i i i 条边连接节点 U i U_i Ui 和 V i V_i Vi。
初始时,所有节点都被染成黑色。
按顺序处理 Q Q Q 个查询,并输出每个查询的答案。
- 给定一个整数 x x x( 1 ≤ x ≤ N 1 \leq x \leq N 1≤x≤N)。如果节点 x x x 是白色,则将其重新染成黑色;如果节点 x x x 是黑色,则将其重新染成白色。然后,求所有黑色节点中两个节点之间的最大距离。(这里的两点间距离定义为树上两点之间简单路径的边数)
输入保证在处理每个查询时,树中至少有两个黑色节点。
解题思路
换个角度描述:假设所有黑色节点是"有效节点",白色是"无效节点"。需要在动态维护节点有效性的同时,查询任意时刻树中两个黑色节点之间的最大距离。
有一个重要性质(树的直径的性质):假设初始时树的直径的两个端点是 x , y x, y x,y,加入一个新节点 a a a 后,新的直径端点只可能是 ( x , y ) (x, y) (x,y)、 ( a , x ) (a, x) (a,x) 或 ( a , y ) (a, y) (a,y) 三种情况之一。
两点间距离可以通过 LCA(最近公共祖先)配合每个点的深度来求解。
对于删除有效节点的问题,使用线段树进行"离线"处理:将所有查询离线,根据时间区间建立线段树,每个节点维护一个时间区间。节点同时记录当前情况下直径的两个端点,然后执行插入和查询操作。
代码
cpp
#include <bits/stdc++.h>
using namespace std;
const int maxn = 2e5 + 10;
int n, q; // n 为节点数,q 为查询数
vector<int> g[maxn]; // g 存储树的邻接表
int f[maxn][20], dep[maxn]; // f 为二进制跳表(用于 LCA),dep 存储节点深度
int to[maxn], nxt[maxn], frt[maxn], idx; // 邻接表实现:to 存储目标节点,nxt 存储下一条边,frt 存储头指针
// 添加一条无向边
void add_edge(int x, int y)
{
to[++idx] = y;
nxt[idx] = frt[x];
frt[x] = idx;
}
// DFS 预处理:计算每个节点的深度和二进制跳表
void dfs(int x, int fa)
{
f[x][0] = fa; // x 的 2^0 祖先是 fa
dep[x] = dep[fa] + 1; // 深度比父节点多 1
for (int i = 1; i <= 19; i++) // 预处理二进制跳表
{
f[x][i] = f[f[x][i - 1]][i - 1];
}
for (int i = frt[x]; i; i = nxt[i]) // 遍历所有子节点
{
int j = to[i];
if (j == fa) // 跳过父节点
continue;
dfs(j, x);
}
}
// 求两个节点的最近公共祖先(LCA)
int lca(int x, int y)
{
if (dep[x] < dep[y]) // 确保 x 的深度大于等于 y
swap(x, y);
for (int i = 19; i >= 0; i--) // 将 x 提升到与 y 同一深度
{
if (dep[f[x][i]] >= dep[y])
{
x = f[x][i];
}
}
if (x == y) // 如果 x 和 y 相同,直接返回
return x;
for (int i = 19; i >= 0; i--) // 同时提升 x 和 y,直到它们最近公共祖先的子节点
{
if (f[x][i] != f[y][i])
{
x = f[x][i];
y = f[y][i];
}
}
return f[x][0]; // 返回最近公共祖先
}
// 求两个节点之间的距离
int getdis(int x, int y)
{
return dep[x] + dep[y] - 2 * dep[lca(x, y)];
}
// 线段树相关:获取左右子节点编号
int ls(int x) { return x << 1; }
int rs(int x) { return x << 1 | 1; }
vector<int> seg[maxn << 2]; // seg 为线段树,每个节点存储一个需要插入的节点列表
int nl, nr, ans[maxn]; // nl, nr 为当前直径的两个端点,ans 存储每个查询的答案
// 将节点 k 添加到覆盖区间 [L, R] 的线段树节点上
void modify(int x, int l, int r, int L, int R, int k)
{
if (L <= l && r <= R) // 当前区间完全被覆盖
{
seg[x].push_back(k); // 将节点 k 加入该线段树节点
return;
}
int mid = l + r >> 1;
if (L <= mid) // 递归处理左子区间
modify(ls(x), l, mid, L, R, k);
if (mid < R) // 递归处理右子区间
modify(rs(x), mid + 1, r, L, R, k);
}
// 深度优先遍历线段树,计算每个查询时刻的直径
void getans(int x, int l, int r)
{
int tl = nl, tr = nr; // 保存当前直径端点状态
// 将该线段树节点管理的所有节点依次插入,更新直径
for (int a : seg[x])
{
if (!nl) // 如果当前没有有效节点
{
nl = nr = a; // 初始化直径端点
}
else
{
int tmp = getdis(nl, nr); // 当前直径长度
int t1 = getdis(nl, a); // 新节点到左端点的距离
int t2 = getdis(nr, a); // 新节点到右端点的距离
if (t1 > t2)
{
if (t1 > tmp) // 新直径需要更新
nr = a;
}
else
{
if (t2 > tmp)
nl = a;
}
}
}
if (l == r) // 到达叶子节点,记录答案
{
ans[l] = getdis(nl, nr);
nl = tl, nr = tr; // 回溯,恢复状态
return;
}
int mid = l + r >> 1;
getans(ls(x), l, mid); // 递归处理左子区间
getans(rs(x), mid + 1, r); // 递归处理右子区间
nl = tl, nr = tr; // 回溯,恢复状态
}
int main()
{
cin >> n; // 读取节点数
for (int i = 1; i < n; i++) // 读取 n-1 条边
{
int u, v;
cin >> u >> v;
add_edge(u, v); // 添加无向边
add_edge(v, u);
}
dfs(1, 0); // 从节点 1 开始 DFS 预处理
cin >> q; // 读取查询数
vector<int> last(n + 1, 1); // last[i] 记录节点 i 上次变白的时间点,初始为 1
// 离线处理所有查询
for (int i = 1; i <= q; i++)
{
int x;
cin >> x; // 读取查询
if (last[x]) // 如果节点 x 之前是黑色
{
if (last[x] <= i - 1) // 将节点 x 在有效期间 [last[x], i-1] 添加到线段树
modify(1, 1, q, last[x], i - 1, x);
last[x] = 0; // 节点变为白色
}
else // 如果节点 x 之前是白色
{
last[x] = i; // 记录变黑的时间点
}
}
// 处理最后仍然为黑色的节点,它们在所有查询期间都有效
for (int i = 1; i <= n; i++)
{
if (last[i] && last[i] <= q)
{
modify(1, 1, q, last[i], q, i);
}
}
getans(1, 1, q); // 深度优先遍历线段树,计算每个查询的答案
// 输出所有查询的答案
for (int i = 1; i <= q; i++)
{
cout << ans[i] << '\n';
}
return 0;
}