AtCoder Beginner Contest 476 ABCDE

A - Appender

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

题意

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

请按以下规则确定并输出字符串 \(T\)

  • 如果 \(S\) 的最后一个字符是 e,则 \(T\) 为在 \(S\) 末尾追加 r 后得到的字符串。
  • 如果 \(S\) 的最后一个字符不是 e,则 \(T\) 为在 \(S\) 末尾追加 er 后得到的字符串。

数据范围

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

代码

void solve()
{
    string s;
    cin >> s;
    if(s.back() == 'e')
        cout << s << "r";
    else
        cout << s << "er";
}

B - Wild Card

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

题意

给定一个长度为 \(N\)、由小写英文字母组成的字符串 \(S\)
给定一个长度为 \(N\)、由小写英文字母和 * 组成的字符串 \(T\)

当且仅当将 \(T\) 中的每个 * 替换为某个小写英文字母后,\(T\) 能与 \(S\) 相等时,我们称 \(S\)\(T\) 匹配。

请判断 \(S\) 是否与 \(T\) 匹配。

数据范围

  • \(1 \leq N \leq 100\)

思路

对于每个对应位置 \(i\) 的字符 \(S_i, T_i\),只有在满足以下两个条件之一时,才能说明是匹配的:

  • \(S_i = T_i\)
  • \(T_i\) = *

直接判断一遍字符串中所有位置的字符是否均匹配即可。

代码

void solve()
{
    int n;
    string s, t;
    cin >> n >> s >> t;
    for(int i = 0; i < n; i++)
        if(t[i] != '*' && s[i] != t[i])
        {
            cout << "No";
            return;
        }
    cout << "Yes";
}

C - Third Largest Number

  • 预估难度:普及-
  • 标签:循环结构

题意

给定一个不小于 \(3\) 的整数 \(N\),以及一个长度为 \(N\) 的正整数序列 \(A=(A_1,A_2,\ldots,A_N)\)

对于每个 \(k=3,4,\ldots,N\),请解决以下问题:

  • \(A_1,A_2,\ldots,A_k\) 降序排序,然后输出排在第三位的数。

数据范围

  • \(3\le N\le 5\times 10^5\)
  • \(1\le A_i\le 10^9\)

思路

动态求前 \(k\) \((k \ge 3)\) 个数中的第三大的数,并列也分配不同排名。

按顺序考虑整个数组从前往后的每个数 \(A_i\),并使用三个变量 mx1mx2mx3 动态维护前三大的值即可。

以最大值为例,如果新来的数 \(A_i \ge\) mx1,说明新来的这个数将成为新的最大值,那么原本的最大值将会变成次大值,原本的次大值将会变成第三大值。注意变量的更新顺序即可。

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

当然本题也可以直接套对顶堆算法,令较大的堆内元素个数恒等于 \(2\),以实现动态维护第三大值,时间复杂度 \(O(N\log N)\),但没必要。

代码

void solve()
{
    int n;
    cin >> n;
    
    int mx1 = 0; // 前 i 个数中第一大的数
    int mx2 = 0; // 前 i 个数中第二大的数
    int mx3 = 0; // 前 i 个数中第三大的数
    
    for(int i = 1; i <= n; i++)
    {
        int x;
        cin >> x;
        if(x >= mx1) // 成为新的最大值
        {
            mx3 = mx2;
            mx2 = mx1;
            mx1 = x;
        }
        else if(x >= mx2) // 成为新的第二大值
        {
            mx3 = mx2;
            mx2 = x;
        }
        else if(x >= mx3) // 成为新的第三大值
            mx3 = x;
        
        if(i >= 3)
            cout << mx3 << "\n";
    }
}

D - Automat

  • 预估难度:普及
  • 标签:贪心、排序、数学、前缀和、二分/双指针

题意

在 AtCoder 王国,流通着两种纸币:\(1\) 美元纸币和 \(K\) 美元纸币。

在王国 J 公司的全自动食堂中,甜点和饮料作为商品出售。

该公司的全自动食堂有一台甜点自动售货机,出售 \(N\) 种甜点;还有一台饮料自动售货机,出售 \(M\) 种饮料。

