AtCoder Beginner Contest 455 ABCDEF 题目解析

A - 455

  • 预估难度:入门

题意

给定整数 \(A, B, C\),判断是否 \(A \neq B\)\(B = C\)

代码

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

B - Spiral Galaxy

  • 预估难度:入门
  • 标签:枚举

题意

有一个 \(H\)\(W\) 列的网格。将从上往下数第 \(i\) 行、从左往右数第 \(j\) 列的单元格记为 \((i, j)\)

网格的每个单元格都被染成白色或黑色。网格的信息由 \(H\) 个长度为 \(W\) 的字符串 \(S_1, S_2, \ldots, S_H\) 给出:如果 \(S_i\) 的第 \(j\) 个字符是 .,则单元格 \((i, j)\) 是白色的;如果是 #,则是黑色的。

求网格中有多少个子矩形区域是中心对称的。

更正式地说,求有多少组整数元组 \((h_1, h_2, w_1, w_2)\) 满足以下所有条件:

  • \(1 \leq h_1 \leq h_2 \leq H\)
  • \(1 \leq w_1 \leq w_2 \leq W\)
  • 对于满足 \(h_1 \leq i \leq h_2\)\(w_1 \leq j \leq w_2\) 的所有整数 \(i, j\),单元格 \((i, j)\) 和单元格 \((h_1 + h_2 - i, w_1 + w_2 - j)\) 具有相同的颜色。

思路

发现地图大小 \(H, W \le 10\),明显可以 \(O(H^2W^2)\) 枚举所有可能的子矩形的上下左右边界。

判断一个子矩形是否是中心对称图形,方法有很多,其中一种方法可以直接参考题目描述:

  • 当子矩阵的行在 \([h_1, h_2]\) 范围内时,与第 \(i\) 行对称的行是 \(h_2-(i-h_1) = h_1+h_2-i\)
  • 当子矩阵的列在 \([w_1, w_2]\) 范围内时,与第 \(j\) 列对称的列是 \(w_2-(j-w_1) = w_1+w_2-j\)

时间复杂度 \(O(H^3W^3)\)

代码

int h, w;
char mp[15][15];

// 判断 行在 [h1, h2] 内 列在 [w1, w2] 内的子矩形是否中心对称
bool check(int h1, int h2, int w1, int w2)
{
    for(int i = h1; i <= h2; i++)
        for(int j = w1; j <= w2; j++)
            if(mp[i][j] != mp[h1 + h2 - i][w1 + w2 - j])
                return false;
    return true;
}

void solve()
{
    cin >> h >> w;
    for(int i = 1; i <= h; i++)
        for(int j = 1; j <= w; j++)
            cin >> mp[i][j];
    
    int cnt = 0;
    for(int h1 = 1; h1 <= h; h1++) // 上
        for(int h2 = h1; h2 <= h; h2++) // 下
            for(int w1 = 1; w1 <= w; w1++) // 左
                for(int w2 = w1; w2 <= w; w2++) // 右
                    if(check(h1, h2, w1, w2))
                        cnt++;
    cout << cnt;
}

C - Vanish

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

题意

给定一个整数序列 \(A = (A_1, A_2, \ldots, A_N)\)

求在恰好执行 \(K\) 次以下操作后,\(A\) 中所有元素之和的最小可能值。

  • 选择一个整数 \(x\),然后将序列中所有值为 \(x\) 的数全部改为 \(0\)

思路

明显选择一个整数 \(x\) 后,整个序列的总和会减少 \(x \times (x\text{ 的出现次数})\)

那么我们便可以统计序列中每种整数的出现次数,然后将每种整数的 值 乘上 出现次数 所得到的数值存在一个新的数组里。每次操作,我们可以选择这个新数组中的某一项从序列总和中减去。

为了保证最终的总和最小,贪心可得,我们应该从总和中减去新数组中最大的 \(K\) 项之和。排序后直接处理即可。

注意如果数字种类数不足 \(K\) 种,此时一定有方法把整个序列全部变为 \(0\),直接输出 \(0\) 即可。

至于统计每种数字的出现次数,由于数值范围较大,可以采用 map 或者简单双指针进行实现。

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

代码

int a[300005];
int m = 0;
long long b[300005];
long long ans = 0;

