AtCoder Beginner Contest 442 ABCDEF 题目解析

A - Count .

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

题意

给定一个由小写英文字母组成的字符串 \(S\),问 \(S\) 中有多少个字符为 i 或者 j

代码

void solve()
{
    string s;
    cin >> s;
    
    int cnt = 0;
    for(char c : s)
        if(c == 'i' || c == 'j')
            cnt++;
    
    cout << cnt;
}

B - Music Player

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

题意

高桥有一个音乐播放器。最初,播放器的音量为 \(0\),且处于停止播放状态。

\(Q\) 个操作将依次进行。第 \(i\) 个操作以一个整数 \(A_i\) 表示,含义如下:

  • 如果 \(A_i = 1\),则音量增加 \(1\)
  • 如果 \(A_i = 2\),则音量减少 \(1\)。如果音量原本为 \(0\),则不做操作。
  • 如果 \(A_i = 3\),则改变播放状态。即如果当前处于播放状态,则改为停止播放状态;否则,改为播放状态。

在每次操作结束后,请判断播放器是否位于播放状态,且音量至少为 \(3\)

思路

简单模拟题,使用两个变量分别维护当前音量以及播放状态即可。

代码

void solve()
{
    int Q;
    cin >> Q;
    
    int volume = 0; // 音量
    bool playing = false; // 播放状态
    
    while(Q--)
    {
        int a;
        cin >> a;
        if(a == 1)
            volume = volume + 1;
        else if(a == 2)
            volume = max(0, volume - 1);
        else
            playing = !playing;
        
        if(playing && volume >= 3)
            cout << "Yes\n";
        else
            cout << "No\n";
    }
}

C - Peer Review

  • 预估难度:普及-
  • 标签:组合数学

题意

\(N\) 名研究员,编号分别为 \(1, 2, \ldots, N\)

在研究员之间存在着 \(M\) 个利益冲突。对于 \(i = 1, 2, \ldots, M\),已知研究员 \(A_i\)\(B_i\) 存在着利益冲突。

一篇论文的审稿人必须是三名不同的研究员,他们需要不同于论文作者,并且与论文作者之间没有利益冲突。

我们假设每篇论文都只有一位作者。

对于 \(i = 1, 2, \ldots, N\),试求出有多少组不同的研究员三元组,能够作为研究员 \(i\) 所撰写的论文的审稿人?

思路

根据数据范围,保证不存在重复的利益冲突关系。于是我们可以借助计数数组 \(\text{cnt}[i]\) 统计有多少人与研究员 \(i\) 存在利益冲突。

最后对于第 \(i\) 名研究员,除去自己以外,有 \((n-1)-\text{cnt}[i]\) 人是与其没有冲突的。我们可以从这些人中任选三人作为其论文的审稿人。

\(t = (n-1)-\text{cnt}[i]\),则方案总数为从 \(t\) 人中任选三人的组合方案数量,为 \(\text{C}_{t}^3=\frac{t(t-1)(t-2)}{6}\)

代码

int n, m, cnt[200005];
// cnt[i] 表示有多少人与 i 存在冲突

void solve()
{
    cin >> n >> m;
    for(int i = 1; i <= m; i++)
    {
        int a, b;
        cin >> a >> b;
        cnt[a]++;
        cnt[b]++;
    }
    
    for(int i = 1; i <= n; i++)
    {
        int t = n - 1 - cnt[i];
        // 共 t 人与 i 无冲突,从中选出三人,求组合方案
        cout << 1LL * t * (t - 1) * (t - 2) / 6 << " ";
    }
}

D - Swap and Range Sum

  • 预估难度:普及-
  • 标签:前缀和 / 数据结构

题意

给定一个长度为 \(N\) 的序列 \(A=(A_1,A_2,\dots,A_N)\)

请按顺序处理 \(Q\) 个询问。每个询问的格式如下:

  • 1 x :交换 \(A_x\)\(A_{x+1}\) 的值。
  • 2 l r:查找 \(\displaystyle \sum_{l\leq i\leq r} A_i\) 的值。

思路

题目涉及区间求和操作,一般来说静态区间可以直接采用前缀和,而如果涉及修改,则需要借助树状数组/线段树等数据结构进行维护。

