AtCoder Beginner Contest 475 ABCDE

A - mnclr

  • 预估难度:入门
  • 标签:字符串

题意

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

请在 \(S\) 的每两个相邻字符之间插入字符 o,并输出插入后得到的新字符串。

数据范围

  • \(S\) 仅由小写英文字母组成,且长度在 \(2\)\(10\) 之间

思路

做法很多,简单的方法是先单独输出第一个字符,然后在输出后面的每个字符之前先输出一个 o

代码

void solve()
{
    string s;
    cin >> s;
    
    cout << s[0];
    for(int i = 1; i < s.size(); i++)
        cout << 'o' << s[i];
}

B - Change

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

题意

在 AtCoder 王国,流通着三种硬币(\(1\) 元、\(10\) 元、\(100\) 元面额)以及 \(1000\) 元面额的纸币。

高桥进行了 \(N\) 次购物,初始时他拥有 \(10^{100}\)\(1000\) 元纸币,且没有任何硬币。

在第 \(i\) 次购物中,他购买了一件价值 \(A_i\) 元的商品。

在付款时,他仅会使用 \(1000\) 元纸币进行支付,且使用的纸币数量是满足支付的总额不少于 \(A_i\) 元的最小数量,随后他会收到找零,且保证找零获得的硬币总数量是最少的。

请计算在完成这 \(N\) 次购物后,高桥手中每种硬币的数量分别是多少。

数据范围

  • \(1\leq N \leq 1000\)
  • \(1\leq A_i \leq 10^5\)

思路

首先,如果商品单价恰好为 \(1000\) 的倍数,那么将不会收到任何找零,可以直接用 \(1000\) 元面额的纸币付款。

如果单价不是 \(1000\) 的倍数,假设单价为 \(X\),先将可以用 \(1000\) 元纸币直接支付的部分去除,剩余部分即 \(X \bmod 1000\),此时再花一张 \(1000\) 元纸币,可获得 \(1000 - 1000 \bmod X\) 的找零。明显这个数字是一个小于 \(1000\) 的正整数。

考虑最小的找零硬币方案,明显能用大面额硬币找零时就会优先使用大面额,又根据三种硬币面额恰好表示 \(1, 10, 100\),因此可以直接取找零数值的百位作为 \(100\) 元硬币的数量,十位作为 \(10\) 元硬币的数量,个位作为 \(1\) 元硬币的数量。

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

代码

void solve()
{
    int n;
    cin >> n;
    
    int a = 0, b = 0, c = 0;
    
    for(int i = 1; i <= n; i++)
    {
        int x;
        cin >> x;
        if(x % 1000 == 0) // 不会收到找零
            continue;
        x = 1000 - x % 1000; // 求找零的钱数
        
        a += x % 10;
        b += x / 10 % 10;
        c += x / 100;
    }
    
    cout << a << " " << b << " " << c;
}

C - Walk the Line

  • 预估难度:普及-
  • 标签:枚举、前缀和

题意

\(N\) 个城镇排成一条直线。这些城镇的编号依次为 \(1, 2, \ldots, N\)

对于每个整数 \(i\) \((1 \leq i \leq N-1)\),城镇 \(i\) 和城镇 \(i+1\) 之间有一条长度为 \(A_i\) 的道路相连。

你最初位于城镇 \(S\)。你可以沿着道路在相连的两个城镇之间反复移动。

请计算在总移动距离不超过 \(L\) 的一系列移动中,最多可能访问到的城镇数量。

注意:城镇 \(S\) 包含在访问过的城镇中;如果一个城镇被访问了多次,也只计算一次。

数据范围

  • \(2 \leq N \leq 8000\)
  • \(1 \leq S \leq N\)
  • \(0 \leq L \leq 10^{18}\)
  • \(1 \leq A_i \leq 10^9\)

思路

注意到能够访问到的所有城镇应当是连续的,且是一段包含城镇 \(S\) 的区间。