void solve()
{
    int n, k;
    cin >> n >> k;
    for(int i = 1; i <= n; i++)
    {
        cin >> a[i];
        ans += a[i];
    }
    
    sort(a + 1, a + n + 1);
    
    for(int i = 1; i <= n;)
    {
        // i 是 a[i] 这种数字出现的第一个位置
        // j 去找 a[i] 这种数字出现的最后一个位置
        int j = i;
        while(j <= n && a[j + 1] == a[j])
            j++;
        // [i, j] 这一段数字全部相同
        // 说明选择 x=a[i] 会把这一段数字全部改为 0
        b[++m] = 1LL * (j - i + 1) * a[i];
        i = j + 1;
    }
    
    if(k >= m)
    {
        cout << 0;
        return;
    }
    
    sort(b + 1, b + m + 1);
    for(int i = m; i >= m - k + 1; i--)
        ans -= b[i];
    cout << ans;
}

D - Card Pile Query

  • 预估难度:普及/提高-
  • 标签:链表

题意

\(N\) 张卡片和 \(N\) 个牌堆。
卡片和牌堆均分别编号为 \(1, 2, \ldots, N\)
初始时,第 \(i\) 个牌堆中只包含第 \(i\) 张卡片。

按顺序对每个 \(i = 1, 2, \ldots, Q\) 执行以下操作:

  • 将第 \(C_i\) 张卡片以及叠在它上面的所有卡片(保持原有顺序)移动到第 \(P_i\) 张卡片的上面。保证在操作执行前,第 \(C_i\) 张卡片和第 \(P_i\) 张卡片位于不同的牌堆中,且第 \(P_i\) 张卡片位于某个牌堆的顶部。

求所有操作完成后,每个牌堆中的卡片数量。

思路

每次移动要将某张卡片及上面的所有卡片按顺序移动到另一张卡片上面,如果用(动态)数组去存储每个牌堆中目前拥有哪些卡片,那么每次移动操作可能会对牌堆容器进行大量的添加与删除操作,最坏情况下时间复杂度为 \(O(NQ)\)

考虑将每个牌堆自底向上视作一个链表,那么每次移动操作就只需要考虑修改与 \(P_i, C_i\) 这两张卡片有关的链表结点的前后指针即可。于是我们可以建立 \(N\) 个双向链表,第 \(i\) 个链表维护第 \(i\) 个牌堆中的每张卡片。

由于每个链表需要有个表头用于唯一标识,因此这里考虑用 \(i\) 表示第 \(i\) 张卡片的结点编号,用 \(N+i\) 表示第 \(i\) 个牌堆的表头编号。初始时,第 \(i\) 个牌堆只有第 \(i\) 张卡,建立 \(i \leftrightarrow N+i\) 双向对应关系。

\(\text{pre}[i], \text{nxt}[i]\) 分别用于表示 \(i\) 点的前驱结点与后继结点。那么当要将第 \(C_i\) 张卡片及后继链表整体移动到第 \(P_i\) 张卡片后面时,此时的关系为:

\[\begin{aligned} &\text{链表表头A}\leftrightarrow \dots \leftrightarrow \text{pre}[C_i] \leftrightarrow C_i \leftrightarrow \dots \\ &\text{链表表头B}\leftrightarrow \dots \leftrightarrow P_i \end{aligned} \]

需要将关系变为:

\[\begin{aligned} &\text{链表表头A}\leftrightarrow \dots \leftrightarrow \text{pre}[C_i] \\ &\text{链表表头B}\leftrightarrow \dots \leftrightarrow P_i \leftrightarrow C_i \leftrightarrow \dots \end{aligned} \]

更改情况为:

  • \(\text{pre}[C_i]\) 的后继结点置空
  • \(C_i\) 的前驱结点改为 \(P_i\)
  • \(P_i\) 的后继结点改为 \(C_i\)

最后从每个链表的表头开始,向后循环去统计表内有多少个结点即可。

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

代码

int n, q;
int nxt[600005], pre[600005];
// i 表示第 i 张卡片对应结点
// n+i 表示第 i 个牌堆的表头对应结点

void solve()
{
    cin >> n >> q;
    for(int i = 1; i <= n; i++)
    {
        nxt[n + i] = i;
        pre[i] = n + i;
    }
    while(q--)
    {
        int c, p;
        cin >> c >> p;
        
        nxt[pre[c]] = 0; // c 所在牌堆的上一张牌 的后面 没有牌了
        
        pre[c] = p;
        nxt[p] = c;
    }
    for(int i = 1; i <= n; i++)
    {
        int p = nxt[n + i]; // 从表头 n+i 开始 向后查找
        int cnt = 0;        // 统计卡片数量
        while(p != 0)       // 0 表示置空,在遇到 0 之前找到的每个点都是一张卡片
        {
            cnt++;
            p = nxt[p];
        }
        cout << cnt << " ";
    }
}

