AtCoder Beginner Contest 410 ABCDEF 题目解析

A - G1

题意

\(N\) 场赛马比赛,第 \(i\) 场比赛只允许年龄不超过 \(A_i\) 的马参加。

问年龄为 \(K\) 的马能够参加多少场比赛。

思路

判断有多少个数字 \(\ge K\) 即可。

代码

int n, k, a[105];

void solve()
{
    cin >> n;
    for(int i = 1; i <= n; i++)
        cin >> a[i];
    cin >> k;
    
    int cnt = 0;
    for(int i = 1; i <= n; i++)
        if(k <= a[i])
            cnt++;
    cout << cnt;
}

B - Reverse Proxy

题意

\(N\) 个盒子,编号分别为 \(1, 2, \dots , N\),一开始盒子都是空的。

\(Q\) 个球要按顺序放进盒子里。

给定一个操作序列 \(X=(X_1, X_2,\dots, X_Q)\),对于第 \(i\) 个球:

  • 如果 \(X_i \ge 1\),那么第 \(i\) 个球会被放在编号为 \(X_i\) 的盒子里。
  • 如果 \(X_i = 0\),那么第 \(i\) 个球会被放在当前包含球的数量最少的那些盒子中编号最小的那个盒子里。

思路

模拟题,可以用一个计数数组统计每个盒子内部当前有多少个球。

如果 \(X_i = 0\),就先通过打擂台的方法找出包含球的最少数量是多少,然后再从前往后扫一遍数组,找数量最少且编号最小的那个盒子即可。

代码

int cnt[105];
// cnt[x] 表示 x 这个盒子里的球数

void solve()
{
    int n, q;
    cin >> n >> q;
    for(int i = 1; i <= q; i++)
    {
        int x;
        cin >> x;
        if(x > 0)
        {
            cnt[x]++;
            cout << x << " ";
        }
        else
        {
            int minn = 1000; // 包含球的最少数量
            for(int j = 1; j <= n; j++)
                minn = min(minn, cnt[j]);
            
            for(int j = 1; j <= n; j++)
                if(cnt[j] == minn) // j 即编号最小且包含球数量最少的盒子
                {
                    cnt[j]++;
                    cout << j << " ";
                    break;
                }
        }
    }
}

C - Rotatable Array

题意

有一个长度为 \(N\) 的序列 \(A\),初始时 \(A_i = i\)

\(Q\) 次操作,每次操作为以下三种之一:

  • 1 p x:表示将 \(A_p\) 改为 \(x\)
  • 2 p:输出 \(A_p\) 的值。
  • 3 k:执行“将第一个数字移动到序列最后面”这个操作 \(k\) 次。

思路

重点就在于移动这个操作,根据数据范围 \(k \le 10^9\),明显我们不能真的去移动序列。

但可以注意到,如果把第一个数移动到序列最后面,无非就是把原本的第二个数变成当前序列的第一个数而已。并且如果移动了 \(N\) 次,相当于没有移动。

所以我们可以借助一个变量 \(s\) 专门用来表示当前序列的开头应该在原本的序列的哪个位置。

如果要执行 \(k\) 次循环操作,只需要让 \(s\) 变量变为 \(s + k\) 即可。但注意如果超出 \(N\) 则变回 \(1\)

由于上文提到变化 \(N\) 次相当于没有变化,所以这里可以借助取模操作快速求出最终 \(s\) 的值。但要注意下标范围在 \([1, N]\) 之间时,\(N\) 除以 \(N\) 的余数为 \(0\),要特判一下,或者可以先 \(-1\),取模之后再 \(+1\),即:

s = (s + k - 1) % n + 1;

对于查询操作,既然我们知道当前序列开头在下标 \(s\) 位置,为了找到当前的 \(p\) 位置,那就只需要在原本的序列里从 \(s\) 出发往后数 \(p-1\) 个位置即可。这里也可以采取类似的取模操作:

p = (s + (p - 1) - 1) % n + 1;
p = (s + p - 2) % n + 1;

代码

int a[1000005];

