AtCoder Beginner Contest 423 ABCDEF 题目解析

A - Scary Fee

题意

你的存折中有 \(X\) 元,从存折中取钱需要花手续费。

取钱必须以 \(1000\) 元为单位,并且每取 \(1000\) 元就需要额外支付 \(C\) 元的手续费。

问你最多可以取出多少钱?

思路

我们可以把 \(C\) 元手续费当作单次取钱的一部分,也就是每当我们想取 \(1000\) 元钱,存折里就会减少 \(1000+C\) 元。

那我们最多可以取 \(\lfloor \dfrac {X} {1000+C} \rfloor\)\(1000\) 元钱,答案即 \(\lfloor \dfrac {X} {1000+C} \rfloor \times 1000\)

代码

#include<bits/stdc++.h>
using namespace std;

int main()
{
    int x, c;
    cin >> x >> c;
    cout << x / (1000 + c) * 1000;
    return 0;
}

B - Locked Rooms

题意

\(N + 1\) 个房间排成一行,依次编号为 \(0, 1, \ldots, N\)

房间之间有 \(N\) 扇门,编号为 \(1, 2, \ldots, N\),第 \(i\) 扇门位于房间 \(i - 1\)\(i\) 之间。

每扇门都有一个代表门锁状态的值 \(L_i\) 。当 \(L_i = 0\) 时,表示第 \(i\) 扇门未上锁;当 \(L_i = 1\) 时,表示第 \(i\) 扇门上锁。

有两个人,分别在编号为 \(0\)\(N\) 的房间。只有当第 \(i\) 扇门未上锁时,他们才可以在 \(i - 1\)\(i\) 这两个房间之间移动。

求两人都无法到达的房间的数量。

思路

计数数组统计总共有哪些房间可以到达,正着倒着 for 两遍,遇到一个上锁的门就结束循环即可。

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

代码

#include<bits/stdc++.h>
using namespace std;

bool vis[105];
int L[105];

int main()
{
    int n;
    cin >> n;
    for(int i = 1; i <= n; i++)
        cin >> L[i];
    
    vis[0] = true;
    for(int i = 1; i <= n; i++)
    {
        if(L[i] == 1) // 如果上锁则结束
            break;
        vis[i] = true; // 房间 i-1 可以走到房间 i
    }
    
    vis[n] = true;
    for(int i = n; i >= 1; i--)
    {
        if(L[i] == 1)
            break;
        vis[i - 1] = true; // 房间 i 可以走到房间 i-1
    }
    
    int cnt = 0;
    for(int i = 0; i <= n; i++)
        if(!vis[i])
            cnt++;
    cout << cnt;
    
    return 0;
}

C - Lock All Doors

题意

\(N + 1\) 个房间排成一行,依次编号为 \(0, 1, \ldots, N\)

房间之间有 \(N\) 扇门,编号为 \(1, 2, \ldots, N\),第 \(i\) 扇门位于房间 \(i - 1\)\(i\) 之间。

每扇门都有一个代表门锁状态的值 \(L_i\) 。当 \(L_i = 0\) 时,表示第 \(i\) 扇门未上锁;当 \(L_i = 1\) 时,表示第 \(i\) 扇门上锁。

高桥最开始在编号为 \(R\) 的房间里。只有当第 \(i\) 扇门打开时,他才可以在 \(i-1\)\(i\) 这两个房间之间移动。只有当他位于第 \(i-1\) 或第 \(i\) 个房间时,他才可以改变第 \(i\) 扇门的上锁状态(已上锁变成未上锁,未上锁变成已上锁)。

求使所有门都上锁所需的最少操作次数(改变上锁状态的次数)。

思路

我们先不要管一开始高桥在什么位置,先处理出一开始未上锁的门当中的最小及最大编号,分别记作 \(l\)\(r\)