E - Unbalanced ABC Substrings

  • 预估难度:普及+/提高
  • 标签:前缀和、数学、容斥

题意

给定一个由 ABC 组成的长度为 \(N\) 的字符串 \(S\)。明显 \(S\) 共有 \(\frac{N(N+1)}{2}\) 个非空子串。

求其中有多少个子串满足:

  • 字符 ABC 的出现次数互不相同。

即使两个子串作为字符串是相同的,只要它们在 \(S\) 中出现的位置不同,就记作不同的子串。

思路

\(\text{preA}[i], \text{preB}[i], \text{preC}[i]\) 分别表示字符串前 \(i\) 个位置中出现字符 ABC 的次数。

如果一段区间 \([l, r]\) 所描述的子串符合题意,那么应当同时满足以下三个条件:

  • \(\text{preA}[r] - \text{preA}[l-1] \ne \text{preB}[r] - \text{preB}[l-1]\)
  • \(\text{preA}[r] - \text{preA}[l-1] \ne \text{preC}[r] - \text{preC}[l-1]\)
  • \(\text{preB}[r] - \text{preB}[l-1] \ne \text{preC}[r] - \text{preC}[l-1]\)

同时满足三个不等式的区间数量不好统计,可以考虑计算不符合条件的区间数量,再借助容斥反过来求符合条件的区间数。

也就是说,当以下三个条件有一个满足时,对应的区间不符合题意

  • \(A\)\(\text{preA}[r] - \text{preA}[l-1] = \text{preB}[r] - \text{preB}[l-1]\)

    • \(\text{preA}[r] - \text{preB}[r] = \text{preA}[l-1] - \text{preB}[l-1]\)
  • \(B\)\(\text{preA}[r] - \text{preA}[l-1] = \text{preC}[r] - \text{preC}[l-1]\)

    • \(\text{preA}[r] - \text{preC}[r] = \text{preA}[l-1] - \text{preC}[l-1]\)
  • \(C\)\(\text{preB}[r] - \text{preB}[l-1] = \text{preC}[r] - \text{preC}[l-1]\)

    • \(\text{preB}[r] - \text{preC}[r] = \text{preB}[l-1] - \text{preC}[l-1]\)

接下来记 \(x[i] = \text{preA}[i] - \text{preB}[i]\)\(y[i] = \text{preA}[i] - \text{preC}[i]\)\(z[i] = \text{preB}[i] - \text{preC}[i] = x[i] + y[i]\),那么上面三个条件等价于:

  • \(A\)\(x[r] = x[l-1]\)
  • \(B\)\(y[r] = y[l-1]\)
  • \(C\)\(x[r] + y[r] = x[l-1] + y[l-1]\)

\(|A|\) 表示满足条件 \(A\) 的区间数量,根据集合容斥:

\[|A \cup B \cup C| = |A| + |B| + |C| - |A \cap B| - |A \cap C| - |B\cap C| + |A \cap B \cap C| \]

对于 \(|A|\)\(|B|\)\(|C|\) 的做法,以 \(|A|\) 为例,转换公式后可以发现实际上就是统计有多少个不同位置的 \(\text{preA}[i] - \text{preB}[i]\) 相同。这一步可以借助计数数组+下标偏移来统计,或是直接套 map 容器亦可,然后通过枚举右端点再快速统计有多少个左端点符合条件。

接下来考虑后面四项:

  • \(|A \cap B|\)\(x[r] = x[l-1]\)\(y[r] = y[l-1]\)
  • \(|A \cap C|\)\(x[r] = x[l-1]\)\(x[r] + y[r] = x[l-1] + y[l-1]\),可以推出 \(y[r] = y[l-1]\),因此等价于 \(|A \cap B|\)
  • \(|B \cap C|\)\(y[r] = y[l-1]\)\(x[r] + y[r] = x[l-1] + y[l-1]\),可以推出 \(x[r] = x[l-1]\),因此等价于 \(|A \cap B|\)
  • \(|A \cap B \cap C|\)\(x[r] = x[l-1]\)\(y[r] = y[l-1]\)\(x[r] + y[r] = x[l-1] + y[l-1]\),同理等价于 \(|A \cap B|\)

因此后四项条件完全相同,原式可直接记作:

\[|A \cup B \cup C| = |A| + |B| + |C| - 2 \times |A \cap B| \]