甜点编号为 \(1\)\(N\),饮料编号为 \(1\)\(M\)
甜点 \(i\) 的价格为 \(A_i\) 美元,饮料 \(j\) 的价格为 \(B_j\) 美元。

在付款时,甜点自动售货机可以同时接受 \(1\) 美元纸币和 \(K\) 美元纸币,但饮料自动售货机仅能接受 \(K\) 美元纸币。
两台自动售货机都只使用 \(1\) 美元纸币进行找零。
自动售货机同一时间只能卖一件商品,只有当上一件商品付款成功并找零后才能继续买下一件商品。
而对于每一种商品,你最多只能够买一件。

高桥带着 \(X\)\(1\) 美元纸币和 \(Y\)\(K\) 美元纸币来到了 J 公司的全自动食堂。请找出他最多能购买多少件商品。

数据范围

  • \(1 \leq N \leq 2 \times 10^5\)
  • \(1 \leq M \leq 2 \times 10^5\)
  • \(2 \leq K \leq 10^9\)
  • \(0 \leq X \leq 10^{15}\)
  • \(0 \leq Y \leq 10^9\)
  • \(1 \leq A_i \leq 10^9\) (\(1 \leq i \leq N\))
  • \(1 \leq B_j \leq 10^9\) (\(1 \leq j \leq M\))

思路

首先贪心可得,为了让能买的商品数量最多,我们肯定优先购买最便宜的那些商品。因此可以先将所有甜点和所有饮料按价格排序。

由于甜品售货机支持 \(1\) 美元纸币付款,因此甜品这边只要带的总金额付得起就能买。饮料售货机只支持 \(K\) 美元纸币付款,因此需要重点关注饮料购买的数量。

对于单价为 \(B_j\) 美元的饮料,购买所需要的 \(K\) 美元纸币数量可以通过 \(\lceil\dfrac{B_j}{K}\rceil = \lfloor\dfrac{B_j+K-1}{K}\rfloor\) 计算得到。

接下来考虑枚举要购买的饮料数量 \(i = 0 \sim M\),先求出购买价格最便宜的 \(i\) 瓶饮料需要的纸币数量,判断携带的纸币数量 \(Y\) 是否足够,然后再将购买所剩余的钱全部用于买甜品即可。

pa[i] 表示最便宜的 \(i\) 个甜点总价格,记 pb[i] 表示最便宜的 \(i\) 瓶饮料总价格。当购买 \(i\) 瓶饮料后,剩余钱数可以通过 \((X+Y\times K) -\)pb[i] 计算得出,然后为了找出最多能继续买多少份甜点,可以借助二分或是双指针找到一个最大的且不超过剩余钱数的 pa[j] 即可。

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

代码

typedef long long ll;

int n, m;
ll k, x, y, a[200005], b[200005];
ll pa[200005], pb[200005], need[200005];
// pa[i] 表示最便宜的 i 个甜点总价格
// pb[i] 表示最便宜的 i 瓶饮料总价格
// need[i] 表示购买最便宜的 i 瓶饮料总共需要多少张 k 美元纸币

void solve()
{
    cin >> n >> m >> k;
    cin >> x >> y;
    for(int i = 1; i <= n; i++)
        cin >> a[i];
    for(int i = 1; i <= m; i++)
        cin >> b[i];
    
    sort(a + 1, a + n + 1);
    sort(b + 1, b + m + 1);

    int ans = 0; // 答案
    for(int i = 1; i <= n; i++)
    {
        pa[i] = pa[i - 1] + a[i];
        if(pa[i] <= x + y * k) // 如果只买甜点,求最多能买多少份
            ans = i;
    }
    for(int i = 1; i <= m; i++)
    {
        pb[i] = pb[i - 1] + b[i];
        need[i] = need[i - 1] + (b[i] + k - 1) / k;
    }
    
    // i 枚举要买的饮料数量
    // j 双指针,维护目前最多能买的甜点数量
    for(int i = 1, j = ans; i <= m; i++)
    {
        if(need[i] > y) // k 美元纸币不够用
            break;
        ll z = (x + y * k) - pb[i]; // 剩余钱数,包括找零的
        while(j >= 1 && pa[j] > z) // 剩下的钱全用来买甜点,求最大数量 j
            j--;
        ans = max(ans, i + j);
    }
    
    cout << ans;
}

