AtCoder Beginner Contest 449 ABCDEF 题目解析

A - π

  • 预估难度:入门
  • 标签:数学

题意

求直径为 \(D\) 的圆的面积。

代码

const double PI = 3.141592653589793;

void solve()
{
    double d;
    cin >> d;
    double r = d / 2;
    printf("%.15f", PI * r * r);
}

B - Deconstruct Chocolate

  • 预估难度:入门
  • 标签:模拟

题意

有一块由 \(H\)\(W\) 列的小块组成的矩形巧克力。

你需要依次处理 \(Q\) 个查询,并回答每个查询的结果。每个查询的格式如下:

  • 类型 1:给定一个整数 \(R\)。计算底部 \(R\) 行中巧克力小块的数量,然后将它们吃掉。
  • 类型 2:给定一个整数 \(C\)。计算右侧 \(C\) 列中巧克力小块的数量,然后将它们吃掉。

当按顺序处理这些查询时,每次处理完查询后,剩余的巧克力仍然保持矩形形状。
保证在处理类型 1 查询前,巧克力至少有 \(R + 1\) 行;在处理类型 2 查询前,巧克力至少有 \(C + 1\) 列。

思路

意思是每次会对于一个 \(H \times W\) 的矩形去掉 \(R\) 行或者去掉 \(C\) 列,并实时输出被去掉的总面积。

只需要用两个变量 \(H, W\) 维护当前矩形剩余的行数与列数即可。

代码

void solve()
{
    int h, w, q; // h w 表示当前的总行数、总列数
    cin >> h >> w >> q;
    while(q--)
    {
        int op;
        cin >> op;
        if(op == 1)
        {
            int r;
            cin >> r;
            cout << r * w << "\n";
            h -= r; // 去掉 r 行
        }
        else
        {
            int c;
            cin >> c;
            cout << h * c << "\n";
            w -= c; // 去掉 c 列
        }
    }
}

C - Comfortable Distance

  • 预估难度:普及-
  • 标签:前缀和 / 二分

题意

给定一个长度为 \(N\) 的字符串 \(S\),由小写英文字母组成。

请找出满足以下所有条件的整数对 \((i, j)\) 的数量:

  • \(1 \leq i \leq j \leq N\)
  • \(S_i = S_j\)
  • \(L \leq j - i \leq R\)

思路

即统计有多少对相同的字符,下标差值在范围 \([L, R]\) 内。

考虑枚举其中后一个字符 \(S_j\) 的位置,那么前一个字符的下标范围便可通过 \([j-R, j-L]\) 进行描述。由于前后两个字符是相同的,那么问题便转变为“在区间 \([j-R, j-L]\) 中出现了多少次字符 \(S_j\)”。

统计一段区间内某个字符的出现次数,可以借助前缀和进行记录,时间复杂度 \(O(N\sigma)\),其中 \(\sigma = 26\);也可以通过记录每种字符的下标,再对下标序列进行二分计数,时间复杂度 \(O(N\log N)\)

注意答案可能为长整型,且区间端点 \(j-R, j-L\) 可能不存在。

代码一 前缀和

int pre[500005][26];
// pre[i][j] 表示前 i 个位置内出现了多少次字符 j

void solve()
{
    int N, L, R;
    cin >> N >> L >> R;
    string s;
    cin >> s;
    
    long long ans = 0;
    for(int i = 1; i <= N; i++)
    {
        for(int j = 0; j < 26; j++)
            pre[i][j] = pre[i-1][j];
        
        int id = s[i - 1] - 'a'; // a~z 转为 0~25
        pre[i][id]++;
        
        // 假设当前位置为整数对的后一个位置
        // 那么满足条件的前一个位置需要在 [i-R, i-L] 范围内
        if(i - L < 1)
            continue;
        
        int x = max(1, i - R);
        int y = max(1, i - L);
        // 找区间 [x, y] 内有多少个字符与当前字符相同
        ans += pre[y][id] - pre[x-1][id];
    }
    cout << ans;
}