最后,对于 \(|A \cap B|\),即统计有多少个区间 \([l, r]\) 满足 \(x[r] = x[l-1]\)\(y[r] = y[l-1]\),因此可以将 \((x[i], y[i])\) 记作二元组,或是通过哈希的方式改为一值,然后同样通过枚举右端点的方式,借助 map 等容器再快速统计有多少个左端点符合条件即可。

最终答案即 \(\frac{N(N+1)}{2} - |A \cup B \cup C|\)

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

代码

int n;
string s;
int preA[200005], preB[200005], preC[200005];
map<int, int> mp1, mp2, mp3; // 分别记 x, y, z 的出现次数
map<pii, int> mp4; // 记 (x, y) 二元组的出现次数

void solve()
{
    cin >> n >> s;
    s = " " + s;
    
    long long cnt = 0;
    
    mp1[0] = mp2[0] = mp3[0] = 1;
    mp4[pii(0, 0)] = 1;
    
    for(int i = 1; i <= n; i++)
    {
        preA[i] = preA[i - 1] + (s[i] == 'A');
        preB[i] = preB[i - 1] + (s[i] == 'B');
        preC[i] = preC[i - 1] + (s[i] == 'C');
        
        int x = preA[i] - preB[i];
        int y = preB[i] - preC[i];
        int z = preA[i] - preC[i]; // x + y
        
        // 统计此前有多少个左端点能够与当前位置 i 组成不合法的区间
        cnt += mp1[x] + mp2[y] + mp3[z] - 2 * mp4[pii(x, y)];
        
        mp1[x]++;
        mp2[y]++;
        mp3[z]++;
        mp4[pii(x, y)]++;
    }
    
    cout << 1LL * n * (n + 1) / 2 - cnt;
}

F - Merge Slimes 2

  • 预估难度:提高+/省选-
  • 标签:数学、线段树

题意

有一个长度为 \(N\) 的非负整数序列 \(A\),初始时所有元素均为 \(0\)。按顺序处理 \(Q\) 个查询。
对于第 \(q\) 个查询,给定整数 \(l_q, r_q, a_q\) \((1 \le l_q \le r_q \le N)\)。按顺序执行以下操作:

  • \(A_{l_q}, A_{{l_q}+1}, \dots, A_{r_q}\) 的每个元素加上 \(a_q\)
  • 然后,令 \(M=r_q-l_q+1\)\(B=(B_1,B_2,\dots,B_M)=(A_{l_q}, A_{l_q+1}, \dots, A_{r_q})\),求以下问题的答案:

\(M\) 个史莱姆 \(1,2,\dots,M\),其中第 \(m\) 个史莱姆的重量为 \(B_m\)
重复选择两个史莱姆并将它们合并的操作 \(M-1\) 次。
当合并重量为 \(x\)\(y\) 的史莱姆时,会出现一个重量为 \(x+y\) 的史莱姆,原来的两个史莱姆消失。此操作会产生 \(x \times y\) 的代价。
\(M-1\) 次操作的最小可能总代价,结果对 \(998244353\) 取模。

注意,每个查询中对 \(A\) 的修改会保留到后续的查询中。

思路

首先考虑史莱姆合并的子问题。

由于每次合并两个数字 \(x\)\(y\) 时,生成的新数字是 \(x + y\),而操作代价则是 \(x \times y\)

这说明对于原本 \(B\) 数组中的两个数字 \(B_i, B_j\) 而言,他们一定会在某一次合并操作中被选择成为乘法的左右两侧的数值。又因为生成的新数字是通过加法合并的,根据分配律,这两个数共同对代价的贡献不会因为操作的先后顺序而发生改变,恒等于 \(B_i \times B_j\)

因此整个问题的总代价即 \(\sum\limits_{1 \le i \lt j \le M} B_i \cdot B_j\)

\(S = \sum\limits_{i=1}^M B_i = B_1 + B_2 + \dots + B_M\),那么:

\[\begin{aligned} S^2 &= (B_1 + B_2 + \dots + B_M) \times (B_1 + B_2 + \dots + B_M) \\ &= (B_1^2 + B_2^2 + \dots + B_M^2) + 2 \sum\limits_{1 \le i \lt j \le M} B_i \cdot B_j \end{aligned} \]

\(T = \sum\limits_{i=1}^M B_i^2 = B_1^2 + B_2^2 + \dots + B_M^2\),那么代价便可以描述为 \(\dfrac{S^2-T}{2}\)

回到原问题,每次需要实现对数组的:

  1. 区间修改
  2. 区间和查询
  3. 区间平方和查询