考虑本题的修改操作,发现每次只会交换序列中的两个数,并且这两个数在原序列中是相邻的

假设 \(S\)\(A\) 序列的前缀和序列,其中 \(S_i = \sum\limits_{j=1}^i A_j\)

  • 由于只会交换,因此整个序列的总和 \(S_N\) 是不变的。
  • 又因为发生交换的是相邻两个数字 \(A_x\)\(A_{x+1}\),明显 \(x-1\) 及之前的所有位置的前缀和,以及 \(x+1\) 及之后的所有位置的前缀和均不会发生更改,因此只需要更新 \(x\) 这一个位置的前缀和数组即可。

综上,借助前缀和可以在 \(O(N+Q)\) 的时间复杂度内完成所有任务(代码一)。

或者直接借助高级数据结构在 \(O(N+Q\log N)\) 的时间复杂度内完成(代码二)。

代码一

int a[200005], s[200005];

void solve()
{
    int n, q;
    cin >> n >> q;
    for(int i = 1; i <= n; i++)
    {
        cin >> a[i];
        s[i] = s[i - 1] + a[i];
    }
    
    while(q--)
    {
        int op;
        cin >> op;
        if(op == 1)
        {
            int x;
            cin >> x;
            swap(a[x], a[x+1]);
            s[x] = s[x-1] + a[x]; // 仅更新 x 位置的前缀和即可
        }
        else
        {
            int l, r;
            cin >> l >> r;
            cout << s[r] - s[l-1] << "\n";
        }
    }
}

代码二

struct BIT
{
    int n, a[200005];
    void init(int _n)
    {
        n = _n;
        for(int i = 0; i <= n; i++)
            a[i] = 0;
    }
    void update(int p, int v)
    {
        while(p <= n)
        {
            a[p] += v;
            p += p & -p;
        }
    }
    int query(int p)
    {
        int r = 0;
        while(p)
        {
            r += a[p];
            p -= p & -p;
        }
        return r;
    }
};

BIT tree;
int a[200005];

void solve()
{
    int n, q;
    cin >> n >> q;
    
    tree.init(n);
    for(int i = 1; i <= n; i++)
    {
        cin >> a[i];
        tree.update(i, a[i]);
    }
    
    while(q--)
    {
        int op;
        cin >> op;
        if(op == 1)
        {
            int x;
            cin >> x;
            tree.update(x, a[x+1] - a[x]);
            tree.update(x+1, a[x] - a[x+1]);
            swap(a[x], a[x+1]);
        }
        else
        {
            int l, r;
            cin >> l >> r;
            cout << tree.query(r) - tree.query(l-1) << "\n";
        }
    }
}

E - Laser Takahashi

  • 预估难度:普及+/提高
  • 标签:计算几何、排序

题意

在一个二维平面上有 \(N\) 只怪物。怪物分别编号为 \(1, 2, \dots, N\),且怪物 \(i\) 所在的坐标为 \((X_i, Y_i)\),保证 \((X_i,Y_i) \neq (0,0)\) 。(每个怪物都可以视为一个静止的点。也就是说,怪物没有大小之分)。

高桥站在这个平面的原点处,他的双眼总是能发射出强大的激光(可以当作是一条从原点出发的射线),并瞬间消灭他所面向的所有怪物(若多只怪物与原点共线,则全部消灭)。

青木正在进行 \(Q\)独立的思想实验。第 \(j\) 次思想实验如下:

  • 最初,高桥面向怪物 \(A_j\) 所在的方向。从现在起,高桥将按顺时针方向一直旋转,直到他朝向怪物 \(B_j\) 所在的方向时才会停止旋转。问在这个旋转过程中,他一共会消灭多少只怪兽(包括怪物 \(A_j\)\(B_j\))?
    • 如果怪物 \(A_j\)\(B_j\) 在同一方向上,那么高桥就不会旋转。

思路

一道比较模板的极角排序题。先按顺时针将所有点进行排序,与原点共线时可以任意处理。

  • 为规避浮点数带来的精度误差问题,极角排序最好采用向量叉积判符号的形式进行处理。先判断两点是否同时在坐标系的上半部分(第一、第二象限)或下半部分(第三、第四象限),如果不是,则优先让上半部分的点排在前。若两点同时位于某半部分,则按照原点出发的向量叉积正负性排序,顺时针则判断叉积 \(\lt 0\) 即可。