void solve()
{
    int n, q;
    cin >> n >> q;
    for(int i = 1; i <= n; i++)
        a[i] = i;
    
    int s = 1; // 当前开头在原来数组的哪个位置
    
    while(q--)
    {
        int op;
        cin >> op;
        if(op == 1)
        {
            int p, x;
            cin >> p >> x;
            p = (st + p - 2) % n + 1;
            a[p] = x;
        }
        else if(op == 2)
        {
            int p;
            cin >> p;
            p = (st + p - 2) % n + 1;
            cout << a[p] << "\n";
        }
        else
        {
            int k;
            cin >> k;
            st = (st + k - 1) % n + 1;
        }
    }
}

D - XOR Shortest Walk

题意

给定一张包含 \(N\) 个点以及 \(M\) 条边的带权有向图,你需要找出一条从 \(1\)\(N\) 的路径,路上可以经过任意次相同的点或者相同的边,记这条路径的权值为路上走过的所有边的边权按位异或和

请求出最小的权值,或判断不存在任何一条从 \(1\)\(N\) 的路径。

思路

本题的入手点在于数据范围。注意到 \(0 \le W_i \lt 2^{10} = 1024\),不论异或上多少个在此范围内的边权,最终的异或和都会保持在 \([0, 1023]\) 的范围内。

又因为本题总点数最多 \(1000\),对于每个点而言,从 \(1\) 号点走到当前点的过程中经过的所有边权异或和最多也就 \(1024\) 种可能,因此可以开一个 bool 类型的数组,用 vis[i][j] 专门记录是否在搜索过程中遇到过\(1\) 出发走到 \(i\) 点,且边权异或和为 \(j\) 的情况。这个数组的每个状态如果出现过,则只需要搜一次,因此总共搜索的次数不会超过 \(1000 \times 1024\) 次。

等整张图全部搜索完成后,针对 \(n\) 号点看看被搜过的最小边权是多少即可。

时间复杂度 \(O(NW)\)

代码

typedef pair<int, int> pii;

int n, m;
vector<pii> G[1005]; // G[i] 邻接表 存所有与 i 相关的边 所连接的下一个点 及其 边权
bool vis[1005][1024];
// vis[i][j] 表示从 1 到 i 且边权异或和为 j 的情况是否已经搜过

void solve()
{
    cin >> n >> m;
    for(int i = 1; i <= m; i++)
    {
        int u, v, w;
        cin >> u >> v >> w;
        G[u].push_back(pii(v, w));
    }
    
    queue<pii> q;
    q.push(pii(1, 0)); // 一开始在 1 号点,边权异或和为 0
    vis[1][0] = true;
    
    while(!q.empty())
    {
        pii u = q.front(); // <当前点, 当前异或和>
        q.pop();
        for(pii &p : G[u.first]) // 搜从 当前点 出发能到达的下一个点 以及这条边的边权
        {
            pii v = pii(p.first, u.second ^ p.second); // 假设从 u -> v,权值异或上当前边的边权
            if(vis[v.first][v.second]) // 这个状态已经搜索过
                continue;
            q.push(v);
            vis[v.first][v.second] = true;
        }
    }
    
    for(int i = 0; i < 1024; i++) // 从小到大找最小的能被搜到的边权
        if(vis[n][i])
        {
            cout << i;
            return;
        }
    cout << -1;
}

E - Battles in a Row

题意

高桥将按顺序\(N\) 只怪物战斗。一开始他的体力为 \(H\),魔力为 \(M\)

当他与第 \(i\) 只怪物战斗时,它可以选择以下两种方法打败这只怪物:

  • 仅当当前体力不小于 \(A_i\) 时可选择。高桥不使用魔法,降低自身体力 \(A_i\) 点,从而战胜这只怪物。
  • 仅当当前魔力不小于 \(B_i\) 时可选择。高桥使用魔法,降低自身魔力 \(B_i\) 点,从而战胜这只怪物。

当所有怪物都被打败,或者高桥无法采取任何行动时,战斗结束。

问在战斗结束之前,高桥最多可以战胜多少只怪物?

