魔法蘑菇
题目描述
本题源自 Codeforces:Kirill and Mushrooms。
现有 n 个蘑菇,第 i 个蘑菇的能量为 v_i。
你需要从中选择一些蘑菇制作灵药。若选择了若干个蘑菇,则灵药的价值定义为:灵药价值 = 选择的蘑菇数量 × 所选蘑菇中的最小能量。
现在给你一个长度为 n 的排列 p。当你选择 k 个蘑菇时,下标为 p_1, p_2, ..., p_{k-1} 的蘑菇能量都会变成 0。
也就是说,只有下标不在 p_1 ~ p_{k-1} 中的蘑菇,才仍然保持原本的能量,能够被你正常选择。
你需要求出:
- 能得到的最大灵药价值;
- 在取得最大灵药价值的前提下,所选择蘑菇数量最少是多少。
如果有多个方案得到相同的最大灵药价值,输出其中选择蘑菇数量最少的那个。
排列的定义
对于一个长度为 n 的序列,如果数字 1 ~ n 恰好各出现一次,则称该序列是一个排列。
输入格式
第一行输入一个正整数 t,表示测试用例组数,满足 1 <= t <= 10^4。
对于每组测试数据:
- 第一行输入一个正整数
n,表示蘑菇的数量,满足1 <= n <= 2 × 10^5。 - 第二行输入
n个正整数v_1, v_2, ..., v_n,表示每个蘑菇的能量,满足1 <= v_i <= 10^9。 - 第三行输入
n个正整数p_1, p_2, ..., p_n,表示一个排列p。
保证所有测试数据中,n 的总和不超过 2 × 10^5。
输出格式
对于每组测试数据,输出一行两个整数:
- 最大灵药价值;
- 在达到最大灵药价值时,选择的最少蘑菇数量。
样例输入
in
6
3
9 8 14
3 2 1
5
1 2 3 4 5
1 2 3 4 5
6
1 2 3 4 5 6
6 5 4 3 2 1
5
1 4 6 10 10
2 1 4 5 3
4
2 2 5 5
4 2 3 1
5
1 2 9 10 10
1 4 2 3 5
样例输出
out
16 2
9 3
8 2
20 2
5 1
20 2
题解
题意概括
有 n 个蘑菇,第 i 个蘑菇的能量为 v_i。
如果你选择 k 个蘑菇,那么排列 p 中前 k-1 个位置对应的蘑菇能量会变成 0,也就是说这些蘑菇不能再作为有效蘑菇参与选择。
此时你需要从剩余还能用的蘑菇中选出 k 个,使得灵药价值最大。灵药价值定义为 价值 = k × min(所选蘑菇能量)。
要求输出最大价值,以及在价值最大的前提下,所选蘑菇数量最少是多少。
思路分析
一、先明确"选 k 个"时哪些蘑菇可用
当选择 k 个蘑菇时,排列中前 k-1 个蘑菇失效,即 p_1, p_2, ..., p_{k-1} 这些位置上的蘑菇能量变为 0。
因此,真正还能用的蘑菇就是 p_k, p_{k+1}, ..., p_n,一共有 n - (k - 1) = n - k + 1 个。
而我们需要从这些可用蘑菇中选出 k 个,使得最小值尽量大。
显然,最优策略一定是:在当前所有可用蘑菇中,选出能量最大的 k 个。
这样这 k 个蘑菇中的最小值才会尽可能大。
所以问题就转化成:
对于每个
k,在集合{v_{p_k}, v_{p_{k+1}}, ..., v_{p_n}}中,求前k大元素中的最小值。
二、为什么不能正着做
如果顺着题目,从拿 1 个蘑菇开始思考,会遇到一个问题:
当你从拿 k 个变成拿 k+1 个时:
- 一方面你想多加入一个蘑菇;
- 但另一方面,排列中又会多一个蘑菇失效,变得不可选。
问题在于:这个新失效的蘑菇,有可能正在你维护的最优候选集合里。如果它在小根堆里,你就需要把它从堆中删除。
而普通优先队列只支持删除堆顶,不支持删除任意元素,所以这个方向很难维护。
三、反过来思考:从大到小枚举 k
既然正着做不好维护,那就倒过来。
我们从"能拿最多蘑菇"的情况开始思考,然后逐渐减少选择数量。
设当前枚举的是 k,可选蘑菇集合是 {v_{p_k}, v_{p_{k+1}}, ..., v_{p_n}}。
如果从 k 变成 k-1,那么可选集合会多出来一个新蘑菇,也就是 v_{p_{k-1}}。
也就是说:
- 当
k变小的时候; - 可选集合只会新增元素,不会删除元素。
这就非常适合用堆来维护。
四、用小根堆维护"当前最大的 k 个数"
对于当前的 k,我们需要知道当前可选集合中"最大的 k 个能量值"里的最小值。
这正是一个经典做法:
- 用一个小根堆 维护当前最大的
k个数; - 堆顶就是这
k个数中的最小值; - 于是当前价值就是
k × 堆顶。
维护方式
当我们从大到小枚举 k 时:
- 把新加入可选集合的那个蘑菇放入堆中;
- 如果堆的大小超过
k,就不断弹出堆顶; - 最终堆里剩下的就是当前可选集合中最大的
k个数; - 堆顶就是这
k个数中的最小值。
正确性说明
我们证明该做法正确。
对于某个固定的 k:
- 可选蘑菇集合为
{v_{p_k}, v_{p_{k+1}}, ..., v_{p_n}}; - 我们必须从中选出
k个蘑菇; - 灵药价值取决于所选
k个蘑菇中的最小值。
要让最小值尽可能大,显然应该选出该集合中最大的 k 个数 。因为任意不是前 k 大的数替换进来,都只会让最小值不增反降。
而小根堆始终维护当前集合中最大的 k 个数,因此:
- 堆顶等于这
k个数中的最小值; - 当前计算出的
k × q.top()就是固定k时的最优价值。
枚举所有 k 后取最大值,即得到全局最优解。
若价值相同,题目要求选择蘑菇数量最少的方案。代码中由于是从大到小枚举 k,并且使用 >= 更新答案,后出现的更小 k 会覆盖前面的更大 k,因此最终留下的正是价值相同时最小的 k。
复杂度分析
对于每组数据:
- 每个元素至多入堆一次、出堆一次;
- 堆操作复杂度为
O(log n)。
因此总复杂度为 O(n log n)。
由于所有测试数据的 n 总和不超过 2 × 10^5,所以可以通过。
空间复杂度为 O(n)。
代码细节说明
1. 为什么从 (n + 1) / 2 开始枚举
当选择 k 个蘑菇时,可用蘑菇数量为 n - k + 1。
必须满足 n - k + 1 >= k,也就是 n + 1 >= 2k。
所以有 k <= floor((n + 1) / 2)。
因此,合法的 k 最大就是 (n + 1) / 2。
2. 为什么 while (q.size() != num_of_mushrooms) q.pop();
因为每次我们希望堆中只保留"当前可选集合中最大的 k 个数"。
如果堆元素超过了 k,就弹出最小的,这样留下来的就是最大的 k 个。
3. 为什么用 >= 更新答案
题目要求:若最大价值相同,输出选择蘑菇数量最少的那个。
代码是从大 k 枚举到小 k。因此如果当前价值和最优值相同,应该让更小的 k 覆盖之前的答案,所以要写成:
cpp
if (num_of_mushrooms * q.top() >= ans_energy)
而不是 >。
代码
cpp
#include<bits/stdc++.h>
using namespace std;
struct mycmp {
bool operator()(const long long& a, const long long& b) { return a > b; }
};
int t, n;
long long v[200009], p[200009];
int main() {
std::ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
cin >> t;
while (t--) {
priority_queue<long long, vector<long long>, mycmp> q;
cin >> n;
long long ans_mushrooms = 0, ans_energy = 0, num_of_mushrooms = (n + 1) / 2;
for (int i = 0; i < n; i++) cin >> v[i];
for (int i = 0; i < n; i++) cin >> p[i];
for (int i = n - 1; i >= num_of_mushrooms; i--) q.push(v[p[i] - 1]);
while (num_of_mushrooms >= 1) {
q.push(v[p[num_of_mushrooms - 1] - 1]);
while (q.size() != num_of_mushrooms) q.pop();
if (num_of_mushrooms * q.top() >= ans_energy) {
ans_energy = num_of_mushrooms * q.top();
ans_mushrooms = num_of_mushrooms;
}
num_of_mushrooms--;
}
cout << ans_energy << " " << ans_mushrooms << endl;
}
return 0;
}//by wqs
一句话总结
这题的关键在于:
正着枚举会遇到"堆中删除任意元素"的难题,难以维护;倒着枚举时,可选集合只会不断加入新元素,于是可以用小根堆维护"当前最大的
k个数",堆顶就是答案所需的最小值。