代码二 二分

vector<int> G[26];
// G[i] 统计 i 这个字符出现过的下标位置

void solve()
{
    int N, L, R;
    cin >> N >> L >> R;
    string s;
    cin >> s;
    
    long long ans = 0;
    for(int i = 1; i <= N; i++)
    {
        int id = s[i - 1] - 'a'; // a~z 转为 0~25
        G[id].push_back(i);
        
        if(i - L < 1)
            continue;
        
        int x = max(1, i - R);
        int y = max(1, i - L);
        // 找区间 [x, y] 内有多少个字符与当前字符相同
        ans += upper_bound(G[id].begin(), G[id].end(), y)
               - lower_bound(G[id].begin(), G[id].end(), x);
    }
    cout << ans;
}

D - Make Target 2

  • 预估难度:普及/提高-
  • 标签:数学、二维前缀和

题意

在二维坐标平面上,对于坐标点 \((x, y)\):

  • 如果 \(\max(|x|, |y|)\) 是偶数,则将其涂成黑色
  • 如果是奇数,则将其涂成白色

在所有满足 \(L \leq x \leq R\)\(D \leq y \leq U\) 的坐标点 \((x, y)\) 之中,找出被涂成黑色的坐标点数。

思路

被涂黑的所有点在坐标系内的分布情况可以描述为:

  • 边长为 \(4k\)\(k \ge 0\) 且为整数) 且以 \((0, 0)\) 为中心的正方形边界上所有点。

图像可以参照样例一所给定的图:

image

可以发现该图形是一个水平对称且垂直对称的图形,因此坐标为负数的位置可以通过对称性都转到第一象限来进行处理。


我们先解决第一象限内的问题。如果今天需要求解满足 \(L \leq x \leq R\)\(D \leq y \leq U\)\(L, D \ge 0\))的坐标点 \((x, y)\) 中的黑色点数,可以借助二维前缀和思想,将其转为求解以下四部分答案:

  • \(0 \leq x \leq R\)\(0 \leq y \leq U\) 的坐标点 \((x, y)\) 中的黑色点数,记作①
  • \(0 \leq x \lt L\)\(0 \leq y \leq U\) 的坐标点 \((x, y)\) 中的黑色点数,记作②
  • \(0 \leq x \leq R\)\(0 \leq y \lt D\) 的坐标点 \((x, y)\) 中的黑色点数,记作③
  • \(0 \leq x \lt L\)\(0 \leq y \lt D\) 的坐标点 \((x, y)\) 中的黑色点数,记作④

黑色点的数量便可以记作 ①-②-③+④。


为了求解第一象限内形如“\(0 \leq x \leq A\)\(0 \leq y \leq B\) 的坐标点 \((x, y)\) 中的黑色点数”,可以讨论下图形的性质。

假设目前给定的两坐标最大边界满足 \(A \le B\)(如果 \(A \gt B\) 可以交换,根据对称性答案不变),如下图所示:

image

那么我们可以将该问题拆为两部分:

  • 以原点 \((0, 0)\) 为左下角的正方形区域内黑色点数
  • 剩余部分黑色点数

image

对于第一部分,由左下向右上观察图形可得,每一层的黑色点数是一个首项为 \(1\) 公差为 \(4\) 的等差数列,且层数可以通过 \(\lfloor \frac A 2 \rfloor + 1\) 获得。因此该部分答案可通过等差数列求和公式求解。

对于第二部分,发现要数的就是 \(A+1 \sim B\) 之间有多少个偶数列,并且每遇到一个偶数列,就相当于多了 \(A+1\) 个黑色点。\(A+1 \sim B\) 之间的偶数可以借助前缀和思想,通过 \(\lfloor \frac B 2 \rfloor - \lfloor \frac A 2 \rfloor\) 求得,对答案的贡献即 \((\lfloor \frac B 2 \rfloor - \lfloor \frac A 2 \rfloor) \times (A + 1)\)


