AtCoder Beginner Contest 440 ABCDEF 题目解析

A - Octave

题意

已知音高每增加 \(1\) 个八度,声音的频率就会变为原来的两倍。

如果将频率为 \(X\) 赫兹的声音的音高提高 \(Y\) 个八度,那么它的频率将是多少赫兹?

思路

提高 \(Y\) 个八度即变为原来的 \(2^Y\) 倍,输出 \(X \times 2^Y\)

代码

void solve()
{
    int x, y;
    cin >> x >> y;
    while(y--)
        x *= 2;
    cout << x;
}

B - Trifecta

题意

\(N\) 匹马参加了比赛,编号分别为 \(1, 2, \dots, N\)

所有马匹同时起跑,编号为 \(i\) 的马从起跑线出发只用了 \(T_i\) 秒就到达了终点。保证所有 \(T_i\) 都是不同的。

请输出排在第一、第二、第三的这三匹马的编号。

思路

可以简单地将 时间 和 编号 合并为结构体,按时间从小到大排序后,再按顺序输出前三名的编号。

或者也可以暴力地打擂台,每次找出最小值所在下标,输出后将这个最小值改为一个较大的数,以防止重复找到该位置亦可。

代码

void solve()
{
    int n, t[40];
    cin >> n;
    for(int i = 1; i <= n; i++)
        cin >> t[i];
    
    for(int i = 1; i <= 3; i++)
    {
        int p = 1; // 找最小值所在下标
        
        for(int j = 1; j <= n; j++)
            if(t[j] < t[p])
                p = j;
        
        cout << p << " ";
        t[p] = 201; // 将最小值改为极大值,防止重复选择
    }
}

C - Striped Horse

题意

\(N\) 个方格排成一行,编号从 \(1\)\(N\)

初始时,所有方格都是白色的。而将编号为 \(i\) 的方格涂成黑色需要花费 \(C_i\) 的成本。

你可以执行以下程序一次,以将某些方格涂成黑色:

  • 任意选择一个正整数 \(x\),然后将 \(1 \le i \le N\) 范围内的每一个满足 \((i + x) \bmod 2W \lt W\) 的方格 \(i\) 全部涂成黑色。

求最小总成本是多少。

思路

由于 \(W\) 固定,发现只要 \(i\) 被涂黑,那么 \(i+2W,i+4W,i+6W,\dots\) 这些位置的方格肯定也都会被涂黑,所以我们可以先把 \(i \bmod 2W\) 相同的所有方格 \(i\) 涂黑的代价加到一起计算。

\(S_p\) 表示将满足 \(i \bmod 2W = p\) 的所有方格 \(i\) 涂黑的总代价。

然后考虑涂黑操作。如果 \(x \equiv -1\ (\bmod 2W)\)(对应的可以是 \(x = 2W-1\)),那么涂黑操作便相当于是将\(W\) 个方格涂黑,然后接下来 \(W\) 个方格不涂,再接下来 \(W\) 个方格继续涂黑,……。以此每 \(2W\) 个方格作为一个周期进行涂黑。

而如果 \(x\) 发生改变,只是相当于在每个 \(2W\) 的周期内对要涂黑的方格进行循环移动,被涂黑的 \(W\) 个方格在周期内一定还是连续的(首尾相连)。

于是我们便可以尝试在 \(S\) 数组内枚举被涂黑的方格占据哪些位置,取区间和即可作为总成本:

  • 如果周期内被涂黑的方格起始位置 \(i \lt W\),那么被涂黑的所有方格下标便在区间 \([i, i+W-1]\) 范围内。
  • 如果周期内被涂黑的方格起始位置 \(i \ge W\),那么被涂黑的所有方格下标便在区间 \([i, 2W-1]\) 以及 \([0, (i+W-1)\bmod 2W]\) 范围内。
    • 这里涉及到在大小为 \(2W\) 的环上首尾位置的操作,也可以采用化环为链的技巧来免除复杂的判断。

最后的区间和部分可以借助前缀和思想进行快速实现。单组数据时间复杂度为 \(O(N+W)\)

代码

typedef long long ll;

ll s[600005];

