AtCoder Beginner Contest 461 ABCDE

A - Armor

  • 预估难度:入门

题意

高桥的铠甲能够抵挡所有威力不超过 \(D\) 的攻击,但无法抵挡威力大于 \(D\) 的攻击。

请问这件铠甲能否抵挡一次威力为 \(A\) 的攻击?

代码

void solve()
{
    int a, d;
    cin >> a >> d;
    if(a <= d)
        cout << "Yes";
    else
        cout << "No";
}

B - The Honest Woodcutters

  • 预估难度:入门

题意

\(N\) 名伐木工,编号分别为 \(1, 2, \dots, N\),每人拥有一把斧头。
某天,他们都不慎将斧头掉进了池塘里。
随后,在池塘底发现了 \(N\) 把编号分别为 \(1, 2, \dots, N\) 的斧头。

编号为 \(i\) 的伐木工声称他拥有的是编号为 \(A_i\) 的斧头。
另一方面,池塘女神知道:原本真正拥有编号为 \(i\) 的斧头的伐木工是编号为 \(B_i\) 的伐木工。

请判断这 \(N\) 名伐木工是否全都在说真话。

思路

编号为 \(i\) 的斧头属于伐木工 \(B_i\),而伐木工 \(B_i\) 声称编号为 \(A_{B_i}\) 的斧头是他的。

因此,只需要对于每把斧头 \(i\),判断 \(A_{B_i}\) 是否为 \(i\),即可验证伐木工 \(B_i\) 是否说了假话。

代码

int a[105], b[105];

void solve()
{
    int n;
    cin >> n;
    for(int i = 1; i <= n; i++)
        cin >> a[i];
    for(int i = 1; i <= n; i++)
    {
        cin >> b[i];
        if(a[b[i]] != i)
        {
            cout << "No";
            return;
        }
    }
    cout << "Yes";
}

C - Variety

  • 预估难度:普及-
  • 标签:排序、贪心、计数思想

题意

\(N\) 颗宝石。第 \(i\) 颗宝石的颜色为 \(C_i\),价值为 \(V_i\)

请从这 \(N\) 颗宝石中选择 \(K\) 颗,要求选出的宝石必须包含至少 \(M\) 种不同的颜色。

求所选宝石的总价值的最大可能值。

保证一定存在满足条件的选择方案。

思路

有两个必须满足的条件:颜色至少 \(M\) 种,数量恰好 \(K\) 颗。由于题目保证 \(M \le K\),因此必定有解。

为了让选择的总价值最大,可以先把所有宝石按价值从高到低排序。

但如果我们直接选择价值最高的 \(K\) 颗宝石,可能颜色种类数 \(\ge M\) 的条件会无法满足。因此可以先考虑颜色种类数的条件。

按价值从高到低的顺序看一遍所有宝石。对于每块宝石,只要在这之前还没选过对应颜色的其它宝石,就可以把这块宝石给选过来,直到选出恰好 \(M\) 块宝石(对应 \(M\) 种颜色)为止,以此让颜色种类数的条件先满足。

接下来为了让总价值最大,只要继续选择剩余还没被选过的宝石当中价值最高的那些宝石,凑满 \(K\) 颗即可。

实现过程可以借助结构体排序,借助两个计数数组标记每种颜色/每颗宝石是否已被选过。

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

代码

struct gem
{
    int c, v;
    bool operator < (const gem &g) const
    {
        return v > g.v; // 按价值从大到小排序
    }
};

gem a[200005];
bool used[200005]; // used[i] 标记 a[i] 这块宝石是否已被选择
bool vis[200005];  // vis[i] 标记颜色为 i 的这种宝石是否已被选择了至少一次

