AtCoder Beginner Contest 459 ABCDE

A - Hell, World!

  • 预估难度:入门

题意

给定一个介于 1 到 10 之间的整数 \(X\)

请输出将字符串 HelloWorld 中的\(X\) 个字符删除后所得到的字符串。

代码

void solve()
{
    int x;
    cin >> x;
    
    string s = "HelloWorld";
    for(int i = 1; i <= 10; i++)
        if(i != x)
            cout << s[i - 1];
}

B - 459

  • 预估难度:入门

题意

给定 \(N\) 个由小写英文字母组成的字符串 \(S_1, S_2, \ldots, S_N\)

按照以下规则定义 \(N\) 个数字 \(C_1, C_2, \ldots, C_N\)

  • 如果 \(S_i\) 的第一个字符是 abc,则 \(C_i=\) 2
  • 如果 \(S_i\) 的第一个字符是 def,则 \(C_i=\) 3
  • 如果 \(S_i\) 的第一个字符是 ghi,则 \(C_i=\) 4
  • 如果 \(S_i\) 的第一个字符是 jkl,则 \(C_i=\) 5
  • 如果 \(S_i\) 的第一个字符是 mno,则 \(C_i=\) 6
  • 如果 \(S_i\) 的第一个字符是 pqrs,则 \(C_i=\) 7
  • 如果 \(S_i\) 的第一个字符是 tuv,则 \(C_i=\) 8
  • 如果 \(S_i\) 的第一个字符是 wxyz,则 \(C_i=\) 9

请按顺序将 \(C_1, C_2, \ldots, C_N\) 拼接起来,并输出最终得到的字符串。


提示:这其实就是经典的老式手机(九宫格键盘)按键映射规则哦!只要看每个单词的首字母落在哪个数字键上就可以。

思路

借助条件分支来确定首字母对应的数字,根据 ASCII 码范围进行判断可以更方便。

代码

int f(char c)
{
    if(c <= 'c')
        return 2;
    if(c <= 'f')
        return 3;
    if(c <= 'i')
        return 4;
    if(c <= 'l')
        return 5;
    if(c <= 'o')
        return 6;
    if(c <= 's')
        return 7;
    if(c <= 'v')
        return 8;
    return 9;
}

void solve()
{
    int n;
    cin >> n;
    while(n--)
    {
        string s;
        cin >> s;
        cout << f(s[0]);
    }
}

C - Drop Blocks

  • 预估难度:普及/提高-
  • 标签:计数、模拟

题意

\(N\) 个单元格从左到右排列。最初,没有任何一个单元格里有方块。

你需要按顺序处理 \(Q\) 个查询。每个查询是以下两种类型之一:

  • 1 x:往左数第 \(x\) 个单元格中放置 \(1\) 个方块。然后,如果此时每一个单元格中都至少有 \(1\) 个方块,则从每一个单元格中移除 \(1\) 个方块。
  • 2 y:输出当前拥有至少 \(y\) 个方块的单元格数量。

共用思路

\(\text{cnt}[i]\) 表示第 \(i\) 个单元格内的方块数量。

根据操作一可知,当每个 \(\text{cnt}[i]\)\(\gt 0\) 时,所有 \(\text{cnt}[i]\) 均需 \(-1\)。又因为每次执行操作一只会让某个 \(\text{cnt}[x]\) 变为 \(\text{cnt}[x]+1\),因此能得知每次操作结束后,数组最小值一定 \(=0\)

但如果在出现每个 \(\text{cnt}[i]\)\(\gt 0\) 的情况时,我们不去执行让所有 \(\text{cnt}[i]\)\(-1\) 的这个操作,而是转而去维护一个全局偏移量 \(M\),用它来表示此时每个单元格内的方块实际数量\(\text{cnt}[i] - M\) 个。

明显此时 \(M = \min\limits_{i=1}^{N} (\text{cnt}[i])\)

再来到操作二,那么问题就变成了找有多少个位置 \(x\) 满足 \(cnt[x] - M \ge y\),即查询此时数组中有多少个数字 \(\ge M + y\)

思路一

由于每次操作一,我们都会减少一个方块数量为 \(\text{cnt}[x]\) 的单元格,同时增加一个方块数量为 \(\text{cnt}[x]+1\) 的单元格,每个单元格内的方块数量一定只增不减