假设我们打算访问的城镇区间可以描述为 \([i, j]\) \((i \le S \le j)\),那么为了让走过的总距离最小,明显走法只有两种:

  • \(S\) 出发向左走到 \(i\),再向右一直走到 \(j\)
  • \(S\) 出发向右走到 \(j\),再向左一直走到 \(i\)

假设 \(S\)\(i\) 的距离记作 \(X\)\(S\)\(j\) 的距离记作 \(Y\),那么这两种走法的总距离可以分别用 \(2X+Y\)\(X + 2Y\) 进行描述。我们只要判断二者较小值是否不超过题目限制 \(L\),即可判断当前区间内所有 \(j-i+1\) 个城镇是否能被全部访问到。

两点间距离可以借助前缀和思想来快速求得。至于区间 \([i, j]\),直接 \(O(N^2)\) 枚举即可。

代码

typedef long long ll;

ll A[8005], P[8005];

void solve()
{
    int N, S;
    ll L;
    cin >> N >> S >> L;
    
    for(int i = 1; i < N; i++)
    {
        cin >> A[i];
        P[i] = P[i-1] + A[i]; // 前缀和
    }
    // 从 x 到 y 的总距离 (x < y)
    // 即 A[x] + A[x+1] + ... + A[y-1]
    // 即 P[y-1] - P[x-1]
    
    int ans = 0;
    for(int i = S; i >= 1; i--) // 枚举要到达的最小编号城镇
        for(int j = S; j <= N; j++) // 枚举要到达的最大编号城镇
        {
            ll x = P[S - 1] - P[i - 1]; // S 向左走到 i 的距离
            ll y = P[j - 1] - P[S - 1]; // S 向右走到 j 的距离
            if(x + x + y <= L || x + y + y <= L)
                ans = max(ans, j - i + 1);
        }
    cout << ans;
}

D - Alphametic Prime

  • 预估难度:普及
  • 标签:素数筛、字符串、模拟

题意

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

如果存在至少一个满足以下条件的素数 \(P\),请找出任意一个。

  • \(T\) 为素数 \(P\) 在十进制表示下的字符串(不包含前导 0),\(T\) 需满足:
    • \(|S|=|T|\)
    • 对于所有的 \(1 \leq i \lt j \leq |S|\),当且仅当 \(S_i=S_j\) 时,有 \(T_i=T_j\)

数据范围

  • \(S\) 仅由小写英文字母组成,长度在 \(1\)\(7\) 之间。

思路

也就是找出格式与 \(S\) 相同的一个质数,\(S\) 中字母相同的那些位置,质数对应位上的数位也应当相同。

可以借助素数筛将 \(10^7\) 以内的所有质数全部筛出,然后枚举每个质数,将其转为字符串 \(T\) 后,直接 \(O(|S|^2)\) 判断其是否符合字符串 \(S\) 的格式即可。

对于判断过程,根据题意中所描述的“当且仅当 \(S_i=S_j\) 时,有 \(T_i=T_j\)”,可以得出对于前后两个不同位置 \(i, j\),要么 \(S_i = S_j\)\(T_i = T_j\),要么 \(S_i \ne S_j\)\(T_i \ne T_j\)。如果出现其他情况,则说明不符合格式。

\(M = 10^7\),记 \(C\) 表示 \(M\) 以内的素数个数。素数筛时间复杂度 \(O(M)\),枚举素数时间复杂度 \(O(C)\),数字转字符串时间复杂度 \(O(\log_{10} M)\),格式判断时间复杂度 \(O(|S|^2)\)。总时间复杂度 \(O(M + C(\log M + |S|^2))\)

代码

bool vis[10000005]; // 1e7
int pr[1000005], pcnt = 0;