排序完成之后,如果保证不存在任意两点与原点能够组成三点共线的情况的话,我们便可以根据输入的起点 \(a\) 与终点 \(b\) 的编号,通过一个映射数组 pos[] 在排序后的序列中快速找出其对应的新下标。

\(p = \text{pos}[a], q = \text{pos}[b]\)

  • 如果 \(p \le q\),则能杀死的怪物就是新序列中下标在 \([p, q]\) 区间内的所有怪物,数量为 \(q - p + 1\)
  • 如果 \(p \gt q\),则能杀死的怪物就是新序列中下标在 \([p, N]\)\([1, q]\) 区间内的所有怪物,数量为 \(N-p+1+q\)

但本题可能出现多只怪物与原点共线的情况,需要特殊处理。在排序后的序列中,由于方向相同的点一定是连续出现的,因此可以借助循环来对于每个点快速求出与我方向相同的所有点的最小下标及最大下标,分别记作 L[i]R[i]

  • 判断两个向量的方向是否相同,首先向量叉积需要 \(=0\),但这只能保证两向量共线,可能会出现方向相反的情况。所以最好再套上一个点积 \(\gt 0\) 的判断,以保证两向量夹角不超过 \(90^{\circ}\)

然后记 \(p = \text{L}[\text{pos}[a]], q = \text{R}[\text{pos}[b]]\),再进行上述的分类讨论即可。

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

代码

typedef long long ll;

struct Point
{
    ll x, y;
    int id;
    Point(){}
    Point(ll x, ll y): x(x), y(y){}
};

ll Dot(Point a, Point b) // 向量点积
{
    return a.x * b.x + a.y * b.y;
}
ll Cross(Point a, Point b) // 向量叉积
{
    return a.x * b.y - a.y * b.x;
}

bool cmp(Point &a, Point &b)
{
    // 弧度 [-PI, 0) 作为下方两象限,[0, +PI) 作为上方两象限
    
    bool fa = a.y > 0 || a.y == 0 && a.x > 0; // 判断 a 在不在坐标系的上半部分
    bool fb = b.y > 0 || b.y == 0 && b.x > 0;
    
    if(fa != fb) // 两点不同时在坐标系的上半/下半部分
        return fa; // 将上半部分的点排在前面
    
    return Cross(a, b) < 0; // 两点同时在上方/下方的象限内,直接按叉积顺时针排序
}

int n, q;
Point p[200005];

int pos[200005];
// pos[i] 表示原本第 i 个点在排序后的数组中的哪个下标

int L[200005], R[200005];
// L[i], R[i] 表示与排序后的第 i 个点方向相同的所有点的最小及最大下标

void solve()
{
    cin >> n >> q;
    for(int i = 1; i <= n; i++)
    {
        cin >> p[i].x >> p[i].y;
        p[i].id = i;
    }
    sort(p + 1, p + n + 1, cmp);
    
    for(int i = 1; i <= n; i++)
        pos[p[i].id] = i;
    
    L[1] = 1;
    for(int i = 2; i <= n; i++)
    {
        if(Cross(p[i-1], p[i]) == 0 && Dot(p[i-1], p[i]) > 0) // 两点在同个方向上,继承上个点的最小下标
            L[i] = L[i-1];
        else
            L[i] = i;
    }
    R[n] = n;
    for(int i = n-1; i >= 1; i--)
    {
        if(Cross(p[i+1], p[i]) == 0 && Dot(p[i+1], p[i]) > 0) // 两点在同个方向上,继承下个点的最小下标
            R[i] = R[i+1];
        else
            R[i] = i;
    }
    
    while(q--)
    {
        int a, b;
        cin >> a >> b;
        a = L[pos[a]];
        b = R[pos[b]];
        if(a <= b)
            cout << (b - a + 1) << "\n";
        else
            cout << (n - a + 1) + b << "\n";
    }
}

F - Diagonal Separation 2

  • 预估难度:普及+/提高
  • 标签:动态规划、前缀和

题意

有一个 \(N\)\(N\) 列的网格图。第 \(i\) 行第 \(j\) 列的位置被称作 \((i, j)\)