最后便是将其余几个象限的问题都根据对称性转到第一象限进行求解,分类讨论方法有很多。

这里进行分类讨论的主要依据是询问的矩形是否跨过 \(x\) 轴 / \(y\) 轴,以此分为四类,具体详见代码。

重点注意两条坐标轴上的黑色点不要重复计算,比如对于 \(x\) 方向的 \([L, R]\) 坐标范围,如果说 \(L \lt 0\)\(R \gt 0\),则可以分为 \([L, -1]\)\([0, R]\) 两段,再根据对称性变为 \([1, -L]\)\([1, R]\) 两端。\(y\) 方向同理。

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

代码

typedef long long ll;

// 计算第一象限内 0 <= x <= A 且 0 <= y <= B 中的黑色点数
ll cal(int A, int B)
{
    if(A < 0 || B < 0)
        return 0;
    
    if(A > B)
        swap(A, B);
    
    int d = A / 2 + 1;
    // 首项为 1 公差为 4 项数为 d 的等差数列求和
    ll res = 1LL * (1 + (1 + (d - 1) * 4)) * d / 2;
    
    // A+1 ~ B 之间的偶数坐标数量 * 每个偶数坐标上存在的黑色点数
    res += 1LL * (B / 2 - A / 2) * (A + 1);
    return res;
}

// 计算第一象限内 L <= x <= R 且 D <= y <= U 中的黑色点数
ll cal(int L, int R, int D, int U)
{
    return cal(R, U) - cal(L-1, U) - cal(R, D-1) + cal(L-1, D-1);
}

void solve()
{
    int L, R, D, U;
    cin >> L >> R >> D >> U;
    ll ans = 0;
    if(L < 0 && R > 0) // 横跨 y 轴
    {
        if(D < 0 && U > 0) // 横跨 x 轴
        {
            ans += cal(0, R, 0, U); // 第一象限
            ans += cal(1, -L, 0, U); // 第二象限
            ans += cal(1, -L, 1, -D); // 第三象限
            ans += cal(0, R, 1, -D); // 第四象限
        }
        else
        {
            // 要么在一二象限 要么在三四象限
            // 将三四象限改为一二象限(y 改为非负整数)
            if(D < 0)
            {
                swap(D, U);
                D = -D, U = -U;
            }
            ans += cal(0, R, D, U); // 第一象限
            ans += cal(1, -L, D, U); // 第二象限
        }
    }
    else
    {
        if(D < 0 && U > 0) // 横跨 x 轴
        {
            // 要么在一四象限 要么在二三象限
            // 将二三象限改为一四象限(x 改为非负整数)
            if(L < 0)
            {
                swap(L, R);
                L = -L, R = -R;
            }
            ans += cal(L, R, 0, U); // 第一象限
            ans += cal(L, R, 1, -D); // 第四象限
        }
        else
        {
            // 仅位于某一象限内
            // 直接对称改到第一象限
            if(L < 0)
            {
                swap(L, R);
                L = -L, R = -R;
            }
            if(D < 0)
            {
                swap(D, U);
                D = -D, U = -U;
            }
            ans += cal(L, R, D, U);
        }
    }
    cout << ans;
}

E - A += v

  • 预估难度:普及+/提高
  • 标签:二分查找、树状数组、排序、离线

题意

给定整数 \(N, M\) 以及一个长度为 \(N\) 的整数序列 \(A=(A_1,A_2,\ldots,A_N)\),其中每个元素都在 \(1\)\(M\) 之间。

对该整数序列 \(A\) 执行 \(10^{100}\) 次以下操作:

  • 找出 \(1 \sim M\) 范围内在序列 \(A\) 中出现次数最少的整数 \(v\)。如果有多个这样的 \(v\),则取其中值最小的一个。然后,将 \(v\) 追加到 \(A\) 的末尾。