为了锁上所有未上锁的门,那么 \(l, r\) 这两扇门之间的所有房间我们在过程中肯定是都要至少经过一次的,所以我们可以先把 \([l, r]\) 范围内的所有上锁的门先全部开锁,最后再一扇扇锁起来。

到此为止,答案可以描述为“\([l, r]\) 区间内上锁的门的数量 + \([l, r]\) 的区间长度”。

但我们还需要注意高桥一开始所在的位置。因为如果高桥的初始位置周围的门并不在区间 \([l, r]\) 范围内的话,那么高桥必须得先从他的位置出发,一扇扇打开门,直到碰到区间 \([l, r]\) 范围内的门为止。

这一步可以通过修改区间 \([l, r]\) 的方式实现。假设高桥一开始在 \(p\) 房间,那么左右两扇门的编号就是 \(p\)\(p+1\)。如果区间左端点在 \(p+1\) 的右侧,那么就扩展左端点到 \(p+1\);如果区间右端点在 \(p\) 的左侧,那么就扩展右端点到 \(p\)

综上,\(l = \min(l, p+1)\)\(r = \max(r, p)\) 后,再通过上面的方法求答案即可。

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

代码

#include<bits/stdc++.h>
using namespace std;

int L[200005];

int main()
{
    int n, p;
    cin >> n >> p;
    
    int l = n + 1, r = 0; // 未上锁的门的最小及最大编号
    int cnt = 0; // 未上锁的门的数量
    for(int i = 1; i <= n; i++)
    {
        cin >> L[i];
        if(L[i] == 0)
        {
            l = min(l, i);
            r = max(r, i);
            cnt++;
        }
    }
    
    if(r == 0) // 门都关着
    {
        cout << 0;
        return 0;
    }
    
    // 扩展到高桥所在房间周围
    l = min(l, p + 1);
    r = max(r, p);
    
    // 区间内上锁的数量 = 区间长度 - 区间内未上锁的数量
    // 区间长度 = r - l + 1
    cout << (r - l + 1) * 2 - cnt;
    
    return 0;
}

D - Long Waiting

题意

有一家餐厅最多可同时接待 \(K\) 位顾客。餐厅门前有个队列。

在时间 \(0\) 时,餐厅内没有顾客,队列也是空的。

\(N\) 组顾客预定前来就餐,他们按照到达的先后顺序被编号为从 \(1\)\(N\) 。第 \(i\) 组由 \(C_i\) 人组成,在时间 \(A_i\) 到达餐厅门前并进入队尾,并在进入餐厅 \(B_i\) 个时间单位之后离开餐厅。

每组人会在同时满足以下两个条件的最早时间时离开队列并进入餐厅:

  • 该组位于队列队首。
  • 该组进入餐厅后,餐厅内总人数不超过 \(K\)

求每组人进入餐厅的时间。

思路

因为进入餐厅的顺序就是按照编号顺序来的,所以我们可以按顺序处理每组人进入餐厅的时间。

因此只需要模拟餐厅内每一组离开的时间即可,这里可以借助优先队列进行维护。

优先队列内存储二元组 \((\texttt{time}, \texttt{id})\),分别表示目前在餐厅内就餐的每组人离开餐厅的时间及其编号,按照第一关键字(离开时间)从小到大排序。

当第 \(i\) 组的人想要进入餐厅时,如果餐厅内剩余位置数量不足 \(C_i\) 个,那我们就可以尝试去优先队列内查看离开时间最早的那一组人,记录其离开时间后,让他们离开餐厅,并把位置空出来。一直这样处理,直到空位至少有 \(C_i\) 个为止,那么此时第 \(i\) 组人进入餐厅的时间就是最后一组人离开餐厅的时间。

注意第 \(i\) 组人进入餐厅的时间不能早于 \(A_i\),所以处理出最后一组人离开餐厅的时间后,要和 \(A_i\) 取较大值。

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

代码

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<ll, int> pli;

int n, k;
int a[300005], b[300005], c[300005];