// 素数筛
void init()
{
    vis[0] = vis[1] = true;
    for(int i = 2; i <= 10000000; i++)
    {
        if(!vis[i])
            pr[++pcnt] = i;
        for(int j = 1; j <= pcnt && pr[j] * i <= 10000000; j++)
        {
            vis[pr[j] * i] = true;
            if(i % pr[j] == 0)
                break;
        }
    }
}

// 判断字符串是否格式对应
bool check(string &s, string &t)
{
    if(s.size() != t.size())
        return false;
    for(int x = 0; x < s.size(); x++)
        for(int y = x + 1; y < s.size(); y++)
            if(s[x] == s[y] && t[x] != t[y] || s[x] != s[y] && t[x] == t[y])
                return false;
    return true;
}

void solve()
{
    init();
    
    string s;
    cin >> s;
    
    for(int i = 1; i <= pcnt; i++)
    {
        string t = to_string(pr[i]); // 质数转字符串
        if(check(s, t))
        {
            cout << t;
            return;
        }
    }
    cout << -1;
}

E - Quiz Competition: Qualifiers

  • 预估难度:提高
  • 标签:字典树

题意

有一场智力竞赛的预选赛正在举行。共有 \(N\) 名参与者,编号为 \(1\)\(N\),其中最多有 \(M\) 人可以通过预选赛。

预选赛包含 \(K\) 道二选一的问答题,每道题的答案为 ox
参与者 \(i\) 对第 \(j\) 道题的回答由字符串 \(S_i\) 的第 \(j\) 个字符给出。
\(j\) 道题的正确答案由字符串 \(T\) 的第 \(j\) 个字符给出。

晋级者按以下流程确定:

  • 初始时,晋级人数和淘汰人数均为 \(0\);所有 \(N\) 名参与者均处于“未定”状态。
  • 按照 \(k=1,2,\dots,K\) 的顺序,依次执行以下处理:
    • 如果“当前晋级人数”加上“未定参与者中答对第 \(k\) 题的人数”不超过 \(M\),那么所有未定参与者中答对第 \(k\) 题的人均成为晋级者。
    • 否则,所有未定参与者中答错第 \(k\) 题的人均成为淘汰者。
  • 所有剩余的未定参与者均成为淘汰者。

接下来给定 \(Q\) 个查询,请按顺序处理它们。

  • 给定整数 \(i\)\(j\)。将参与者 \(i\) 对第 \(j\) 题的回答从 o 改为 x,或从 x 改为 o。然后,判断参与者 \(i\) 是否通过了预选赛。

每个查询中对回答的修改在后续查询的处理中依然有效。

数据范围

  • \(1 \leq M \leq N \leq 3\times 10^4\)
  • \(1 \leq K \leq 200\)
  • \(S_i\)\(T\) 仅包含字符 ox,且长度为 \(K\)
  • \(1 \leq Q \leq 5\times 10^4\)
  • 对于每个询问,\(1\leq i \leq N\)\(1 \leq j \leq K\)

思路

由于每次考虑每个人的作答与答案之间的关系比较麻烦,不如在输入时直接将每个人每题的作答直接改成存储是否答对的状态。即记 ok[i][j] 表示第 \(i\) 个人在第 \(j\) 题目前是否答对。

由于晋级流程是按照题目的顺序从前往后看的,优先看更靠前的题目的答题情况, 且每个人在所有 \(K\) 道题里的答题结果可以描述为一个仅由 \(0/1\) 构成的长度为 \(K\) 的字符串,因此可以考虑将每个人的作答插入到一颗 \(01\)-Trie 树上进行维护,同时再对于树上每个结点,维护一个 cnt[i] 用于表示有多少个人共用了 \(i\) 号点(即之前的答题情况完全相同),便于判断人数。

先考虑修改。每次修改无非只会将某个人在某道题上的作答情况进行翻转,也就是将其中某个字符串的单个字进行修改。因此可以在修改之前先将这个人对应的作答字符串从字典树上删去,改完后再重新将新字符串插入回来,时间复杂度只取决于字符串长度 \(O(K)\)