思路

首先可以发现,如果高桥有办法战胜前 \(x\) 只怪物,那么如果我们只将目标定为打败前 \(x-1\) 只怪物,那么这一目标也是一定能够实现的。

所以最终能够打败的怪物数量符合二分的性质,可以借助二分先确定我们想要打败多少只怪物,然后去判断是否能打败这么多只。

考虑如何判断。假设目前我们的目标是打败前 \(n\) 只怪物,可以先假设全部采用第一种方法,也就是用体力硬拼过去。如果拼完后体力还没变成负数,那就直接判断可行。如果变成负数了,我们记这时候的体力为 \(-h\),那么我们再考虑在前面的某几场战斗当中,将原本采用的第一种方法换成第二种方法,也就是尝试用魔力把我们缺少的 \(h\) 点体力慢慢换回来。

那接下来的问题就变成了:对于 \(1 \sim n\) 中的第 \(i\) 场战斗,如果选择换为第二种方法进行,那么我们需要花费 \(B_i\) 点魔力,从而换取 \(A_i\) 点体力。我们的目标是在花费的魔力总数不超过 \(M\) 的前提下,至少换回 \(h\) 点体力。

这很明显是一个 0/1 背包问题,套上去直接解决即可。

时间复杂度 \(O(NM\log N)\)

代码

int n, h, m;
int a[3005], b[3005];

int dp[3005]; // dp[i] 表示花费 i 点魔力最多能换回多少点体力

bool check(int N)
{
    int H = 0;
    for(int i = 1; i <= N; i++)
        H += a[i];
    if(H <= h) // 体力没用完
        return true;
    
    H = H - h; // 需要换回超出的部分体力
    
    memset(dp, 0, sizeof dp);
    for(int i = 1; i <= N; i++)
    {
        // 选择后 代价为 b[i] 价值为 a[i]
        for(int j = M; j >= b[i]; j--)
            dp[j] = max(dp[j], dp[j - b[i]] + a[i]);
    }
    
    return dp[M] >= H; // 只要花费 M 点魔力能够换回的体力数量超过 H 即可满足条件
}

void solve()
{
    cin >> n >> h >> m;
    for(int i = 1; i <= n; i++)
        cin >> a[i] >> b[i];
    int l = 1, r = n, ans = 0;
    while(l <= r)
    {
        int mid = (l + r) / 2;
        if(check(mid))
        {
            ans = mid;
            l = mid + 1;
        }
        else
            r = mid - 1;
    }
    cout << ans;
}

F - Balanced Rectangles

题意

给定一个 \(H \times W\) 的网格图,每个网格仅由 #. 组成。

我们将网格图看作一个大矩形,请找出对于这个矩形,有多少个子矩形内部的 #. 数量相同。

思路

首先将字符转为数字,可以考虑用 \(-1\)\(1\) 替换掉原本的 #. 两种字符,那么我们的问题就变成了:找有多少个子矩形,内部数字总和为 \(0\)

求一个二维数组内部子矩形的总和,可以借助二维前缀和来快速获得,因此可以先处理出这个二维前缀和数组。

需要注意,因为本题没有明确说明 \(H, W\) 的最大值,因此可能需要用到动态数组来存储二维前缀和。

接下来,我们可以暂时先假设行数不大于列数 \((H \le W)\),此时根据题意,在 \(H \times W\) 取到最大时,\(H \le \sqrt{3 \times 10^5} \approx 547\)

然后我们 \(O(H^2)\) 枚举最终的子矩形的两行,记作 \(i, j\) \((i \le j)\),然后考虑如何快速统计有多少个子矩形是以 \(i, j\) 这两行作为其上下边界的。

