atcoder ABC 458 题解

atcoder abc 458 题解

A - Chompers

题目描述

给定一个由小写英文字母组成的字符串 SSS 和一个正整数 NNN。SSS 的长度至少为 2N+12N+12N+1。

请找出从 SSS 的开头移除 NNN 个字符、从结尾移除 NNN 个字符后得到的字符串。

解题思路

我们直接计算在截取之后的字符串长度是多少,然后从要保留的字符串开始的地方开始,用 substr 求解即可。

代码

cpp 复制代码
#include <bits/stdc++.h>

using namespace std;

int main()
{
    string s;
    int n;
    cin >> s >> n;
    int len = s.size();
    len -= 2 * n; // 计算中间剩余字符的长度
    cout << s.substr(n, len); // 从第n个字符开始截取len个字符
    return 0;
}

B - Count Adjacent Cells

题目描述

有一个 HHH 行 WWW 列的网格。从上往下数第 iii 行、从左往右数第 jjj 列的单元格记为 (i,j)(i, j)(i,j)。

当 ∣x1−x2∣+∣y1−y2∣=1|x_1 - x_2| + |y_1 - y_2| = 1∣x1−x2∣+∣y1−y2∣=1 时,我们称单元格 (x1,y1)(x_1, y_1)(x1,y1) 和 (x2,y2)(x_2, y_2)(x2,y2) 是边相邻的。

对每个单元格,求出与它边相邻的单元格的数量。

解题思路

我们可以发现,中间的单元格最多有 4 个相邻的格子。对于每个格子,我们先假设答案是 4,然后检查它是否在边界上:如果在第一行或最后一行,就减 1;如果在第一列或最后一列,也减 1。这样就能得到每个格子的相邻数了。

代码

cpp 复制代码
#include <bits/stdc++.h>

using namespace std;

int h, w;

int main()
{
    cin >> h >> w;
    for (int i = 1; i <= h; i++, cout << '\n') // 遍历每一行
        for (int j = 1; j <= w; j++) // 遍历每一列
        {
            int ans = 4; // 初始假设最多4个相邻格子
            if (i == 1)
                ans--; // 如果在第一行,上面没有格子
            if (j == 1)
                ans--; // 如果在第一列,左边没有格子
            if (i == h)
                ans--; // 如果在最后一行,下面没有格子
            if (j == w)
                ans--; // 如果在最后一列,右边没有格子
            cout << ans << ' ';
        }
    return 0;
}

C - C Stands for Center

题目描述

给定一个由大写英文字母组成的字符串 SSS。请找出满足以下所有条件的子串(连续子序列)的数量。

  • 子串包含奇数个字符。
  • 它的中间字符是 C。更正式地说,如果提取的子串有 lll 个字符,那么它的第 ((l+1)/2)((l+1)/2)((l+1)/2) 个字符是 C

即使两个子串作为字符串完全相同,但只要它们是从不同位置提取的,就需要分别计数。

解题思路

可以对每个 C 单独考虑,因为每个符合条件的子串都必须以某个 C 为中心。对于位置 i 上的 C,左边最多可以延伸 i 个字符,右边最多可以延伸 s.size() - i - 1 个字符。我们取左右两边较小的那个值,就是以这个 C 为中心的符合条件的子串数量。把所有 C 的情况加起来就是答案了。

代码

cpp 复制代码
#include <bits/stdc++.h>
#define int long long

using namespace std;

string s;

signed main()
{
    cin >> s;
    int ans = 0;
    for (int i = 0; i < s.size(); i++) // 遍历每个字符
        if (s[i] == 'C') // 如果是'C'
            ans += min((int)s.size() - i, i + 1); // 计算以它为中心的子串数量
    cout << ans;
    return 0;
}

D - Chalkboard Median

题目描述

黑板上写着一个整数 XXX。

给定 QQQ 个查询,按顺序处理它们。第 iii 个查询(1≤i≤Q1 \le i \le Q1≤i≤Q)如下:

