atcoder ABC 460 题解

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;
}
相关推荐
ʚ希希ɞ ྀ1 小时前
全排列 --- 回溯
算法·leetcode·深度优先
水无痕simon1 小时前
9 C语言的基础练习
c语言·开发语言·算法
少司府1 小时前
C++进阶:二叉搜索树
开发语言·数据结构·c++·二叉树·stl·二叉搜索树·tree
8Qi81 小时前
LeetCode 124. 二叉树中的最大路径和(Hard)
算法·leetcode·二叉树·递归
Huangjin007_1 小时前
【C++ STL篇(十四)】哈希表实现:开放定址法与链地址法
c++·哈希算法·散列表
不会就选b1 小时前
数据结构之单链表
数据结构
And_Ii1 小时前
LeetCode 1. 两数之和 python
数据结构·算法·leetcode
承渊政道1 小时前
【MySQL数据库学习】MySQL表的约束(上)
数据库·c++·学习·mysql·bash·数据库架构·数据库系统
minji...1 小时前
Linux高级IO(六)基于ET模式、单reactor反应堆的epoll版本的TCP计算服务器
linux·服务器·网络·c++·epoll·socket套接字·reactor反应堆模式