图中的每个网格都会被涂成白色或黑色。网格的初始涂色信息由 \(N\) 个字符串 \(S_1, S_2, \ldots, S_N\) 给出:

  • 如果字符串 \(S_i\) 的第 \(j\) 个字符是 .,则网格 \((i, j)\) 会被涂成白色;
  • 如果字符串 \(S_i\) 的第 \(j\) 个字符是 #,则网格 \((i, j)\) 会被涂成黑色。

您将重新绘制一些网格的颜色,以同时满足以下两个条件:

  • 对于每一行:所有白色网格全部集中在该行的左侧,所有黑色网格全部集中在该行的右侧。
  • 对于每一列:所有白色网格全部集中在该列的上方,所有黑色网格全部集中在该列的下方。

求为了满足条件所需要重新涂色的网格数量最小值。

思路

根据题意,每一行的黑色格子必须连续集中在右侧,每一列的黑色格子必须连续集中在下方,可知对于网格图中的每一个黑色格子,其右下方必定也全都是黑色格子。

换句话说,如果第 \(i\) 行最靠左的黑色格子位于第 \(j\) 列,那么第 \(i+1\) 行最靠左的黑色格子的列数一定 \(\le j\)

相邻两行之间黑色格子的起始列存在限制关系,考虑动态规划。

\(\text{dp}[i][j]\) 表示从上往下考虑到第 \(i\) 行,并且第 \(i\) 行最靠左的黑色格子位于第 \(j\) 列时,需要重新涂色的网格数量最小值是多少。如果该行不存在黑色格子(全白),则令 \(j=N+1\)

根据上面的讨论,可以简单地得到状态转移方程为:

\[\text{dp}[i][j] = \min_{k=j}^{N+1}(\text{dp}[i-1][k]) + \text{让第 }i\text{ 行黑色格子从第 }j\text{ 列开始需要改变的网格数} \]

先考虑让第 \(i\) 行的黑色格子改成从第 \(j\) 列开始,所需要改变的网格数量。明显此时这一行第 \(1 \sim j-1\) 列上的所有黑色格子都得改成白色格子,第 \(j \sim N\) 列上的所有白色格子都得改成黑色格子,这两部分数量均可以借助行上的一维前缀和或是整体的二维前缀和来快速求出。

但即使这一数量能快速求出,上述状态转移方程仍是 \(O(N^3)\) 的。考虑 \(\min\limits_{k=j}^{N+1}(\text{dp}[i-1][k])\) 这一项求解的优化。我们只需要将状态中的第二维 \(j\) 倒序进行求解,这样就可以在枚举 \(j\) 的同时,用一个变量辅助维护上一行第 \(j\) 列的后缀最小值了。

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

代码

int n;
int a[5005][5005];
// a[i][j] = 0/1 表示 白/黑
int s[5005][5005];
// s[i] 为 a[i] 这一行的 黑色格子数量 的一维前缀和
int dp[5005][5005];
// dp[i][j] 表示从上往下看到第 i 行
// 且第 i 行最靠左的黑色格子位于第 j 列时
// 需要改变的格子数量最小值

void solve()
{
    cin >> n;
    
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= n; j++)
        {
            char c;
            cin >> c;
            if(c == '#')
                a[i][j] = 1;
            s[i][j] = s[i][j-1] + a[i][j];
        }
    
    for(int i = 1; i <= n; i++)
    {
        int minn = dp[i-1][n+1]; // 维护 dp[i-1][j后面的所有列] 中的 最小值
        for(int j = n+1; j >= 1; j--) // 倒序枚举当前这一行最左侧黑色格子的位置
        {
            // 1 ~ j-1 的黑色全改白色,j ~ n 的白色全改黑色
            int t = s[i][j-1] + ((n-j+1) - (s[i][n] - s[i][j-1]));
            // 将上一行 j 列的答案纳入 minn,更新后缀最小值
            minn = min(minn, dp[i-1][j]);
            // 上一行黑色格子起始列 >= j 的最小答案,加上当前行黑色格子以 j 作为起始列的操作数
            dp[i][j] = minn + t;
        }
    }
    
    int ans = 1e9;
    for(int j = 1; j <= n+1; j++)
        ans = min(ans, dp[n][j]);
    cout << ans;
}
posted @ 2026-01-24 22:26  StelaYuri  阅读(57)  评论(0)    收藏  举报