给出两个整数 AiA_iAi 和 BiB_iBi。在黑板上写下这两个新整数 AiA_iAi 和 BiB_iBi。

然后,输出黑板上写的 2i+12i+12i+1 个整数的中位数。

解题思路

我们可以用两个堆来维护所有数字。一个最大堆用来存较小的那一半数,这样它的顶部就是这一半数里最大的;一个最小堆用来存较大的那一半数,它的顶部就是这一半数里最小的。这样安排的好处是,中位数要么是最小堆的顶部,要么是最大堆的顶部,很容易拿到。

对于总数是奇数的情况,我们让最小堆的大小比最大堆多 1,这样中位数就是最小堆的顶部元素。每次添加新数字时,我们先看它和最大堆顶部的大小关系,如果比它大就放到最小堆,否则放到最大堆。

放完之后我们需要调整两个堆的大小平衡。如果最大堆的大小超过了最小堆,就把最大堆的顶部移到最小堆;如果最小堆的大小比最大堆多了不止 1,就把最小堆的顶部移到最大堆。这样每次调整完,中位数就一定是最小堆的顶部了。

代码

cpp 复制代码
#include <bits/stdc++.h>

using namespace std;

int x, q;

int main()
{
    priority_queue<int> q1; // 最大堆,存较小的一半
    priority_queue<int, vector<int>, greater<int>> q2; // 最小堆,存较大的一半
    cin >> x >> q;
    q1.push(x); // 初始数字放入q1
    while (q--)
    {
        int a, b;
        cin >> a >> b;
        // 将a放入合适的堆
        if (a > q1.top())
            q2.push(a);
        else
            q1.push(a);
        // 将b放入合适的堆
        if (b > q1.top())
            q2.push(b);
        else
            q1.push(b);
        // 调整堆的大小,保持q2.size() <= q1.size() + 1
        while (q1.size() > q2.size())
            q2.push(q1.top()), q1.pop();
        while (q2.size() > q1.size() + 1)
            q1.push(q2.top()), q2.pop();
        cout << q2.top() << '\n'; // 中位数就是q2的顶部
    }
    return 0;
}

E - Count 123

题目描述

求满足以下所有条件的序列 A=(a1,⋯ ,aX1+X2+X3)A = (a_1, \cdots, a_{X_1 + X_2 + X_3})A=(a1,⋯,aX1+X2+X3) 的数量,对 998244353998244353998244353 取模。序列的长度为 X1+X2+X3X_1+X_2+X_3X1+X2+X3。

  • AAA 中恰好包含 X1X_1X1 个 111、X2X_2X2 个 222 和 X3X_3X3 个 333。
  • 相邻元素的绝对差不超过 111。即对于所有满足 1≤i≤X1+X2+X3−11 \leq i \leq X_1+X_2+X_3-11≤i≤X1+X2+X3−1 的整数 iii,都有 ∣ai+1−ai∣≤1|a_{i+1} - a_i| \leq 1∣ai+1−ai∣≤1。

解题思路

观察题目条件,相邻元素的差不能超过 1,这意味着 1 和 3 不能直接相邻。我们可以先考虑如何把 1 插入到 2 的里面,然后再放 3。

我们先枚举有 i 个位置插入 1,这 i 个位置要从 x2 + 1 个空位里选(x2 个 2 之间有 x2 + 1 个空位)。选好位置后,我们要把所有的 1 分到这 i 个位置里,用插板法,就是从 x1 - 1 个间隔里选 i - 1 个板子。

最后考虑放 3。因为我们用了 i 个位置放 1,还剩下 x2 - i 个位置可以插入 3。放 3 也可以用插板法,相当于把 x3 个 3 分到 x2 - i + 1 个位置里,也就是从 x3 + x2 - i 个位置里选 x2 - i 个板子。把这三个组合数乘起来,再枚举所有可能的 i 加起来就是答案了。

代码

cpp 复制代码
#include <bits/stdc++.h>
#define int long long

using namespace std;