priority_queue<pli, vector<pli>, greater<pli>> pq;
// <离开时间, 该组编号>

int main()
{
    cin >> n >> k; // k 接下来表示餐厅内的空位数
    for(int i = 1; i <= n; i++)
        cin >> a[i] >> b[i] >> c[i];
    
    ll tim = 0; // 上一组人离开餐厅的时间
    for(int i = 1; i <= n; i++)
    {
        while(k < c[i]) // 只要餐厅内人数不足以支持第 i 组人进入
        {
            pli p = pq.top();
            pq.pop();
            
            int id = p.second; // 离开的这组人的编号
            k += c[id]; // 空位多出 c[id] 个
            
            tim = p.first; // id 这组离开的时间
        }
        
        // 让 i 这一组进入餐厅
        tim = max(tim, (ll)a[i]); // 如果还没来,直接跳过无人的这段时间
        cout << tim << "\n";
        
        k -= c[i]; // 空位减少 c[i] 个
        pq.push(pli(tim + b[i], i));
    }
    
    return 0;
}

E - Sum of Subarrays

题意

给定一个长度为 \(N\) 的整数序列 \(A = (A_1, A_2, \ldots, A_N)\)

\(Q\) 个询问,第 \(i\) 个询问给定整数 \(L_i, R_i\) ,请输出 \(\displaystyle\sum_{l = L_i}^{R_i}\sum_{r = l}^{R_i}\sum_{j = l}^{r} A_j\)

思路

公式可以简单理解为:对于区间 \([L_i, R_i]\) 的所有子区间 \([l, r]\) \((L_i \le l \le r \le R_i)\),输出每个子区间内所有数字的和的总和。

对于区间内的每个整数 \(A_j\),所有包含位置 \(j\) 的区间的左端点一定在 \([L_i, j]\),右端点一定在 \([j, R_i]\) 内,因此区间数量共有 \((j-L_i+1) \times (R_i - j + 1)\) 种。换言之,答案可以描述为:

\[\begin{aligned} &\sum_{j=L_i}^{R_i} [(j-L_i+1) \cdot (R_i - j + 1) \cdot A_j] \\ =& \sum_{j=L_i}^{R_i} [(jR_i - j^2 + j - L_iR_i + jL_i - L_i + R_i - j + 1) \cdot A_j] \\ =& \sum_{j=L_i}^{R_i} [(j(L_i + R_i) - j^2 - L_iR_i + (R_i - L_i + 1)) \cdot A_j] \\ =& [(L_i + R_i) \cdot \sum_{j=L_i}^{R_i} (j \cdot A_j)] - \sum_{j=L_i}^{R_i} (j^2 \cdot A_j) + [(R_i-L_i+1-L_iR_i) \cdot \sum_{j=L_i}^{R_i} A_j] \end{aligned} \]

\(X = \sum\limits_{j=L_i}^{R_i} A_j\)\(Y = \sum\limits_{j=L_i}^{R_i} (j\cdot A_j)\)\(Z = \sum\limits_{j=L_i}^{R_i} (j^2\cdot A_j)\),则上式可简化为:

\[(L_i + R_i) \cdot Y - Z + (R_i-L_i+1-L_iR_i) \cdot X \]

这里的 \(X, Y, Z\) 可以借助前缀和思想预处理后快速求得。

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

代码

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;

int n, q, a[300005];
ll s1[300005], s2[300005], s3[300005];

int main()
{
    cin >> n >> q;
    for(int i = 1; i <= n; i++)
    {
        cin >> a[i];
        s1[i] = s1[i-1] + a[i];
        s2[i] = s2[i-1] + 1LL * a[i] * i;
        s3[i] = s3[i-1] + 1LL * a[i] * i * i;
    }
    
    while(q--)
    {
        int l, r;
        cin >> l >> r;
        ll x = s1[r] - s1[l-1];
        ll y = s2[r] - s2[l-1];
        ll z = s3[r] - s3[l-1];
        cout << (l + r) * y - z + (r - l + 1 - 1LL * l * r) * x << "\n";
    }
    
    return 0;
}