现在给定 \(Q\) 个查询。第 \(i\) 个查询给出一个整数 \(X_i\),请找出在执行完 \(10^{100}\) 次操作后,\(A_{X_i}\) 的值是多少。

思路

题目的操作可以换个角度描述:

  • 记当前出现次数并列最小的整数有 \(k\) 个,分别为 \(B_1, B_2, \ldots, B_k\)
  • 那么接下来的 \(k\) 次操作,一定是将 \(B_1, B_2, \ldots, B_k\)\(k\) 个整数按从小到大的顺序依次拼在序列 \(A\) 的末尾。
  • \(k\) 次操作结束后,所有出现次数并列最小的整数,出现次数就全部 \(+1\) 了。

接下来重复执行上述操作,直到所有整数的出现次数全部相同。此后的操作便一定是循环将 \(1 \sim M\) 的每个数拼接在序列 \(A\) 末尾。

考虑离线所有询问,按顺序模拟所有整数出现次数变为相同之前的过程,在过程中按从小到大的顺序解决询问。

由于每个整数的出现次数不会超过 \(N\) 次,我们便可以枚举 \(i = 0 \sim N-1\),按顺序考虑当前出现次数并列为 \(i\) 的所有整数,模拟将其出现次数均改为 \(i+1\) 次。在这个过程中,只要某个整数的初始出现次数 \(\le i\),那么接下来每轮操作该数字都会出现。

假设在考虑当前出现次数并列为 \(i\) 的所有整数之前,我们已经将序列 \(A\) 的长度扩展到 \(\text{maxn}\) 的位置,记出现次数并列为 \(i\) 的整数有 \(k\) 个,那么这一轮操作会将序列 \(A\) 的长度扩展到 \(\text{maxn} + k\) 的位置。因此,离线的询问中所有下标 \(\le \text{maxn} + k\) 的询问在这一轮都可以直接解决。

  • 例如,对于满足 \(\text{maxn} \lt x \le \text{maxn} + k\) 的下标 \(x\),其在这一轮操作中是第 \(x - \text{maxn}\) 个被添加到序列 \(A\) 末尾的数字,只需要求出当前出现的数字当中的第 \(x - \text{maxn}\) 小值即可。

剩余问题就是如何快速找到当前出现的所有数字当中的第 \(t\) 小值了。由于每个整数均在 \(1 \sim M\) 的范围内,可以借助树状数组维护计数数组,标记每个出现过的次数,然后在树状数组上进行二分查找来实现。

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

代码

typedef long long ll;

struct BIT
{
    int a[500005];
    // 将 p 位置改为 1
    void update(int p)
    {
        while(p <= 500000)
        {
            a[p]++;
            p += p & -p;
        }
    }
    int query(int p)
    {
        int sum = 0;
        while(p)
        {
            sum += a[p];
            p -= p & -p;
        }
        return sum;
    }
    // 二分找第 k 小值
    int find(int k)
    {
        int l = 1, r = 500000, ans;
        while(l <= r)
        {
            int mid = l + r >> 1;
            if(query(mid) >= k) // 前 mid 个位置出现的数字个数 >= k
            {
                ans = mid;
                r = mid - 1;
            }
            else
                l = mid + 1;
        }
        return ans;
    }
};
BIT tree;

int n, m;
int a[500005], cnt[500005]; // cnt[i] 表示 i 的出现次数
vector<int> G[500005]; // G[i] 表示初始出现次数为 i 的所有整数

struct query
{
    ll x; // 询问的下标
    int id; // 第几个问题
    bool operator < (const query &q) const
    {
        return x < q.x; // 按询问下标从小到大排序
    }
};
query qr[200005];

int ans[200005];