const int mod = 998244353, maxn = 3e6 + 10;
int x1, x2, x3, fac[maxn]; // fac[i] 是 i! mod mod

int quick_pow(int a, int b) // 快速幂,计算 a^b mod mod
{
    int ans = 1;
    while (b)
    {
        if (b & 1)
            (ans *= a) %= mod;
        a = a * a % mod;
        b >>= 1;
    }
    return ans;
}

int get_num(int a, int b) // 计算组合数 C(a, b)
{
    if (a < b || a < 0 || b < 0)
        return 0;
    return fac[a] * quick_pow(fac[a - b], mod - 2) % mod * quick_pow(fac[b], mod - 2) % mod;
}

signed main()
{
    cin >> x1 >> x2 >> x3;
    fac[0] = 1;
    for (int i = 1; i < maxn; i++) // 预处理阶乘
        fac[i] = fac[i - 1] * i % mod;
    int ans = 0;
    for (int i = 1; i <= min(x1, x2 + 1); i++) // 枚举i个1被2隔开
    {
        int sum = 1;
        (sum *= get_num(x2 + 1, i)) %= mod; // 选i个位置放分隔的2
        (sum *= get_num(x1 - 1, i - 1)) %= mod; // 把x1个1分成i段
        (sum *= get_num(x3 + x2 - i, x2 - i)) %= mod; // 放x3个3
        ans += sum;
        ans %= mod;
    }
    cout << ans;
    return 0;
}

F - Critical Misread

题目描述

给定 KKK 个由小写英文字母组成的字符串 SiS_iSi。

求长度为 NNN 的、由小写英文字母组成的字符串中,不包含任何 S1,S2,...,SKS_1, S_2, \dots, S_KS1,S2,...,SK 作为子串(连续子序列)的字符串数量,对 998244353998244353998244353 取模。

解题思路

首先把所有禁止的字符串插到 trie 树上,然后建立 AC 自动机的 fail 指针。这样我们就能知道,当我们在某个状态时,添加一个新字符会转移到哪个状态。

我们可以用动态规划来计数。dpij 表示长度为 i 的字符串,最后在 AC 自动机的状态 j 的合法方案数。转移就是从状态 j 添加一个字符 c,转移到状态 k,如果 j 和 k 都不是禁止状态,就可以转移。

因为 N 可能很大,直接递推会超时,所以我们可以把转移关系表示成矩阵,然后用矩阵快速幂来加速。这样就能在 O((K*L)^3 * logN) 的时间内解决问题(应该没算错),其中 L 是禁止字符串的平均长度。

代码

cpp 复制代码
#include <bits/stdc++.h>

using namespace std;
typedef long long ll;

const int MOD = 998244353;
const int maxn = 110;
int trie[maxn][26], fail[maxn], idx; // trie树、fail指针、节点编号
bool ban[maxn]; // 标记该状态是否包含禁止字符串
ll a[maxn][maxn], res[maxn][maxn]; // 转移矩阵和结果矩阵

void insert(char *s) // 把禁止字符串插入trie树
{
    int x = 0;
    for (int i = 0; s[i]; i++)
    {
        int c = s[i] - 'a';
        if (!trie[x][c])
            trie[x][c] = ++idx;
        x = trie[x][c];
    }
    ban[x] = true; // 标记该节点是禁止的
}

void build() // 建立AC自动机的fail指针
{
    queue<int> q;
    for (int i = 0; i < 26; i++)
        if (trie[0][i])
            q.push(trie[0][i]);
    while (!q.empty())
    {
        int u = q.front();
        q.pop();
        ban[u] |= ban[fail[u]]; // 如果fail指针指向的状态是禁止的,当前状态也是禁止的
        for (int i = 0; i < 26; i++)
        {
            if (trie[u][i])
            {
                fail[trie[u][i]] = trie[fail[u]][i];
                q.push(trie[u][i]);
            }
            else
            {
                trie[u][i] = trie[fail[u]][i]; // 路径压缩
            }
        }
    }
}

