AtCoder Beginner Contest 420 ABCDEG 题目解析

A - What month is it?

题意

\(X\) 月开始往后数 \(Y\) 个月是几月?

思路

直接取模运算或者循环 \(+1\) 判断均可。

代码

void solve()
{
    int x, y;
    cin >> x >> y;
    cout << (x + y - 1) % 12 + 1;
}

B - Most Minority

题意

\(N\) 个人(\(N\) 是奇数)进行了 \(M\) 次投票,每个人会投 0 或者 1

每次投票给定一个长度为 \(N\) 的仅由 01 组成的字符串,表示每个人的投票内容。

每次投票中,处于少数票中的所有人都会获得 \(1\) 分。

换言之,我们假设 \(x\) 个人投了 0\(y\) 个人投了 1

  • 如果 \(x=0\)\(y=0\),则所有人均获得 \(1\) 分。
  • 如果 \(x \lt y\),则所有投 0 的人均获得 \(1\) 分。
  • 如果 \(x \gt y\),则所有投 1 的人均获得 \(1\) 分。

问在进行完 \(M\) 次投票之后,分值最高(或并列最高)的人有哪些。

思路

每次输入字符串后按题意数出 01 的数量,然后分类讨论即可。

如果所有人都能获得 \(1\) 分,其实加或者不加均可,不影响互相之间的分值关系。

记录分值的部分可以采用计数数组。

代码

int val[105]; // val[i] 表示 i 的分值
char s[105][105];

void solve()
{
    int n, m;
    cin >> n >> m;
    
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++)
            cin >> s[i][j];
    
    for(int j = 1; j <= m; j++)
    {
        int cnt[2] = {0, 0}; // 统计第 j 轮投 0/1 的人数
        for(int i = 1; i <= n; i++)
            cnt[s[i][j] - '0']++;
        
        if(cnt[0] == 0 || cnt[1] == 0) // 全加 = 全不加
            continue;
        
        for(int i = 1; i <= n; i++)
            if(cnt[0] < cnt[1] && s[i][j] == '0' || cnt[1] < cnt[0] && s[i][j] == '1')
                val[i]++;
    }
    
    int mx = *max_element(val + 1, val + n + 1); // 找最高分
    for(int i = 1; i <= n; i++)
        if(val[i] == mx)
            cout << i << " ";
}

C - Sum of Min Query

题意

给定两个长度为 \(N\) 的序列 \(A = (A_1, A_2, \dots, A_N)\) 以及 \(B = (B_1, B_2, \dots, B_N)\)

你需要按顺序处理以下操作。第 \(i\) 次操作为:

  • 给定一个字符 \(c_i\) 以及两个整数 \(X_i, V_i\)
  • 如果 \(c_i = \texttt{A}\),则将 \(A_{X_i}\) 改为 \(V_i\);如果 \(c_i = \texttt{B}\),则将 \(B_{X_i}\) 改为 \(V_i\)
  • 每次修改完成后,输出 \(\displaystyle \sum_{k=1}^N \min(A_k,B_k)\).

思路

注意到每次只会修改一个位置,并且最终答案对于每个下标位置都是独立求和的,因此我们可以把未发生修改前的答案先预处理出来。

每发生一次更改,先把对应下标位置的 \(\min(A_k, B_k)\) 从答案中减去,修改完成后再把新的 \(\min(A_k, B_k)\) 加回来即可。

代码

int a[200005], b[200005];

void solve()
{
    int n, q;
    cin >> n >> q;
    for(int i = 1; i <= n; i++)
        cin >> a[i];
    for(int i = 1; i <= n; i++)
        cin >> b[i];
    
    long long sum = 0;
    for(int i = 1; i <= n; i++)
        sum += min(a[i], b[i]);
    
    while(q--)
    {
        char c;
        int x, v;
        cin >> c >> x >> v;
        
        sum -= min(a[x], b[x]); // 把修改前 x 位置的值去掉
        
        if(c == 'A')
            a[x] = v;
        else
            b[x] = v;
        
        sum += min(a[x], b[x]); // 把修改后 x 位置的值加上
        
        cout << sum << "\n";
    }
}

D - Toggle Maze

题意

有一个 \(H \times W\) 的网格,第 \(i\) 行第 \(j\) 列的网格状态以 \(A_{i, j}\) 表示,含义如下:

  • .:空。
  • #:障碍物。
  • S:起点。
  • G:终点。
  • o:有一扇打开的门。
  • x:有一扇关闭的门。
  • ?:有一个开关。

高桥从起点出发,每次可以从当前所在单元格移动到一个既不是 # 也不是 x 的相邻单元格上(上、下、左、右)。

此外,每当他移动到 ?(包含开关的单元格)时,所有打开的门都会关闭,而所有关闭的门都会打开(即所有 ox,所有 xo)。

试判断高桥是否能够从起点走到终点。如果能,请输出最少的移动次数。

思路

明显是一道宽搜题,但与普通宽搜题的区别主要在于地图中多了三种情况:打开的门、关闭的门、开关。