void solve()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
    {
        cin >> a[i];
        cnt[a[i]]++;
    }
    
    for(int i = 1; i <= m; i++)
        G[cnt[i]].push_back(i);
    
    int q;
    cin >> q;
    for(int i = 1; i <= q; i++)
    {
        cin >> qr[i].x;
        qr[i].id = i;
    }
    sort(qr + 1, qr + q + 1); // 离线询问 按询问位置排序
    
    int j = 1; // 当前还没解决的下一个问题下标
    while(j <= q && qr[j].x <= n) // 询问下标如果 <= n 则直接解决
    {
        ans[qr[j].id] = a[qr[j].x];
        j++;
    }
    
    ll maxn = n; // 当前已经模拟到的最大下标
    for(int i = 0; i < n && j <= q; i++)
    {
        // 接下来会将出现次数并列为 i 的所有数字按顺序拼到序列末尾
        // 将其出现次数全部改为 i+1 次
        
        // 将所有出现次数为 i 的数标记出现
        for(int v : G[i])
            tree.update(v);
        
        int tot = tree.query(m); // 目前出现次数并列为 i 的数字个数
        
        // 可处理的下标区间为 [maxn + 1, maxn + tot]
        while(j <= q && qr[j].x <= maxn + tot)
        {
            ans[qr[j].id] = tree.find(qr[j].x - maxn);
            j++;
        }
        
        maxn += tot;
    }
    
    while(j <= q) // 还有剩余问题
    {
        // 该问题对应的答案为接下来拼接的第 qr[j].x - maxn 个数
        // 此时已经是每 m 个数为一次循环,每次循环将 1~m 每个数字按顺序拼在末尾
        int k = (qr[j].x - maxn - 1) % m + 1; // 等同于出现的整数中第 k 小值
        ans[qr[j].id] = k;
        j++;
    }
    
    for(int i = 1; i <= q; i++)
        cout << ans[i] << "\n";
}

F - Grid Clipping

  • 预估难度:提高+/省选
  • 标签:扫描线(线段树)

题意

有一个大小为 \(H \times W\) 的网格。第 \(r\) 行第 \(c\) 列的格子被称为格子 \((r, c)\)

其中,有 \(N\) 个格子 \((R_k, C_k)\) 会被涂成黑色,而其余 \(HW-N\) 个格子都是白色的。

请找出该网格中所有的大小为 \(h \times w\) 且内部所有格子均为白色的矩形区域的数量。

思路

题目要求数大小为 \(h \times w\) 且内部所有格子均为白色的矩形区域的数量。

直接统计比较麻烦,不如考虑容斥,先计算出内部格子不是均为白色的矩形区域数量,再通过总数减去该数量亦可求出答案。

接下来我们可以将 \(h \times w\) 的矩形缩成一个点,例如用其左上角的点来描述这个矩形。接下来我们的任务是计算平面内有多少个点 \((r, c)\) 满足:

  • \(1 \le r \le H - h + 1\)
  • \(1 \le c \le W - w + 1\)
  • \((r, c)\) 为左上角的大小为 \(h \times w\) 的矩形内部至少有一个黑色点

对于其中的第三个条件,我们也可以直接在 \((r, c)\) 这个点上统计其所代表的矩形内部的黑色点数。

那么,对于位于 \((R_k, C_k)\) 位置的这个黑色点,它会直接影响到所有满足 \(R_k - h + 1 \le r \le R_k\)\(C_k - w + 1 \le c \le C_k\) 的位置 \((r, c)\)

因此,每个给定的黑色点对整张平面的影响都可以描述为一个矩形,所有被矩形影响到的位置均不能当作符合题意的左上角点。

我们的任务便是求出所有没被任何矩形覆盖的点数,或者也可以理解为面积。因此本题剩余部分就是一个矩形区域求并集的问题,借助扫描线算法处理即可。

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

代码

struct edge
{
    int y, l, r, v;
    // y 表示垂直坐标
    // l r 表示影响的水平区间 左闭右开
    // v = 1/-1 表示是某个矩形开始被扫描的位置还是结束扫描的位置
    bool operator < (const edge &e) const
    {
        return y < e.y;
    }
};
vector<edge> G;
int vv[400050], vcnt = 0; // 处理离散化