void mul(ll x[][maxn], ll y[][maxn]) // 矩阵乘法
{
    ll z[maxn][maxn] = {0};
    for (int i = 0; i <= idx; i++)
        for (int k = 0; k <= idx; k++)
            for (int j = 0; j <= idx; j++)
                z[i][j] = (z[i][j] + x[i][k] * y[k][j]) % MOD;
    for (int i = 0; i <= idx; i++)
        for (int j = 0; j <= idx; j++)
            x[i][j] = z[i][j];
}

ll solve(int n) // 矩阵快速幂求解
{
    memset(a, 0, sizeof(a));
    memset(res, 0, sizeof(res));
    for (int i = 0; i <= idx; i++)
        res[i][i] = 1; // 单位矩阵
    for (int i = 0; i <= idx; i++) // 构建转移矩阵
    {
        if (ban[i])
            continue;
        for (int j = 0; j < 26; j++)
        {
            if (!ban[trie[i][j]])
                a[i][trie[i][j]]++; // 从i转移到trie[i][j]有1种方式
        }
    }
    while (n) // 快速幂
    {
        if (n & 1)
            mul(res, a);
        mul(a, a);
        n >>= 1;
    }
    ll ans = 0;
    for (int i = 0; i <= idx; i++)
        ans = (ans + res[0][i]) % MOD; // 从初始状态0出发,所有可能的终点
    return ans;
}

int main()
{
    int n, k;
    char s[12];
    scanf("%d%d", &n, &k);
    while (k--)
    {
        scanf("%s", s);
        insert(s);
    }
    build();
    printf("%lld\n", solve(n));
    return 0;
}

G - Children Yearn for the Evil Kindergarten

题目描述

游戏场地有 1010010^{100}10100 个孩子。初始时,没有孩子有任何奖牌。

一个孩子只有在被淘汰逃跑时才会离开场地。

游戏共 NNN 天。在第 iii 天(1≤i≤N1 \leq i \leq N1≤i≤N),按顺序执行以下操作。

  • 收集场地中所有孩子持有的奖牌,设 sss 为收集到的奖牌总数。
  • 将 s+Ais + A_is+Ai 枚奖牌自由分配给场地中的孩子(如果场地中没有孩子,则不做任何操作)。
  • 在场地中的孩子里,奖牌少于 BiB_iBi 的被淘汰。奖牌不少于 BiB_iBi 的孩子各失去 BiB_iBi 枚奖牌。
  • 在场地中的孩子里,奖牌不少于 CiC_iCi 的孩子可以选择此时逃跑或留在场地。

在第 NNN 天结束时仍留在场地的孩子会被淘汰。

求最终逃跑的孩子的最大可能数量。

给定 TTT 组测试数据,请分别求解。

解题思路

这道题可以二分答案。对于某个答案 m,我们可以检查是否存在一种方式让 m 个孩子最终逃跑。如果可以,我们就尝试更大的 m;如果不行,就尝试更小的 m。

关键在于如何高效地检查某个 m 是否可行。我们可以用一些线性函数来表示每个孩子的状态变化,并用凸包来维护这些函数。因为每天的操作都是线性变换,所以可以用斜率来表示状态的变化,用一个双端队列来维护凸包。

我们需要处理的操作包括:分配奖牌、淘汰奖牌不够的孩子、让奖牌够多的孩子逃跑。这些都可以通过维护凸包上的点来实现,确保我们只保留可能得到最优解的状态。最后根据能否在凸包上找到合适的状态来判断 m 是否可行。

代码

cpp 复制代码
#include <bits/stdc++.h>

using namespace std;

typedef long long ll;
typedef pair<ll, ll> pr;

const ll INF = 1LL << 60;

ll n;
vector<ll> a, b, c;

ll slope(pr l, pr r) // 计算两点之间的斜率
{
    ll y = r.second - l.second;
    ll x = r.first - l.first;
    return y / x;
}