对于包含开关的格子而言,每遇到一次,都会对整张图所有门的属性产生影响。暴力更改肯定是不可行的,但我们可以在宽搜的过程中记录当前已经遇到了多少次包含开关的格子,这样就可以通过当前遇到的门的开关状态以及使用开关的次数来推断出下一个点能不能走了。

使用两次开关和没有使用是一样的,所以我们只需要知道使用开关的次数的奇偶性即可。在搜索过程中可以开一个状态 changed 来表示目前是否需要反转所有门的属性。

其余部分与宽搜相同。

代码

const int dx[4] = {-1, 1, 0, 0};
const int dy[4] = {0, 0, -1, 1};

struct point
{
    int x, y, sta, step;
    // 当前行、列、门的反转状态、总步数
};

int n, m;
char mp[505][505];

bool vis[505][505][2];
// vis[x][y][s] 表示到 (x, y) 这个位置,且(s=0 表示门为原样,s=1 表示所有门属性反转)的情况是否已经搜索过

void solve()
{
    queue<point> q;
    
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++)
        {
            cin >> mp[i][j];
            if(mp[i][j] == 'S')
            {
                q.push(point{i, j, 0, 0});
                vis[i][j][0] = true;
            }
        }
    
    while(!q.empty())
    {
        point u = q.front();
        q.pop();
        for(int i = 0; i < 4; i++)
        {
            point v;
            v.x = u.x + dx[i];
            v.y = u.y + dy[i];
            v.sta = u.sta;
            v.step = u.step + 1;
            
            if(v.x < 1 || v.x > n || v.y < 1 || v.y > m)
                continue;
            
            if(mp[v.x][v.y] == '#')
                continue;
            else if(mp[v.x][v.y] == 'o' && v.sta == 1) // 开着的门,但是反转了
                continue;
            else if(mp[v.x][v.y] == 'x' && v.sta == 0) // 关着的门,且没有反转
                continue;
            else if(mp[v.x][v.y] == '?') // 遇到开关
                v.sta = v.sta ^ 1; // 0 -> 1, 1 -> 0
            else if(mp[v.x][v.y] == 'G')
            {
                cout << v.step;
                return;
            }
            
            if(vis[v.x][v.y][v.sta])
                continue;
            q.push(v);
            vis[v.x][v.y][v.sta] = true;
        }
    }
    
    cout << -1;
}

E - Reachability Query

题意

给定一张初始包含 \(N\) 个点和 \(0\) 条边的无向图。

每个点分别编号为 \(1, 2, \dots, N\),且初始时都是白色的。

请按顺序处理以下 \(Q\) 次操作,每次操作为以下三种之一:

  • 类型 \(1\):加一条连接 \(u, v\) 两点的无向边。
  • 类型 \(2\):黑白翻转点 \(v\) 的颜色。
  • 类型 \(3\):问从点 \(v\) 出发经过一些边,是否能够走到某个黑色点。

思路

根据类型 \(3\),我们只需要能够维护出每个连通块内是否存在黑色点即可。

考虑并查集,同时维护每个并查集内部的黑色点数。

当出现操作 \(1\) 时,合并两个并查集,同时合并原本两个集合内的黑色点数。

当出现操作 \(2\) 时,翻转点 \(v\) 的颜色,如果是白变黑,则让点 \(v\) 所在集合内的黑色总点数 \(+1\),反之 \(-1\) 即可。

当出现操作 \(3\) 时,查询点 \(v\) 所在集合内部是否存在至少一个黑色点即可。

代码

int clr[200005]; // clr[i] 表示 i 点当前的颜色,0 表示白,1 表示黑

int fa[200005];
int cnt[200005]; // cnt[i] 表示以 i 为根结点的集合内部有多少个黑色点

int find(int p)
{
    return p == fa[p] ? p : (fa[p] = find(fa[p]));
}
void merge(int a, int b) // 合并两集合
{
    a = find(a), b = find(b);
    if(a == b)
        return;
    fa[a] = b; // a -> b 在并查集内建边,b 作为新集合根结点
    cnt[b] += cnt[a]; // 把以 a 为根结点时集合内的黑色点数加给 b
    // cnt[a] = 0;
}

void solve()
{
    int n, q;
    cin >> n >> q;
    
    for(int i = 1; i <= n; i++)
        fa[i] = i;
    
    while(q--)
    {
        int op, u, v;
        cin >> op;
        if(op == 1)
        {
            cin >> u >> v;
            merge(u, v);
        }
        else if(op == 2)
        {
            cin >> v;
            int f = find(v); // v 点当前所在集合的根结点
            cnt[f] -= clr[v]; // 减去此前点 v 的影响
            clr[v] ^= 1; // 翻转 v 的颜色
            cnt[f] += clr[v]; // 加上现在点 v 的影响
        }
        else
        {
            cin >> v;
            if(cnt[find(v)] > 0) // 只要 v 所在集合内的黑色点数 >0
                cout << "Yes\n";
            else
                cout << "No\n";
        }
    }
}

G - sqrt(n²+n+X)

题意