因此考虑带懒惰标记的线段树。

考虑第 3 步维护区间平方和的步骤。当我们将某个数字 \(x \rightarrow x + v\) 时,其平方值从 \(x^2\) 变为了 \((x+v)^2 = x^2 + 2xv + v^2\),也就是在原来值的基础上增加了 \(2xv + v^2\)

考虑区间平方和,如果一段区间 \([l, r]\) 每个数都加上了 \(v\),那么区间的平方和也就会增加 \(2v\sum x + (r-l+1)v^2\)

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

代码

typedef long long ll;
const ll mod = 998244353;
const ll inv2 = (mod + 1) / 2; // 2 的逆元

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

struct node
{
    int l, r;
    ll sum, sum2;
    // sum 表示区间和
    // sum2 表示区间平方和
    ll lazy;
    // lazy 表示区间加的懒惰标记
};
node tr[100005 << 2];

// 答案上传
void push_up(int p)
{
    tr[p].sum = (tr[ls].sum + tr[rs].sum) % mod;
    tr[p].sum2 = (tr[ls].sum2 + tr[rs].sum2) % mod;
}

// 标记下传
void push_down(int p)
{
    if(tr[p].lazy == 0)
        return;
    
    tr[ls].lazy = (tr[ls].lazy + tr[p].lazy) % mod;
    tr[rs].lazy = (tr[rs].lazy + tr[p].lazy) % mod;
    
    tr[ls].sum2 = (tr[ls].sum2 + 2 * tr[p].lazy * tr[ls].sum % mod) % mod;
    tr[ls].sum2 = (tr[ls].sum2 + (tr[ls].r - tr[ls].l + 1) * tr[p].lazy % mod * tr[p].lazy % mod) % mod;
    
    tr[rs].sum2 = (tr[rs].sum2 + 2 * tr[p].lazy * tr[rs].sum % mod) % mod;
    tr[rs].sum2 = (tr[rs].sum2 + (tr[rs].r - tr[rs].l + 1) * tr[p].lazy % mod * tr[p].lazy % mod) % mod;
    
    tr[ls].sum = (tr[ls].sum + (tr[ls].r - tr[ls].l + 1) * tr[p].lazy % mod) % mod;
    tr[rs].sum = (tr[rs].sum + (tr[rs].r - tr[rs].l + 1) * tr[p].lazy % mod) % mod;
    
    tr[p].lazy = 0;
}

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

// 区间修改 [l, r] += v
void update(int l, int r, ll v, int p = 1)
{
    if(l <= tr[p].l && tr[p].r <= r)
    {
        tr[p].lazy = (tr[p].lazy + v) % mod;
        
        tr[p].sum2 = (tr[p].sum2 + 2 * v * tr[p].sum % mod) % mod;
        tr[p].sum2 = (tr[p].sum2 + (tr[p].r - tr[p].l + 1) * v % mod * v % mod) % mod;
        
        tr[p].sum = (tr[p].sum + (tr[p].r - tr[p].l + 1) * v % mod) % mod;
        return;
    }
    push_down(p);
    if(l <= tr[ls].r)
        update(l, r, v, ls);
    if(r >= tr[rs].l)
        update(l, r, v, rs);
    push_up(p);
}

// 求区间和
ll query1(int l, int r, int p = 1)
{
    if(l <= tr[p].l && tr[p].r <= r)
        return tr[p].sum;
    push_down(p);
    ll res = 0;
    if(l <= tr[ls].r)
        res = (res + query1(l, r, ls)) % mod;
    if(r >= tr[rs].l)
        res = (res + query1(l, r, rs)) % mod;
    return res;
}

// 求区间平方和
ll query2(int l, int r, int p = 1)
{
    if(l <= tr[p].l && tr[p].r <= r)
        return tr[p].sum2;
    push_down(p);
    ll res = 0;
    if(l <= tr[ls].r)
        res = (res + query2(l, r, ls)) % mod;
    if(r >= tr[rs].l)
        res = (res + query2(l, r, rs)) % mod;
    return res;
}

void solve()
{
    int n, q;
    cin >> n >> q;
    
    build(1, n);
    
    for(int i = 1; i <= q; i++)
    {
        int l, r, a;
        cin >> l >> r >> a;
        update(l, r, a);
        
        ll S = query1(l, r), T = query2(l, r);
        cout << (S * S % mod - T + mod) % mod * inv2 % mod << "\n";
    }
}
posted @ 2026-04-25 22:42  StelaYuri  阅读(264)  评论(0)    收藏  举报