void solve()
{
    int n, w;
    cin >> n >> w;
    
    for(int i = 0; i < 2 * w; i++) // 清空
        s[i] = 0;
    
    for(int i = 1; i <= n; i++)
    {
        int c;
        cin >> c;
        s[i % (2 * w)] += c; // 将 下标 % 2w 相同的位置涂黑的成本放到一起计算
    }
    
    for(int i = 0; i < w; i++)
        s[i + 2 * w] = s[i]; // 化环为链,复制 [0, w-1] 到 [2w, 3w-1] 位置
    
    for(int i = 1; i < 3 * w; i++) // 取前缀和
        s[i] += s[i - 1];
    
    ll ans = 1e18;
    for(int i = 0; i < 2 * w; i++)
    {
        // 假设被涂黑的格子起始下标 % 2w = i
        // 那么 下标 % 2w 在区间 [i, i+w-1] 范围内的格子都会被涂黑
        if(i == 0)
            ans = min(ans, s[i + w - 1]);
        else
            ans = min(ans, s[i + w - 1] - s[i - 1]);
    }
    cout << ans << "\n";
}

int main()
{
	int T;
    cin >> T;
    while(T--)
        solve();
    return 0;
}

D - Forbidden List 2

题意

有一个包含 \(N\) 个不同整数的列表,列表中第 \(i\) 个整数为 \(A_i\)

给你 \(Q\) 个问题,第 \(j\) 个问题如下:

  • \(\ge X_j\) 的所有整数中,找出不在列表内\(Y_j\)的整数。

思路

考虑二分答案。

假设答案为 \(M\),考虑检查 \(M\) 是否是 \(\ge X\) 的所有整数中不在列表内的第 \(Y\) 小整数。

于是我们首先就得先知道 \([X, M]\) 范围内所有多少个整数在列表内。可以对列表先排序,然后采用二分法在有序数组内区间范围内的数字个数,记作 \(t\)

  • \(\gt M\) 的第一个数下标 减去 \(\ge X\) 的第一个数下标

由于区间 \([X, M]\) 内共有 \(M-X+1\) 个整数,容斥可得区间 \([X, M]\)不在列表内的整数数量即 \(M-X+1 - t\) 个。

考虑不在列表内的整数数量与 \(Y\) 之间的关系:

  • 如果 \(M-X+1-t \lt Y\),说明区间内还没有出现第 \(Y\) 小的符合条件的值,则继续向右找。
  • 如果 \(M-X+1-t \gt Y\),说明区间内已经出现第 \(Y\) 小的符合条件的值,且第 \(Y+1\) 小也已经出现,于是可以向左找。
  • 如果 \(M-X+1-t = Y\),此时需要对 \(M\) 分类讨论。
    • 如果 \(M\) 是列表内的数字,则说明第 \(Y\) 小的符合条件的值在 \(M\) 的左侧,需要继续向左找;
    • 而如果 \(M\) 就是没有出现在列表内的数字,则 \(M\) 就是答案。

时间复杂度 \(O(Q\log^2N)\)。注意二分边界与二分过程的值和 int 类型极值的大小关系。

代码

int a[300005];

void solve()
{
    int n, q;
    cin >> n >> q;
    for(int i = 1; i <= n; i++)
        cin >> a[i];
    
    sort(a + 1, a + n + 1);
    
    while(q--)
    {
        int x, y;
        cin >> x >> y;

        int l = x, r = 2.1e9, ans;
        while(l <= r)
        {
            int mid = l + (r - l) / 2;
            
            // 计算 [x, mid] 范围内 出现在列表中的数字个数
            int R = upper_bound(a + 1, a + n + 1, mid) - a;
            int L = lower_bound(a + 1, a + n + 1, x) - a; // 移到二分外面去可以节省点时间
            int cnt = R - L;
            
            // 计算 [x, mid] 范围内 未出现在列表中的数字个数
            cnt = mid - x + 1 - cnt;
            
            if(cnt < y)
                l = mid + 1;
            else if(cnt > y)
                r = mid - 1;
            else
            {
                if(R - 1 >= 1 && a[R - 1] == mid) // mid 在列表中存在,说明实际答案在 mid 之前
                    r = mid - 1;
                else
                {
                    ans = mid;
                    break;
                }
            }
        }
        cout << ans << "\n";
    }
}

E - Cookies

题意

\(N\) 种饼干,每种都有 \(10^{100}\) 块。第 \(i\) 种饼干的美味度为 \(A_i\)

您可以从中选择 \(K\) 块饼干。

两种选择方案被认为是不同的,当且仅当存在至少一种饼干选择数量不一致(即同种类的饼干都当作是相同的)。

那么从 \(N\) 种饼干中选出 \(K\) 块饼干的方案数共有 \(\binom{N+K-1}{K}\) 种。

对于每一种方案,考虑选择的每块饼干的美味度总和

\(S_1, S_2, \dots\) 表示对所有选择方案的饼干美味度总和降序排序后所形成的序列(包括重复的),请输出 \(S\) 序列的前 \(X\) 项。