给定一个整数 \(X\),请输出满足 \(\sqrt{n^2 + n + X}\) 为整数的所有整数 \(n\)

思路一

假设开方后得到的整数值为 \(m\) \((m \ge 0)\),可得:

\[\begin{aligned} \sqrt{n^2 + n + X} &= m \\ n^2 + n + X &= m^2 \\ X &= m^2 - (n^2 + n) \\ X &= m^2 - (n^2 + n + \frac 1 4) + \frac 1 4 \\ 4X &= 4m^2 - (4n^2 + 4n + 1) + 1 \\ 4X &= (2m)^2 - (2n + 1)^2 + 1 \\ 4X &= [2m + (2n+1)] \times [2m - (2n+1)] + 1 \\ 4X - 1 &= (2m + 2n + 1) \times (2m - 2n - 1) \end{aligned} \]

因为 \(X, m, n\) 均是整数,所以我们可以尝试找出所有 \(4X-1\) 的因数(包括正负)。

假设 \(d\)\(4X-1\) 的其中一个因数,又因为:

\[d \times \dfrac{4X-1}{d} = 4X-1 = (2m + 2n + 1) \times (2m - 2n - 1) \]

接下来我们只需要判断是否存在一对整数 \((n, m)\),满足:

  • \(2m+2n+1 = d\)
  • \(2m-2n-1 = \dfrac{4X-1}{d}\)
  • \(m \ge 0\) (因为算术平方根不为负数)

若存在,则此时的 \(n\) 即所求。

\(O(\sqrt X)\) 的复杂度内枚举所有 \(4X-1\) 的因数,然后解二元一次方程即可。

代码一

ll x, y;
set<ll> st;

void check(ll d)
{
    // A   2m + 2n + 1 == d
    // B   2m - 2n - 1 == y / d
    
    // A-B  ->  4m == d + y / d
    // A-B  ->  4n == d - y / d - 2
    
    if((d + y / d) % 4 != 0)
        return;
    if((d - y / d - 2) % 4 != 0)
        return;
    st.insert((d - y / d - 2) / 4);
}

void solve()
{
    cin >> x;
    y = 4 * x - 1;
    
    for(ll d = 1; d * d <= abs(y); d++)
    {
        if(y % d != 0)
            continue;
        check(d);
        check(y / d);
        check(-d);
        check(-y / d);
    }
    
    cout << st.size() << "\n";
    for(ll i : st)
        cout << i << " ";
}

思路二

要让表达式开方后为整数,不妨假设开方后得到的整数值为 \(n + k\),其中 \(k\) 可为任意整数,得:

\[\begin{aligned} \sqrt{n^2 + n + X} &= n + k \\ n^2 + n + X &= (n + k)^2 \\ n^2 + n + X &= n^2 + 2kn + k^2 \\ 2kn - n &= X - k^2 \\ n &= \frac{X - k^2}{2k - 1} \end{aligned} \]

此时只要我们能够找到任意整数,使得 \(X-k^2\)\(2k-1\) 的倍数,此时两者相除后的商即为所求。

\(d = 2k - 1\),即 \(k = \dfrac{d + 1}{2}\),则:

\[\begin{aligned} X-k^2 &= X-(\frac{d+1}2)^2 \\ &= X - \frac{d^2 + 2d + 1}{4} \\ &= \frac{4X - d^2 - 2d - 1}{4} \end{aligned} \]

我们需要让上式能被 \(d\) 整除,又因为 \(d^2\)\(2d\) 已经是 \(d\) 的倍数,所以我们只需要保证 \(4X-1\)\(d\) 的倍数即可。

换言之,我们只需要保证 \(4X-1\)\(2k-1\) 的倍数即可。

\(O(\sqrt X)\) 的时间复杂度内枚举一遍 \(4X-1\) 的所有因数(包括正负),通过因数求出 \(k\) 的值,再回到最开始的式子中求出所有可能的 \(n\) 即可。

但注意这种方法在因数较大时,可能会导致 \(k^2\) 超出 long long 的类型,建议采用 __int128 进行计算。

代码二

typedef long long ll;
typedef __int128 lll;

ll x, y;
set<lll> st;

void check(ll d)
{
    // d == 2k-1
    // k == (d+1)/2
    if((d + 1) % 2 != 0)
        return;
    lll k = (d + 1) / 2;
    st.insert((x - k * k) / (2 * k - 1));
}

void out(lll d)
{
    if(d < 0)
    {
        cout << "-";
        out(-d);
    }
    else
    {
        if(d > 9)
            out(d / 10);
        cout << int(d % 10);
    }
}

void solve()
{
    cin >> x;
    y = 4 * x - 1;
    
    for(ll d = 1; d * d <= abs(y); d++)
    {
        if(y % d != 0)
            continue;
        check(d);
        check(y / d);
        check(-d);
        check(-y / d);
    }
    
    cout << st.size() << "\n";
    for(lll i : st)
    {
        out(i);
        cout << " ";
    }
}
posted @ 2025-08-24 22:36  StelaYuri  阅读(44)  评论(0)    收藏  举报