那么操作二也可以当作是问有多少个单元格的方块数量达到过 \(M+y\) 这个数字。

因此我们可以再维护一个数组 \(\text{grt}[i]\),表示有多少个单元格的方块数量达到过 \(i\) 个(也可以理解为有多少个单元格的方块数量 \(\ge i\))。

初始时,\(\text{grt}[0] = N\)。而接下来每当有个方块数量为 \(\text{cnt}[x]\) 的单元格内部又多了个方块,我们便可以认为又多了一个单元格,其方块数量达到过 \(\text{cnt}[x]+1\)。此时只需要执行 grt[cnt[x] + 1]++ 即可。

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

代码一

int n, q, minn = 0, cnt[300005], grt[300005];
// minn 表示此时 cnt 数组内最小值
// cnt[i] 表示第 i 个单元格内的方块数量
// grt[i] 表示有多少个单元格内的方块数量 >= i

void solve()
{
    cin >> n >> q;
    for(int i = 1; i <= q; i++)
    {
        int op, x;
        cin >> op >> x;
        if(op == 1)
        {
            cnt[x]++;
            grt[cnt[x]]++; // 多了一个方块数量 >= cnt[x] 的单元格
            if(grt[cnt[x]] == n) // 此时所有单元格的数量均已 >= cnt[x]
                minn = cnt[x]; // minn++ 亦可
        }
        else
        {
            if(x + minn > q)
                cout << 0 << "\n";
            else
                cout << grt[x + minn] << "\n";
        }
    }
}

思路二

由于每次操作都是按实际值查询指定范围内的数字个数,可以考虑再开一个计数数组 \(\text{val}[i]\),表示此时有多少个单元格内部恰好有 \(i\) 个方块(即有多少个 \(x\) 满足 \(\text{cnt}[x] = i\))。

那么对于每次操作一,我们只需要减少一个拥有 \(\text{cnt}[x]\) 个方块的单元格(即 val[cnt[x]]--);再加上一个拥有 \(\text{cnt}[x] + 1\) 个方块的单元格即可(即 val[cnt[x]+1]++)。

对于每次操作二,查询下标 \(i \ge M+y\)\(\text{val}[i]\) 的总和即可。

单点修改,区间查询,考虑再借助树状数组维护 \(\text{val}[i]\) 方便求总和。

注意 \(\text{val}[i]\) 值从 \(0\) 开始,树状数组使用时下标需要偏移 \(+1\);且 \(\text{val}[i]\) 最大值与 \(Q\) 有关而不是 \(N\),树状数组最大下标需要处理到 \(Q+1\) 过。

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

代码二

int n, q, cnt[300005], val[300005], minn = 0;
// cnt[i] 表示第 i 个单元格内有多少个方块
// val[i] 表示方块数量恰好为 i 的单元格有多少个
// minn 表示 a[] 的最小值

int tr[300005]; // 树状数组辅助维护 val
int lowbit(int p)
{
    return p & -p;
}
void update(int p, int v)
{
    p++; // 可能用到 0 位置,下标偏移 1
    
    while(p <= q + 1)
    {
        tr[p] += v;
        p += lowbit(p);
    }
}
int query(int p)
{
    p++;
    
    int r = 0;
    while(p)
    {
        r += tr[p];
        p -= lowbit(p);
    }
    return r;
}

void solve()
{
    cin >> n >> q;
    
    // 初始时有 n 个值为 0 的元素
    val[0] = n;
    update(0, n);
    
    for(int i = 1; i <= q; i++)
    {
        int op, x;
        cin >> op >> x;
        if(op == 1)
        {
            val[cnt[x]]--;
            update(cnt[x], -1);
            
            if(val[cnt[x]] == 0 && minn == cnt[x]) // 说明这次操作改变了唯一的最小值,minn 需要 +1
                minn++;
            cnt[x]++;
            
            val[cnt[x]]++;
            update(cnt[x], 1);
        }
        else
        {
            if(x + minn > q)
                cout << 0 << "\n";
            else
                cout << query(q) - query((x + minn) - 1) << "\n";
        }
    }
}

D - Adjacent Distinct String

  • 预估难度:普及/提高-
  • 标签:计数、排序、构造

题意

给定一个由小写英文字母组成的字符串 \(S\)