我们先考虑对于一个一维数组 \(A_1, A_2, \dots, A_N\),应该怎么快速找出有多少个不同区间,内部数值之和为 \(0\)。如果我们知道这个一维数组的前缀和数组为 \(S_1, S_2, \dots, S_N\),明显如果某段区间 \([i, j]\) 的总和为 \(0\),那么 \(S_{i-1} = S_j\) 应当是成立的。(\(S_0 = 0\)

因此,我们只需要对于这个一维前缀和数组,从前往后扫一遍,对于每个位置 \(i\),看看在 \(i\) 之前有多少个值和我一样的位置,这就是以 \(i\) 作为右边界时,符合条件的左边界数量,直接计入答案即可。

回到二维前缀和数组,因为此时子矩形的两行已经固定了,所以我们可以借助二维前缀和数组,将这两行之间的每一列先压缩成一个一维数组,然后像上面说的一维数组的处理方法一样,去统计答案即可。

统计最终答案时,发现本题子矩阵的总和的范围在 \([-3\times 10^5, 3 \times 10^5]\) 之间,因此可以开一个大小为 \(6 \times 10^5\) 的计数数组帮助统计每种前缀和出现的次数,在使用时让每个数字的下标偏移 \(+300\,000\),把范围变成 \([0, 6\times 10^5]\) 即可。

而当行数大于列数 \((H \gt W)\) 时,我们可以反过来,\(O(W^2)\) 去枚举最终子矩形的两列,然后在行上进行答案的处理。

在这种分类讨论下,处理答案的时间复杂度是 \(O(\max(H, W))\) 的,枚举两边界的复杂度为 \(O(\min(H, W) ^ 2)\),极限数据大概出现在 \(H = W\) 的情况,因此最终时间复杂度可以大致记作是 \(O(\sqrt{H\times W}^3)\)

代码

vector<vector<int>> G; // 存二位前缀和

int cnt[600005]; // 计数数组,辅助统计答案
int a[300005]; // 把两行/两列之间的数字 压缩成一个一维数组,并取其前缀和,暂存于 a 数组内

// 求以 (xa, ya) 为左上角,(xb, yb) 为右下角的子矩阵总和
int f(int xa, int ya, int xb, int yb)
{
    return G[xb][yb] - G[xa-1][yb] - G[xb][ya-1] + G[xa-1][ya-1];
}

void solve()
{
    int n, m;
    cin >> n >> m;
    
    G.clear();
    
    G.push_back(vector<int>(m + 1, 0)); // 第 0 行
    for(int i = 1; i <= n; i++)
    {
        vector<int> v;
        v.push_back(0); // 第 0 列
        for(int j = 1; j <= m; j++)
        {
            char c;
            cin >> c;
            if(c == '.')
                v.push_back(-1); // 用 -1 和 1 替代字符
            else
                v.push_back(1);
        }
        G.push_back(v);
    }
    
    // 求一遍二位前缀和
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++)
            G[i][j] = G[i][j] + G[i-1][j] + G[i][j-1] - G[i-1][j-1];
    
    long long ans = 0;
    
    cnt[300000] = 1; // cnt[0] 偏移后的位置,即前缀和数组下标为 0 的位置应当有一个总和为 0 的情况
    
    if(n <= m) // 行较小 先枚举两行
    {
        for(int i = 1; i <= n; i++)
            for(int j = i; j <= n; j++)
            {
                for(int k = 1; k <= m; k++)
                    a[k] = f(i, 1, j, k) + 300000; // 求出行在 [i,j] 之间,列在 [1,k] 之间的子矩阵总和
                for(int k = 1; k <= m; k++)
                    ans += cnt[a[k]]++; // 加上前面出现的 a[k] 的次数,然后 a[k] 自增
                for(int k = 1; k <= m; k++)
                    cnt[a[k]]--; // 把上一个循环的影响清空
            }
    }
    else // 下同,列较小则先枚举两列
    {
        for(int i = 1; i <= m; i++)
            for(int j = i; j <= m; j++)
            {
                for(int k = 1; k <= n; k++)
                    a[k] = f(1, i, k, j) + 300000;
                for(int k = 1; k <= n; k++)
                    ans += cnt[a[k]]++;
                for(int k = 1; k <= n; k++)
                    cnt[a[k]]--;
            }
    }
    
    cout << ans << "\n";
}
posted @ 2025-06-14 22:47  StelaYuri  阅读(123)  评论(0)    收藏  举报