void solve()
{
    int n, k, m;
    cin >> n >> k >> m;
    for(int i = 1; i <= n; i++)
        cin >> a[i].c >> a[i].v;
    
    sort(a + 1, a + n + 1);
    
    int cnt = 0; // 已经选了多少颗宝石
    long long sum = 0; // 总价值
    
    // 先选出价值最高的 m 种颜色的宝石各一颗
    for(int i = 1; i <= n && cnt < m; i++)
    {
        if(vis[a[i].c]) // 这种颜色已经选过了
            continue;
        vis[a[i].c] = true; // 标记这种颜色已选择
        used[i] = true;     // 标记这颗宝石已选择
        cnt++;
        sum += a[i].v;
    }
    
    // 再重新按价值从高到低,从没选的宝石中继续选择,凑满 k 颗
    for(int i = 1; i <= n && cnt < k; i++)
    {
        if(used[i]) // 这颗宝石已被选择
            continue;
        cnt++;
        sum += a[i].v;
    }
    
    cout << sum;
}

D - Count Subgrid Sum = K

  • 预估难度:普及/提高-
  • 标签:枚举、二维前缀和、计数思想

题意

有一个 \(H \times W\) 的网格,每个单元格中都包含一个整数 \(0\)\(1\)

请找出所有内部整数之和等于 \(K\) 的矩形区域的数量。

思路

为快速求解子矩形的内部总和,可以先套上二维前缀和。

暴力的枚举上下左右边界是 \(O(N^2M^2)\) 的复杂度,不可行,考虑优化一层枚举。

当我们 \(O(N^2)\) 枚举完上下边界 \([t, b]\) 后,便可以将 \([1, m]\) 每一列的数字总和看作一个整体,将二维问题转为一维问题。

接下来暂记 \(A_i\) 表示第 \(i\) 列上的第 \(t\) 行到第 \(b\) 行的总和,这可以借助二维前缀和在 \(O(M)\) 复杂度内求出。

由于目前左右边界尚未确定,因此问题变成了在一维数组内找有多少段区间的总和恰好等于 \(K\)

\(S_i\) 表示 \(A_i\) 的前缀和,即 \(S_i\) 表示第 \(t\) 行到第 \(b\) 行,第 \(1\) 列到第 \(i\) 列的总和。

考虑枚举右端点 \(r\),在右端点确定的情况下,由于区间 \([l, r]\) 的总和可以直接借助 \(S_r - S_{l-1}\) 求出,因此我们只需要找有多少个左端点 \(l\) 满足 \(S_r - S_{l-1} = K\) 即可,即 \(S_{l-1} = S_r - K\)

接下来只需要借助计数数组统计左侧有多少位置的前缀和符合条件即可。

时间复杂度 \(O(N^2M)\)

代码

int n, m, k;
int s[505][505]; // 二维前缀和数组

int cnt[250005]; // 计数

// 求上下边界为 [t, b],左右边界为 [l, r] 的子矩阵总和
int f(int t, int b, int l, int r)
{
    return s[b][r] - s[t-1][r] - s[b][l-1] + s[t-1][l-1];
}

void solve()
{
    cin >> n >> m >> k;
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++)
        {
            char x;
            cin >> x;
            s[i][j] = s[i-1][j] + s[i][j-1] - s[i-1][j-1] + (x - '0');
        }
    
    long long ans = 0;
    for(int t = 1; t <= n; t++)
        for(int b = t; b <= n; b++) // 枚举上下边界
        {
            cnt[0]++; // l=1 时,对应的 l-1 = 0,前 0 列前缀和为 0
            for(int i = 1; i <= m; i++)
            {
                // 当上下边界为 [t, b] 时,求前 i 列前缀和
                int s = f(t, b, 1, i);
                if(s - k >= 0)
                    ans += cnt[s - k]; // 左侧总和为 s-k 的列数 即答案
                cnt[s]++;
            }
            // 清空 cnt 数组
            cnt[0]--;
            for(int i = 1; i <= m; i++)
            {
                int s = f(t, b, 1, i);
                cnt[s]--;
            }
        }
    cout << ans;
}

E - E-liter

  • 预估难度:普及+/提高
  • 标签:树状数组

题意

有一个 \(N \times N\) 的网格。初始时,所有的单元格都被涂成白色。