思路

先对所有饼干的美味度进行排序,则 \(A_N\) 表示美味度最大的饼干。然后我们可以先选择美味度总和最大的方案(即全选 \(A_N\)),再从这个最优方案开始,一点点妥协去找次优方案。

每次妥协,可以当作是将先前选择的 \(A_N\) 替换为 \(A_{1 \sim N-1}\) 中的一种饼干,然后重新计算该方案的美味度总和。

需要注意的是,如果有多块 \(A_N\) 饼干需要替换,我们肯定是有一个替换的先后顺序的。

  • 举个例子,我们可以先把一个 \(A_N\) 替换为 \(A_1\),再把一个 \(A_N\) 替换为 \(A_2\),这样总美味度会减少 \((A_N-A_1)+(A_N-A_2)\)
  • 但如果我们先把一个 \(A_N\) 替换为 \(A_2\),再把一个 \(A_N\) 替换为 \(A_1\),这时候对于饼干选择方案来说,两种操作得到的方案是相同的,这在最终答案中只能出现一次。
  • 因此如果将每次把饼干替换成的种类编号写成一个序列,这个序列的求解过程应当是一个组合问题而非排列问题,所以我们可以通过控制待替换的饼干编号的单调性,来将问题改为组合问题。
    • 例如,如果我们是以编号从大到小的顺序作为饼干替换的目标编号的话,那么就可以保证后一次替换的目标编号一定不能超过前一次替换的目标编号

于是,我们可以取三个整数 \((\text{val}, \text{pos}, \text{cnt})\) 作为每种方案的状态,其中 \(\text{val}, \text{pos}, \text{cnt}\) 分别表示“当前方案的总美味度”、“当前方案最后一次替换的目标饼干编号”以及“当前方案已经有多少块 \(A_N\) 被替换了”。然后借助大根堆来每次快速找出总美味度最大的方案。

每次取出当前最优方案并输出答案后,考虑再替换掉这个方案中的某块 \(A_N\) 饼干来得到多种非最优方案,然后将替换后的方案重新加回堆内排序即可。

由于每次替换最多生成 \(N\) 种新方案,因此堆内总方案数不会超过 \(N\cdot X\) 这一数量级,总时间复杂度为 \(O(NX\log{(NX)})\)

代码

struct node
{
    ll val;  // 当前方案的总美味度
    int pos; // 记录上一次替换饼干的种类编号,以保证取组合方案,避免重复
    int cnt; // 美味度最大的饼干已经被替换掉多少个了
    
    bool operator < (const node &nd) const
    {
        return val < nd.val; // 在优先队列中按 val 从大到小排序
    }
};

priority_queue<node> q;

int n, k, x;
ll a[55];

void solve()
{
    cin >> n >> k >> x;
    for(int i = 1; i <= n; i++)
        cin >> a[i];
    sort(a + 1, a + n + 1);
    
    q.push(node{a[n] * k, n - 1, 0}); // 假设一开始全选美味度最大的饼干
    
    for(int t = 1; t <= x; t++)
    {
        node nd = q.top();
        q.pop();
        
        cout << nd.val << "\n";
        
        if(nd.cnt == k) // 没有美味度最大的饼干可以替换了
            continue;
        
        for(int i = nd.pos; i >= 1; i--) // 从上一个替换的饼干编号开始继续替换
        {
            // 将一个美味度最大的饼干替换成 a[i]
            q.push(node{nd.val - a[n] + a[i], i, nd.cnt + 1});
        }
    }
}

F - Egoism

题意

在 AtCoder 牧场中,马会自己洗澡。有些马洗澡后会自己清理干净,而有些马则会留下一地的污渍。

牧场里有 \(N\) 匹马,编号分别为 \(1, 2, \dots, N\)。第 \(i\) 匹马的心情值\(A_i\)整洁度\(B_i\)(其中 \(B_i\) 仅为 \(1\)\(2\))。

在晚上,这 \(N\) 匹马会依次各洗一次澡。设第 \(j\) 个洗澡的马的编号为 \(p_j\)\(1 \leq j \leq N\)),那么这匹马(即马 \(p_j\))的满意度按如下方式计算:

  • 如果 \(j \geq 2\) ,则其满意度等于当前洗澡的这匹马 \(p_j\)心情值乘以前一匹洗澡的马 \(p_{j-1}\)整洁度
  • 若为 \(j = 1\) ,则其满意度等于它自己的心情值