请判断是否可以通过重新排列 \(S\) 中的字符,使得任意两个相邻的字符都不相同。如果可以,请找出一种满足条件的重排结果。

思路

首先考虑无解情况。先考虑处理出现次数最多的字符,检查其是否能够通过重排使得没有任何相邻且相同的情况出现。明显我们可以从下标 \(1\) 的位置开始,隔一个位置就放一个字母,那么根据字符串长度 \(|S|\) 的奇偶性,可以分类讨论:

  • \(|S|\) 是一个偶数,从下标 \(1\) 开始每隔一个位置放一个字符,最后一个字符应在倒数第二个位置,最多恰好能放 \(\frac{|S|}{2}\) 个字符。
  • \(|S|\) 是一个奇数,从下标 \(1\) 开始每隔一个位置放一个字符,最后一个字符应在最后一个位置,最多恰好能放 \(\lceil\frac{|S|}{2}\rceil\) 个字符。

因此,当出现次数最多的字符出现次数 \(\gt \lceil\frac{|S|}{2}\rceil\),则说明无解;反之,明显其它字符的出现次数均不会超过字符串长度的一半,只要出现次数最多的这种字符能放完,其余字符也一定可以保证不会出现相邻且相同的情况。

当解存在时,考虑构造方案。可以像上面描述的这样,我们先按每种字符的出现次数排序,优先放出现次数较多的字符。为了保证相同字符不相邻,可以考虑隔位放置,先把所有奇数下标位置放满,即按顺序把每一个字符放到 \(1, 3, 5, 7, \ldots\) 这些位置上。当奇数位置全部放完时,再回到开头把偶数位置按 \(2, 4, 6, 8, \dots\) 的顺序继续填满即可。

单组数据时间复杂度 \(O(|S| + \sigma\log \sigma)\),其中 \(\sigma = 26\)

代码

int cnt[128]; // 统计每种字符出现的次数
char ans[1000005];

void solve()
{
    for(char c = 'a'; c <= 'z'; c++)
        cnt[c] = 0; // 数组清空
    
    string s;
    cin >> s;
    
    for(char c : s)
        cnt[c]++; // 统计每种字符出现的次数
    
    vector<pair<int, char>> G; // 将各英文字母按 <数量,字符> 合并成二元组进行存储
    for(char c = 'a'; c <= 'z'; c++)
        if(cnt[c] != 0)
            G.push_back(pair<int, char>(cnt[c], c));
    
    sort(G.begin(), G.end()); // 按出现数量从小到大排序
    
    int n = s.size();
    if(G.back().first > (n + 1) / 2)
    {
        cout << "No\n";
        return;
    }
    cout << "Yes\n";
    
    for(int i = 1; i <= n; i += 2) // 先把奇数位置按顺序填满
    {
        ans[i] = G.back().second;
        if(--G.back().first == 0)
            G.pop_back();
    }
    for(int i = 2; i <= n; i += 2) // 再把偶数位置按顺序填满
    {
        ans[i] = G.back().second;
        if(--G.back().first == 0)
            G.pop_back();
    }
    
    for(int i = 1; i <= n; i++)
        cout << ans[i];
    cout << "\n";
}
int main()
{
	int T;
    cin >> T;
    while(T--)
        solve();
   	return 0;
}

E - Select from Subtrees

  • 预估难度:普及+/提高
  • 标签:组合数学、树形DP

题意

有一棵包含 \(N\) 个节点的有根树 \(T\),节点分别编号为 \(1, 2, \dots, N\)
节点 \(1\) 是树 \(T\) 的根节点,而节点 \(i\) \((2 \leq i \leq N)\) 的(直接)父节点是 \(P_i\)

此外,每个节点 \(i\) \((1 \leq i \leq N)\) 拥有 \(C_i\) 颗糖果。每一颗糖果都是不同的(可区分的)。

高桥给 \(N\) 只松鼠下达了指令。具体来说,他向第 \(i\) 只松鼠 \((1 \leq i \leq N)\) 下达了如下指令:

  • 在以节点 \(i\) 为根的子树中,选择并收集 \(D_i\) 颗糖果。

不同的松鼠不能拿走同一颗糖果。
请输出所有松鼠选择糖果的可能方案数,对 \(998244353\) 取模。

即使最终被选中的糖果集合完全相同,只要选择的松鼠不同,就会被视为不同的方案。

思路

