Serval and Kaitenzushi Buffet 题解:优先队列 + 数学推导
题目链接
题目分析
Serval 在一家回转寿司餐厅用餐,他需要在 $ n $ 分钟内最大化所吃寿司的美味值总和。每盘寿司有 $ k $ 块,第 $ i $ 盘寿司的美味值为 $ a_i $。在每一分钟,Serval 可以选择以下操作之一:
- 取走当前盘子,增加未吃完的寿司块数 $ r $(即 $ r += k $)。
- 吃掉一块寿司,减少未吃完的寿司块数 $ r $(即 $ r -= 1 $)。
- 什么都不做,保持 $ r $ 不变。
最终,$ r $ 必须为 0(即所有寿司都必须吃完)。我们需要帮助 Serval 最大化他取走的所有盘子的美味值总和。
听起来有点复杂?别急,我们慢慢来~
思路大意
这道题的核心目标是找到一种策略,使得 Serval 能够在 $ n $ 分钟内尽可能多地取走高美味值的盘子,并保证所有寿司都能被吃完。
核心观察
为了最大化美味值总和,我们需要尽可能多地取走高美味值的盘子,并保证在 $ n $ 分钟内可以吃完所有寿司。
数学推导
假设我们选择了 $ m $ 盘寿司,则总共需要吃 $ m \times k $ 块寿司。由于每分钟最多只能吃 1 块寿司,因此完成这些寿司需要至少 $ m \times k $ 分钟。同时,我们还有 $ n - m $ 分钟用于取盘子或什么都不做。
因此,满足条件的 $ m $ 必须满足以下不等式:
即:
这意味着我们可以最多取走 $ \left\lfloor \frac{n}{k+1} \right\rfloor $ 盘寿司。
接下来的问题是如何选择这 $ m $ 盘寿司以使美味值最大。显然,我们应该优先选择美味值最高的盘子。
算法设计
基于上述分析,我们可以设计如下算法:
- 使用一个优先队列(
priority_queue<int>
)来维护当前遇到的盘子的美味值。 - 遍历所有盘子,将每个盘子的美味值加入优先队列。
- 当遍历到某个时刻时,判断是否应该从优先队列中取出一个盘子计入答案。具体地,当剩余时间不足以处理更多的盘子时,我们就应该从优先队列中取出一个盘子。
代码详解
以下是完整的代码实现及其详细解释:
#include <bits/stdc++.h>
#define int long long
#define all(x) x.begin(),x.end()
#define rall(x) x.rbegin(),x.rend()
#define pb push_back
#define pii pair<int,int>
using namespace std;
const int mod = 998244353;void solve() {int n, k; cin >> n >> k; // 输入 n 和 kvector<int> a(n + 1); // 存储每盘寿司的美味值for (int i = 1; i <= n; i++) cin >> a[i]; // 输入美味值priority_queue<int> pq; // 优先队列,存储当前遇到的盘子的美味值int ans = 0; // 答案初始化为 0for (int i = 1; i <= n; i++) {pq.push(a[i]); // 将当前盘子的美味值加入优先队列// 判断是否需要从优先队列中取出一个盘子if ((n - i) % (k + 1) == k) {ans += pq.top(); // 取出当前美味值最大的盘子pq.pop(); // 从优先队列中移除该盘子}}cout << ans << endl; // 输出答案
}signed main() {ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr); // 加速输入输出int _ = 1; cin >> _; // 测试用例数量while (_--) solve(); // 处理每个测试用例
}
代码关键部分解析
priority_queue<int> pq;
- 作用:优先队列是一种数据结构,能够高效地维护一组元素,并支持快速插入和删除最大值的操作。
- 特性:在本题中,我们使用优先队列来存储当前遇到的所有盘子的美味值,并始终保持美味值最大的盘子在队列顶部。
- 时间复杂度:插入和删除操作的时间复杂度均为 $ O(\log m) $,其中 $ m $ 是队列中的元素数量。
关键逻辑
for (int i = 1; i <= n; i++) {pq.push(a[i]);if ((n - i) % (k + 1) == k) {ans += pq.top();pq.pop();}
}
逐行解释
-
pq.push(a[i]);
- 将当前盘子的美味值 $ a[i] $ 加入优先队列。
- 此时,优先队列会自动调整内部结构,确保队列顶部始终是当前美味值最大的盘子。
-
if ((n - i) % (k + 1) == k)
- 这是判断是否应该从优先队列中取出一个盘子的关键条件。
- 条件的核心思想是:在某些特定时刻,剩余时间不足以处理更多的盘子,因此我们需要从优先队列中取出一个盘子计入答案。
- 具体地,当剩余时间 $ (n - i) $ 满足 $ (n - i) \mod (k + 1) == k $ 时,表示我们已经无法再取新的盘子,而应该开始选择之前取过的盘子。
-
ans += pq.top();
和pq.pop();
- 当满足条件时,从优先队列中取出当前美味值最大的盘子,并将其加入答案。
- 同时,将该盘子从优先队列中移除。
数学公式的进一步解释
条件 \((n - i) \% (k + 1) == k\)
这个条件的含义可以通过以下推导理解:
- 设当前时间为 $ i $,则剩余时间为 $ n - i $。
- 如果我们要在剩余时间内处理完所有已取的盘子,则需要满足:\[\text{已取盘子数} \times k \leq \text{剩余时间} \]
- 换句话说,如果我们已经取了 $ m $ 盘寿司,则需要满足:\[m \times k \leq (n - i) \]
- 当 $ (n - i) % (k + 1) == k $ 时,表示剩余时间刚好足够处理一个新的盘子,因此我们需要从优先队列中取出一个盘子。
示例分析
输入
5
5 2
3 6 4 1 2
7 1
3 1 4 1 5 9 2
4 3
4 3 2 1
6 2
1 3 5 2 4 6
6 1
1000000000 1 1000000000 1 1000000000 1
输出
6
16
4
6
3000000000
解释
-
第一个测试用例:
- $ n = 5, k = 2 $
- 盘子美味值为 $ [3, 6, 4, 1, 2] $
- 最多可以取 $ \left\lfloor \frac{5}{2} \right\rfloor = 2 $ 盘寿司。
- 选择美味值为 $ 6 $ 的盘子,最终答案为 $ 6 $。
-
第二个测试用例:
- $ n = 7, k = 1 $
- 盘子美味值为 $ [3, 1, 4, 1, 5, 9, 2] $
- 最多可以取 $ \left\lfloor \frac{7}{1} \right\rfloor = 7 $ 盘寿司。
- 选择美味值为 $ 3, 4, 9 $ 的盘子,最终答案为 $ 16 $。