接下来你会收到连续 \(Q\) 天的 \(Q\) 个查询,请按顺序处理。第 \(k\) 天的查询内容如下:

  • 将第 \(W_k\) 匹马的心情值修改为 \(X_k\),整洁度修改为 \(Y_k\)(其中 \(Y_k\)\(1\)\(2\))。然后,在你可以任意安排\(N\) 匹马的洗澡顺序的前提下,求出所有马的满意度总和的最大可能值。

思路

由于整洁度只有 \(1\)\(2\),我们可以将整洁度当作是计算满意度时要乘的系数。不论怎么安排洗澡的顺序,所有牛的一倍心情值肯定是会直接加到答案里的。

重点考虑整洁度为 \(2\) 的这些牛,因为这些牛会对下一头洗澡的牛的满意度产生 \(\times 2\) 的影响。

分类讨论整洁度为 \(2\) 的牛的数量 \(\text{cnt2}\)

  • 如果 \(\text{cnt2} = 0\),那么此时整道题的答案即所有牛的心情值总和
  • 如果 \(\text{cnt2} = N\),明显除了第一头洗澡的牛以外,后面洗澡的牛的心情值对答案的贡献系数均为 \(2\)。为了保证答案最大,我们肯定会把心情值最小的牛当作第一头洗澡的牛,免去它的贡献系数,以使得其它 \(N-1\) 头牛的心情值均可以 \(\times 2\)。那么此时整道题的答案即所有牛的心情值总和 $\times 2 - $心情值最低的牛的心情值,或者也可以记作所有牛的心情值总和 \(+\) 心情值前 \(N-1\) 大的牛的心情值总和
  • 如果 \(\text{cnt2} \in [1, N-1]\),那么我们需要进一步分类讨论这 \(\text{cnt2}\) 头整洁度为 \(2\) 的牛的心情值分布情况:
    • 如果这 \(\text{cnt2}\) 头整洁度为 \(2\) 的牛恰好也就是心情值最大的 \(\text{cnt2}\) 头牛,那么此时心情值前 \(\text{cnt2}\) 的这些牛当中一定会有一头牛对答案的贡献系数取不到 \(2\)。为了保证答案最大,我们可以选择把心情值第 \(\text{cnt2}\)的这头牛放在第一头洗澡,接下来让心情值前 \(\text{cnt2}-1\) 大的这些牛全部洗掉,然后在剩下的牛当中选一个心情值最大的牛(即心情值第 \(\text{cnt2}+1\) 大的这头牛)跟在后面接着洗澡。这样,对答案的贡献系数为 \(2\) 的牛就是心情值前 \(\text{cnt2}-1\) 大的牛以及心情值第 \(\text{cnt2}+1\) 大的牛。答案即所有牛的心情值总和 \(+\) 心情值前 \(\text{cnt2}-1\) 大的牛的心情值总和 \(+\) 心情值第 \(\text{cnt2} + 1\) 大的牛的心情值
    • 而如果这 \(\text{cnt2}\) 头整洁度为 \(2\) 的牛并非完全是心情值最大的 \(\text{cnt2}\) 头牛,那么我们便可以选择一头整洁度为 \(2\) 但心情值并不是前 \(\text{cnt2}\) 大的牛,让它作为第一头洗澡的牛,接下来,只要前一头牛的整洁度为 \(2\),就马上跟一头心情值前 \(\text{cnt2}\) 大的牛在它后面洗澡,一定可以做到让心情值前 \(\text{cnt2}\) 大的所有牛对答案的贡献系数均为 \(2\),此时答案即所有牛的心情值总和 \(+\) 心情值前 \(\text{cnt2}\) 大的牛的心情值总和

至于如何快速获得心情值前 \(x\) 大的所有牛的心情值总和,以及心情值前 \(x\) 大的所有牛中有多少头的整洁度为 \(2\)。我们可以直接取 \([1, 10^6]\) 作为我们的定义域,表示所有牛的心情值,开一棵线段树用于维护每个心情值区间所对应的“牛的总数”、“牛的心情值总和”以及“整洁度为 \(2\) 的牛的数量”。

接下来便可以根据每个区间中的“牛的总数”来在线段树上进行二分,来找到对应的前 \(x\) 大解。

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

代码

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

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

struct node
{
    int l, r; // 心情区间
    ll sum;   // 心情在 [l, r] 内的所有牛的 心情总和
    int cnt;   // 心情在 [l, r] 内的所有牛的 总数量
    int cnt2;  // 心情在 [l, r] 内的所有牛中 有多少头的整洁度为 2
};
node tr[1000005 << 2];

// 建树
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 >> 1;
    build(l, mid, ls);
    build(mid+1, r, rs);
}

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