bool check(ll m) // 检查是否可以让m个孩子逃跑
{
    if (m <= 0)
        return true;
    ll sa = 0, sb = 0; // 线性变换的参数
    deque<pr> dots; // 双端队列维护凸包上的点
    dots.push_back({m, 0});

    auto evaldot = [&](pr p) -> ll { // 计算点p在当前变换下的值
        return sa * p.first + sb + p.second;
    };

    for (ll di = 0; di < n; di++) // 处理每一天
    {
        sb += a[di]; // 分配奖牌的影响
        sa -= b[di]; // 减去B_i的影响

        // 从队尾移除不合法的点(奖牌不够会被淘汰)
        while (dots.size())
        {
            pr p1 = dots.back();
            ll y1 = evaldot(p1);
            if (y1 >= 0)
                break;
            dots.pop_back();
            if (dots.empty())
                break;
            pr p2 = dots.back();
            ll y2 = evaldot(p2);
            if (y2 < 0)
                continue;
            ll ta = slope(p2, p1);
            ll xadd = y2 / (-(sa + ta));
            if (xadd > 0)
                dots.push_back({p2.first + xadd, p2.second + ta * xadd});
            break;
        }

        // 从队头移除不合法的点
        while (dots.size())
        {
            pr p1 = dots.front();
            ll y1 = evaldot(p1);
            if (y1 >= 0)
                break;
            dots.pop_front();
            if (dots.empty())
                break;
            pr p2 = dots.front();
            ll y2 = evaldot(p2);
            if (y2 < 0)
                continue;
            ll ta = slope(p1, p2);
            ll xadd = y2 / (sa + ta);
            if (xadd > 0)
                dots.push_front({p2.first - xadd, p2.second - ta * xadd});
            break;
        }

        if (dots.empty()) // 如果没有合法的点,m不可行
            return false;

        // 维护凸包,处理逃跑的情况
        while (dots.size() >= 2)
        {
            ll s = sa + slope(dots[0], dots[1]);
            if (s >= c[di])
                dots.pop_front();
            else
                break;
        }

        // 计算可以逃跑的孩子数量
        ll xcadd = evaldot(dots[0]) / c[di];
        if (xcadd >= dots[0].first)
            return true; // 足够让m个孩子逃跑
        else if (xcadd > 0)
            dots.push_front({dots[0].first - xcadd, dots[0].second - (c[di] - sa) * xcadd});
    }

    return false;
}

void solve() // 二分答案
{
    ll ok = 0, ng = max(a[0], 0LL) + 1;
    while (ok + 1 < ng)
    {
        ll med = (ok + ng) / 2;
        if (check(med))
            ok = med;
        else
            ng = med;
    }
    printf("%lld\n", ok);
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int T;
    cin >> T;
    while (T--)
    {
        cin >> n;
        a.resize(n);
        b.resize(n);
        c.resize(n);
        for (ll i = 0; i < n; i++)
            cin >> a[i] >> b[i] >> c[i];
        solve();
    }
    return 0;
}
相关推荐
Jerry1 小时前
LeetCode 28. 找出字符串中第一个匹配项的下标
算法
Jerry3 小时前
LeetCode 459. 重复的子字符串
算法
哥不想学算法6 小时前
【C++】字符串字面量拼接
开发语言·c++
海石6 小时前
1500分的题目,确实有实力,不过还是我略胜一筹
算法·leetcode
海石6 小时前
【记忆化搜索】条条大路通AC,走好适合你的那一条,走到后再考虑走得快
算法·leetcode
Jerry8 小时前
LeetCode 151. 反转字符串中的单词
算法
gugucoding8 小时前
31. 【C语言】堆栈与队列的实现
c语言·开发语言·数据结构·链表
ChaoZiLL10 小时前
我的数据结构3——链表(link list)
数据结构·链表
a11177610 小时前
LM 算法迭代过程动画演示(SLAM)
算法
头茬韭菜10 小时前
Context 的生死抉择:四层压缩、截断算法与 Session Memory
算法·ai