E - Min-Max Swap

  • 预估难度:提高
  • 标签:线段树

题意

给定一个 \((1,2,\ldots,N)\) 的排列 \(P=(P_1,P_2,\ldots,P_N)\),以及两个长度均为 \(M\) 的整数序列 \(L=(L_1,L_2,\ldots,L_M)\)\(R=(R_1,R_2,\ldots,R_M)\)

请按 \(i=1,2,\ldots,M\) 的顺序依次执行以下操作:

  • 对于排列 \(P\),在下标区间 \([L_i, R_i]\) 范围内,交换最小值与最大值所在位置。

在完成这 \(M\) 次操作后,请输出排列 \(P\)

数据范围

  • \(2\le N\le 2\times 10^5\)
  • \(1\le M\le 2\times 10^5\)
  • \(1\le L_i \lt R_i\le N\)

思路

考虑线段树,每个结点维护对应区间内的最大值与最小值下标。

答案上传部分,根据左右儿子给出的各自区间内最值下标情况,分类讨论来决定父结点的最值下标应从哪个子结点的最值下标转移得来即可。

区间查询部分,结合答案上传的方式,来将线段树上的多个相邻区间合并成一个大区间,来求解区间最值位置。

由于每次只需要交换最小值与最大值所在位置,也就是每次操作只需要再针对这两个最值位置进行两遍单调修改即可。

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

代码

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

struct node
{
    int l, r;
    int maxp, minp; // 区间内最大值与最小值所在位置
};
node tr[200005 << 2];

int A[200005];

// 合并 L R 两相邻区间答案
node push_up(node L, node R)
{
    node M;
    M.l = L.l;
    M.r = R.r;
    
    // 比较左右两结点的最大值,哪边更大取哪边,下同
    if(A[L.maxp] > A[R.maxp])
        M.maxp = L.maxp;
    else
        M.maxp = R.maxp;
    
    if(A[L.minp] < A[R.minp])
        M.minp = L.minp;
    else
        M.minp = R.minp;
    
    return M;
}

// 建树
void build(int l, int r, int p = 1)
{
    if(l == r) // 叶子
    {
        tr[p].l = tr[p].r = tr[p].maxp = tr[p].minp = l;
        return;
    }
    int mid = (l + r) / 2;
    build(l, mid, ls);
    build(mid + 1, r, rs);
    tr[p] = push_up(tr[ls], tr[rs]);
}

// 单点更新,修改与 pos 位置相关的结点
void update(int pos, int p = 1)
{
    if(tr[p].l == tr[p].r) // 到达叶子
        return;
    if(pos <= tr[ls].r) // 待修改的点在左儿子内部
        update(pos, ls);
    else
        update(pos, rs);
    tr[p] = push_up(tr[ls], tr[rs]);
}

// 查询区间 [l, r] 内最值位置
node query(int l, int r, int p = 1)
{
    if(l <= tr[p].l && tr[p].r <= r) // 结点被询问区间完整覆盖
        return tr[p];
    if(r <= tr[ls].r) // 询问只和左儿子有关
        return query(l, r, ls);
    if(l >= tr[rs].l) // 询问之和右儿子有关
        return query(l, r, rs);
    return push_up(query(l, r, ls), query(l, r, rs)); // 询问和左右儿子均有关,合并后返回
}

void solve()
{
    int n, m;
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
        cin >> A[i];
    build(1, n);
    
    while(m--)
    {
        int l, r;
        cin >> l >> r;
        node nd = query(l, r);
        swap(A[nd.maxp], A[nd.minp]); // 交换最值位置
        update(nd.maxp); // 针对两个最值位置,更新线段树相关结点
        update(nd.minp);
    }
    
    for(int i = 1; i <= n; i++)
        cout << A[i] << " ";
}
posted @ 2026-09-19 21:39  StelaYuri  阅读(51)  评论(0)    收藏  举报