根据题意,每个结点上都有一只松鼠,并且每个结点的松鼠能够收集的糖果集合可能会与该点的子孙结点中的松鼠能够收集的糖果集合发生冲突。但很明显子孙节点上的松鼠能够收集的糖果集合一定是该点的子集。

为了解决冲突,我们可以先让子孙结点的松鼠收集糖果,然后再让当前结点的松鼠在那些剩余的糖果中再进行收集。

对于整棵树的收集顺序,可以先让叶子结点的松鼠进行收集,然后再逐步向上依次解决非叶子结点的松鼠,最后解决根结点。


对于叶子节点 \(i\) 而言,该位置的松鼠需要在 \(C_i\) 个糖果中选择 \(D_i\) 个进行收集,方案数量共 \(\large\binom{C_i}{D_i}\) 种。选完后剩余 \(C_i-D_i\) 个糖果未被使用。

对于非叶子结点 \(i\) 而言,当前节点上有 \(C_i\) 个糖果,但其子孙结点内可能会有糖果没有被选中,因此该位置的松鼠能够收集的糖果方案共 \(\large\binom{C_i + \text{i 的子孙节点中未被选择的糖果数}}{D_i}\) 种。

因此可以考虑深搜整棵树,对于每个点先解决子结点的问题,然后让每个子结点传回一个整数,表示子树内剩余的未被选择的糖果数量。对于每个结点单独计算组合方案数量即可,最终总方案数即每只松鼠的选择方案数乘积。


但需要注意的是,本题的 \(C_i \le 10^9\),若直接预处理组合数,下标需要达到 \(10^9 \times 2 \times 10^5\) 的级别,不可能实现。

但又因为本题限制 \(\sum D_i \le 10^6\),因此组合数公式 \(\large\binom{n}{m} = \dfrac{\frac{n!}{(n-m)!}}{m!}\) 的上半部分可以考虑直接暴力 for 循环求 \(n-m+1 \sim n\) 的累乘,而下半部分只需要处理 \(10^6\) 以内的所有阶乘逆元就够用。

特别注意 for 循环求累乘的过程,有可能对于某个节点而言,能被选的糖果数量已经远超 int 类型最值,此时如果直接乘进答案可能会导致溢出,最好在乘之前先单独取模一次。

时间复杂度 \(O(N + \sum D_i)\)

代码

const ll mod = 998244353;

int n, c[200005], d[200005];
vector<int> G[200005]; // G[i] 存储 i 的子结点
ll ans = 1; // 答案数量

ll fac[1000005], inv[1000005];

ll qpow(ll a, ll n)
{
    ll r = 1;
    while(n)
    {
        if(n & 1)
            r = r * a % mod;
        a = a * a % mod;
        n >>= 1;
    }
    return r;
}

void init()
{
    fac[0] = 1;
    for(int i = 1; i <= 1000000; i++)
        fac[i] = fac[i - 1] * i % mod;
    inv[1000000] = qpow(fac[1000000], mod - 2);
    for(int i = 999999; i >= 0; i--)
        inv[i] = inv[i + 1] * (i + 1) % mod;
}

ll dfs(int u)
{
    ll cnt = c[u]; // 当前结点拥有的糖果数量
    for(int &v : G[u])
        cnt += dfs(v); // 子树内剩余的糖果数量
    
    // 该节点的松鼠需要从 cnt 个糖果中选出 d[u] 个
    
    if(cnt < d[u]) // 无解
    {
        ans = 0;
        return 0;
    }
    
    // (cnt, d[u]) 组合方案的分子
    for(ll i = cnt; i > cnt - d[u]; i--)
        ans = ans * (i % mod) % mod;
    
    // (cnt, d[u]) 组合方案的分母
    ans = ans * inv[d[u]] % mod;
    
    return cnt - d[u]; // 还剩 cnt - d[u] 个没用,交给祖先节点
}

void solve()
{
    init();
    
    cin >> n;
    for(int i = 2; i <= n; i++)
    {
        int fa;
        cin >> fa;
        G[fa].push_back(i);
    }
    for(int i = 1; i <= n; i++)
        cin >> c[i];
    for(int i = 1; i <= n; i++)
        cin >> d[i];
    
    dfs(1);
    
    cout << ans;
}
posted @ 2026-05-23 22:21  StelaYuri  阅读(99)  评论(0)    收藏  举报