请按给定顺序处理 \(Q\) 个查询。每个查询属于以下两种类型之一:

  • 类型 1:给出一个整数 \(R\)。将网格中第 \(R\) 行的所有单元格涂成黑色。
  • 类型 2:给出一个整数 \(C\)。将网格中第 \(C\) 列的所有单元格涂成白色。

在处理完每一个查询后,输出当前网格中黑色单元格的总数。

思路

对于 \((i, j)\) 这个单元格来说,只有当第 \(i\) 行最后一次涂的时间比第 \(j\) 列最后一次涂的时间更晚,才会是一个黑色的格子。

因此我们可以重点关注每一行和每一列上一次进行操作的时间,通过时间关系来求解答案。

一开始所有格子都是白色的,我们可以假设每一列上一次涂色的时间为 \(0\),每一行上一次涂色的时间为 \(-1\)

记当前时间为 \(i\),考虑两种类型的操作:

  • 如果当前操作类型为 \(1\),这次操作会把第 \(R\) 行整行改为黑色,此时答案应当加上操作前第 \(R\) 行上的白色格子数量。
    • 记上一次操作第 \(R\) 行的时间为 \(\text{oldT}\),明显如果有某些列的上一次操作时间在 \(\text{oldT}\) 之前,那么对应列的格子在本次涂色前后均是黑色,不会改变。
    • 而如果存在某些列,上一次的操作时间在 \(\text{oldT}\) 之后,那么这些列在本次操作前都是白色格子,本次操作结束后便会变成黑色。
    • 因此,我们只需要统计有多少列的最后操作时间 \(\gt\text{oldT}\),对应数量可以直接加入答案。
  • 如果当前操作类型为 \(2\),这次操作会把第 \(C\) 列整行改为白色,此时答案应当减去操作前第 \(C\) 列上的黑色格子数量。
    • 同理,我们只需要统计有多少行的最后操作时间 \(\gt \text{oldT}\),对应数量可以直接从答案中减去。

对于实现过程,我们可以开两个数组 r[], c[] 分别用于存储每行和每列的最后操作时间。由于我们还需要根据时间范围找对应行列的数量(即根据值区间查找下标数量),因此可以再开两个计数数组,用于维护对应时间的行数与列数。

由于查找是按区间查找的,因此计数数组采用树状数组等方式进行维护。

时间复杂度 \(O(N + Q\log Q)\)

代码

int n, q;
int r[300005], c[300005];

int tr[300005], tc[300005], maxx;
int lowbit(int x)
{
    return x & -x;
}
void update(int t[], int p, int v)
{
    p += 2; // 下标整体偏移 +2
    while(p <= q + 2)
    {
        t[p] += v;
        p += lowbit(p);
    }
}
int query(int t[], int p)
{
    p += 2; // 下标整体偏移 +2
    int r = 0;
    while(p > 0)
    {
        r += t[p];
        p -= lowbit(p);
    }
    return r;
}

void solve()
{
    cin >> n >> q;
    
    for(int i = 1; i <= n; i++)
    {
        r[i] = -1;
        c[i] = 0;
    }
    update(tr, -1, n);
    update(tc, 0, n);
    
    long long ans = 0;
    for(int i = 1; i <= q; i++)
    {
        int op, x;
        cin >> op >> x;
        if(op == 1)
        {
            // x 行的最后操作时间为 r[x]
            // 求 最后操作时间 > r[x] 的列数 加上
            ans += n - query(tc, r[x]);
            update(tr, r[x], -1);
            r[x] = i;
            update(tr, r[x], 1);
        }
        else
        {
            // x 列的最后操作时间为 c[x]
            // 求 最后操作时间 > c[x] 的行数 减去
            ans -= n - query(tr, c[x]);
            update(tc, c[x], -1);
            c[x] = i;
            update(tc, c[x], 1);
        }
        cout << ans << "\n";
    }
}
posted @ 2026-06-06 21:46  StelaYuri  阅读(62)  评论(0)    收藏  举报