F - Loud Cicada

题意

\(N\) 种蝉,第 \(i\) 种蝉只有在当前年份为 \(A_i\) 的倍数时才会大规模爆发。

问从第 \(1\) 年到第 \(Y\) 年的这 \(Y\) 年中,共有多少年正好\(M\) 种蝉大规模爆发。

思路

首先可以明确,如果第 \(i\) 种蝉和第 \(j\) 种蝉同时爆发,那么其爆发周期可以通过 \(\text{lcm}(A_i, A_j)\) 获得,其中 \(\text{lcm}\) 表示最小公倍数。

回到本题,发现 \(N\) 的范围很小,可以考虑 \(O(2^N)\) 先枚举出所有可能的同时爆发蝉的方案。

对于每种爆发方案,先计算出所有蝉同时爆发的周期,记作 \(D\),那么 \(Y\) 年内爆发的次数可以通过 \(\lfloor\dfrac Y D\rfloor\) 得到,将该次数暂时记作 \(T\)

  • 如果同时爆发的蝉的种类数不足 \(M\) 种,直接跳过即可。
  • 如果同时爆发的蝉的种类数恰好 \(M\) 种,则当前得到的 \(T\) 年直接计入答案。但很明显这种统计方案可能会出现某些年重复被计算的情况。
  • 如果同时爆发的蝉的种类数超过 \(M\) 种,记种类数为 \(K\),也就是说在上面第二步中共有 \(\text{C}_K^M\) 种方案重复计算了当前的这 \(T\) 年,需要根据容斥定理将 \(\text{C}_K^M \times T\) 处理进答案内。
    • \((K-M)\) 为奇数时,此时 \(T\) 需要从答案内减去;
    • \((K-M)\) 为偶数时,此时 \(T\) 需要重新加进答案。

由于处理过程(例如多个数的最小公倍数)可能很大,容易超出 \(64\) 位整型范围,建议使用 \(128\) 位整型变量辅助进行计算。

时间复杂度 \(O(N\cdot 2^N)\)

代码

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef __int128 LL;

int n, m;
ll y, a[25];

LL C[25][25];
// C[i][j] 表示 i 个数中取出 j 个数的组合方案
LL sum[25];
// sum[i] 暂存有 i 种蝉爆发的年份总数,有重复

LL gcd(LL a, LL b)
{
    return b == 0 ? a : gcd(b, a%b);
}
LL lcm(LL a, LL b)
{
    return a / gcd(a, b) * b;
}

int main()
{
    cin >> n >> m >> y;
    for(int i = 0; i < n; i++)
        cin >> a[i];
    
    // 预处理组合数
    for(int i = 0; i <= n; i++)
    {
        C[i][0] = C[i][i] = 1;
        for(int j = 1; j < i; j++)
            C[i][j] = C[i-1][j-1] + C[i-1][j];
    }
    
    LL ans = 0;
    // 枚举每一种可能方案
    for(int sta = 0; sta < (1 << n); sta++)
    {
        int k = __builtin_popcount(sta);
        if(k < m) // 同时爆发的蝉的数量不足 k 种
            continue;
        
        LL d = 1; // 这种方案的爆发周期
        for(int i = 0; i < n; i++)
            if(sta >> i & 1)
            {
                d = lcm(d, (LL)a[i]);
                if(d > y)
                    break;
            }
        
        // y 年内总共会出现 y/d 次这种爆发方案
        // 容斥处理最终答案
        if((k - m) % 2 == 0)
            ans += (y / d) * C[k][m];
        else
            ans -= (y / d) * C[k][m];
    }
    
    cout << (ll)ans;
    
    return 0;
}
posted @ 2025-09-14 22:32  StelaYuri  阅读(352)  评论(3)    收藏  举报