// pos 表示牛的心情
// tid 表示牛的整洁度 (0/1,1表示原整洁度为 2)
// delta = -1/1 删或加
void update(int pos, int tid, int delta, int p = 1)
{
    if(tr[p].l == tr[p].r)
    {
        tr[p].sum += pos * delta;
        tr[p].cnt += 1 * delta;
        tr[p].cnt2 += tid * delta;
        return;
    }
    if(pos <= tr[ls].r) // 心情落在左子树区间内
        update(pos, tid, delta, ls);
    else                // 心情落在右子树区间内
        update(pos, tid, delta, rs);
    push_up(p);
}

// 线段树上二分,找:
// (first) 心情前 k 大的牛中最多有多少头整洁度为 2 的
// (second) 心情前 k 大的牛的心情总和
pll query(int k, int p = 1)
{
    if(k == 0)
        return pll(0, 0);
    
    if(tr[p].l == tr[p].r)
    {
        // 相同心情时如果有的整洁度为 2 有的不为 2,优先取整洁度为 2 的
        return pll(min(k, tr[p].cnt2), 1LL * k * tr[p].l);
    }
    
    // 分为左右子树考虑,先看心情值较高的右子树
    // 如果右子树内的牛的数量 >= k,那么问题的答案与左子树无关
    if(tr[rs].cnt >= k)
        return query(k, rs);
    
    // 如果右子树内的牛的数量 < k,那么右子树内的所有牛都会成为答案
    // 剩余的 心情前 k - tr[rs].cnt 大 的牛的答案需要继续在左子树中找
    pll t = query(k - tr[rs].cnt, ls);
    return pll(t.first + tr[rs].cnt2, t.second + tr[rs].sum);
}

int n, m, a[200005], b[200005];

void solve()
{
    build(1, 1000000);
    
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
    {
        cin >> a[i] >> b[i];
        // b[i]-1 表示是否是一只整洁度为 2 的牛(0 不是 1 是)
        update(a[i], b[i] - 1, 1);
    }
    
    while(m--)
    {
        int w, x, y;
        cin >> w >> x >> y;
        update(a[w], b[w] - 1, -1); // 移除原本的牛
        a[w] = x;
        b[w] = y;
        update(a[w], b[w] - 1, 1); // 添加新来的牛
        
        int cnt2 = tr[1].cnt2;
        if(cnt2 == 0)
        {
            // 没有任何一支整洁度为 2 的牛
            // 答案即所有牛的心情总和
            cout << tr[1].sum << "\n";
        }
        else if(cnt2 == n)
        {
            // 所有牛的整洁度均为 2
            // 让心情最小的牛最先洗澡,答案为 所有牛的心情总和 * 2 - 心情最小的牛
            // 也可以记作 所有牛的心情总和 + 心情前 n-1 大的牛的心情总和
            cout << tr[1].sum + query(n-1).second << "\n";
        }
        else
        {
            pll p = query(cnt2); // 取心情前 cnt2 大的牛
            if(p.first != cnt2)
            {
                // 如果整洁度为 2 的牛 不完全在 心情前 cnt2 大的牛中
                // 那么此时我们可以选择一头整洁度为 2 且心情不是前 cnt2 大的牛 让它最先洗澡
                // 然后只要前一头牛的整洁度为 2,就紧接着放一头心情 前 cnt2 大的牛
                // 最终每头心情前 cnt2 大的牛对答案的贡献系数均为 2
                // 答案即 所有牛的心情总和 + 心情前 cnt2 大的牛的心情总和
                cout << tr[1].sum + p.second << "\n";
            }
            else
            {
                // 如果整洁度为 2 的所有牛 全部都在 心情前 cnt2 大的牛中
                // 此时心情前 cnt2 大的牛里一定会有一头牛 对答案的贡献系数取不到 2
                // 为了让答案最大,我们可以把心情 第 cnt2 大 的牛放在最前面,让它对答案的贡献系数为 1
                // 然后等所有心情前 cnt2-1 大的牛全部洗完澡后,紧接着跟上心情 第 cnt2+1 大 的牛
                // 此时答案为 所有牛的心情总和 + 心情前 cnt2-1 大的牛的心情总和 + 心情第 cnt2+1 大的牛的心情
                pll p1 = query(cnt2 - 1);
                pll p2 = query(cnt2 + 1);
                cout << tr[1].sum + p1.second + (p2.second - p.second) << "\n";
            }
        }
    }
}
posted @ 2026-01-10 23:44  StelaYuri  阅读(70)  评论(0)    收藏  举报