至于判断修改完成后这个人能否晋级,那就是在字典树上进行查询。

从字典树根结点开始,记当前点为 \(p\),子结点用 \(\text{tr}[p][0/1]\) 表示,当前已知的晋级人数为 \(\text{okcnt}\)(初始为 \(0\)),根据题意模拟:

  • 如果“当前晋级人数”加上“未定参与者中答对第 \(k\) 题的人数”不超过 \(M\),那么所有未定参与者中答对第 \(k\) 题的人均成为晋级者。
    • 先考虑这道题答对的人数,即 \(\text{cnt}[\text{tr}[p][1]]\),如果发现 \(\text{okcnt} + \text{cnt}[\text{tr}[p][1]] \le M\),说明此时可以直接将该题答对的人全部算作晋级。
    • 如果此时发现当前正在判断的这个人该题作答也正确,便能够直接判定他晋级。
    • 否则,继续向后考虑该题没有答对的人,也就是字典树向 \(\text{tr}[p][0]\) 走一格。
  • 否则,所有未定参与者中答错第 \(k\) 题的人均成为淘汰者。
    • 如果此时发现当前正在判断的这个人该题作答也错误,便能够直接判定他被淘汰。
    • 否则,继续向后考虑该题答对的人,也就是向 \(\text{tr}[p][1]\) 走一格。

如此便能够同样在 \(O(K)\) 的复杂度内完成判断。

总时间复杂度 \(O((N+Q)\cdot K)\),注意字典树的最大点数也是这一级别。

代码

int n, m, k;
bool ok[30005][205];
// ok[i][j] 表示 i 这个人在 j 这道题是否答对

int tr[16000050][2], tot = 1;
int cnt[16000050]; // 字典树上每个点对应的人数

// 将 id 这个人的做题状态加到字典树上
void insert(int id)
{
    int p = 1;
    for(int i = 1; i <= k; i++)
    {
        int j = ok[id][i];
        if(tr[p][j] == 0)
            tr[p][j] = ++tot;
        p = tr[p][j];
        cnt[p]++;
    }
}

// 将 id 这个人的做题状态从字典树中去除
void del(int id)
{
    int p = 1;
    for(int i = 1; i <= k; i++)
    {
        int j = ok[id][i];
        p = tr[p][j];
        cnt[p]--;
    }
}

// 检查 id 这个人是否通过比赛
bool check(int id)
{
    int p = 1;
    int okcnt = 0; // 已经晋级的人数
    for(int i = 1; i <= k; i++)
    {
        int j = ok[id][i];
        if(okcnt + cnt[tr[p][1]] <= m) // 名额足够让当前这题答对的所有人晋级
        {
            if(j == 1) // 如果 id 也答对了,直接晋级
                return true;
            okcnt += cnt[tr[p][1]];
            p = tr[p][0]; // 继续考虑没答对的人
        }
        else // 所有答错的人均淘汰
        {
            if(j == 0) // 如果 id 也答错了,直接淘汰
                return false;
            p = tr[p][1]; // 继续考虑答对的人
        }
    }
    return false; // 剩余未定参与者均淘汰(?)
}

void solve()
{
    cin >> n >> m >> k;
    string t;
    cin >> t;
    for(int i = 1; i <= n; i++)
    {
        string s;
        cin >> s;
        for(int j = 1; j <= k; j++)
            if(t[j - 1] == s[j - 1])
                ok[i][j] = true;
        insert(i);
    }
    int q;
    cin >> q;
    while(q--)
    {
        int i, j;
        cin >> i >> j;
        
        del(i);
        ok[i][j] ^= 1; // 当前这一题的答题状态 01 翻转
        insert(i);
        
        if(check(i))
            cout << "Yes\n";
        else
            cout << "No\n";
    }
}
posted @ 2026-09-12 21:41  StelaYuri  阅读(68)  评论(1)    收藏  举报