#define ls (p << 1)
#define rs (p << 1| 1)

struct node
{
    int l, r;
    int sum; // 当前区间内被覆盖至少一次的位置数
    int cnt; // 当前区间被完整覆盖的次数 lazy
};
node tr[400050 << 2];

void push_up(int p)
{
    if(tr[p].cnt > 0) // 被完整覆盖,覆盖位置数即区间对应长度
        tr[p].sum = vv[tr[p].r + 1] - vv[tr[p].l];
    else if(tr[p].l != tr[p].r) // 不是叶子结点,答案从两个子结点上传
        tr[p].sum = tr[ls].sum + tr[rs].sum;
    else
        tr[p].sum = 0;
}

void build(int l, int r, int p = 1)
{
    tr[p].l = l;
    tr[p].r = r;
    tr[p].sum = tr[p].cnt = 0;
    if(l == r)
        return;
    int mid = l + r >> 1;
    build(l, mid, ls);
    build(mid+1, r, rs);
}

// 更新区间 [l, r] 的覆盖次数
// d = -1 / 1
void update(int l, int r, int d, int p = 1)
{
    if(l <= tr[p].l && tr[p].r <= r)
    {
        tr[p].cnt += d;
        push_up(p); // 更新当前点的 sum
        return;
    }
    if(l <= tr[ls].r)
        update(l, r, d, ls);
    if(r >= tr[rs].l)
        update(l, r, d, rs);
    push_up(p);
}

// 求区间被覆盖至少一次的位置数
int query(int l, int r, int p = 1)
{
    if(l <= tr[p].l && tr[p].r <= r)
        return tr[p].sum;
    int sum = 0;
    if(l <= tr[ls].r)
        sum += query(l, r, ls);
    if(r >= tr[rs].l)
        sum += query(l, r, rs);
    return sum;
}

int H, W, h, w, N;

void solve()
{
    cin >> H >> W >> h >> w >> N;
    for(int i = 1; i <= N; i++)
    {
        int r, c;
        cin >> r >> c;
        // 该黑色格子会影响到 r-h+1 <= x <= r 且 c-w+1 <= y <= c 范围内的每个位置
        int xl = max(r - h + 1, 1), xr = min(r, H - h + 1);
        int yl = max(c - w + 1, 1), yr = min(c, W - w + 1);
        
        if(xl > xr || yl > yr)
            continue;
        
        G.push_back(edge{xl, yl, yr + 1, 1});
        G.push_back(edge{xr + 1, yl, yr + 1, -1});
        vv[++vcnt] = yl;
        vv[++vcnt] = yr + 1;
    }
    
    // 将整个网格图起始线与终点线纳入考虑
    G.push_back(edge{1, 1, W - w + 2, 0});
    G.push_back(edge{H - h + 2, 1, W - w + 2, 0});
    vv[++vcnt] = 1;
    vv[++vcnt] = W - w + 2;
    
    sort(vv + 1, vv + vcnt + 1);
    vcnt = unique(vv + 1, vv + vcnt + 1) - (vv + 1);
    for(edge &e : G)
    {
        e.l = lower_bound(vv + 1, vv + vcnt + 1, e.l) - vv;
        e.r = lower_bound(vv + 1, vv + vcnt + 1, e.r) - vv;
    }
    
    sort(G.begin(), G.end());
    
    build(1, vcnt - 1);
    ll ans = 0;
    for(int i = 0; i < G.size(); i++)
    {
        if(i > 0)
            ans += 1LL * (W - w + 1 - tr[1].sum) * (G[i].y - G[i - 1].y);
        update(G[i].l, G[i].r - 1, G[i].v);
    }
    cout << ans;
}
posted @ 2026-03-14 23:33  StelaYuri  阅读(133)  评论(0)    收藏  举报