蓝桥杯 魔法蘑菇

魔法蘑菇

原题目链接

题目描述

本题源自 Codeforces:Kirill and Mushrooms

现有 n 个蘑菇,第 i 个蘑菇的能量为 v_i

你需要从中选择一些蘑菇制作灵药。若选择了若干个蘑菇,则灵药的价值定义为:灵药价值 = 选择的蘑菇数量 × 所选蘑菇中的最小能量

现在给你一个长度为 n 的排列 p。当你选择 k 个蘑菇时,下标为 p_1, p_2, ..., p_{k-1} 的蘑菇能量都会变成 0

也就是说,只有下标不在 p_1 ~ p_{k-1} 中的蘑菇,才仍然保持原本的能量,能够被你正常选择。

你需要求出:

  1. 能得到的最大灵药价值;
  2. 在取得最大灵药价值的前提下,所选择蘑菇数量最少是多少。

如果有多个方案得到相同的最大灵药价值,输出其中选择蘑菇数量最少的那个。

排列的定义

对于一个长度为 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 时:

  1. 把新加入可选集合的那个蘑菇放入堆中;
  2. 如果堆的大小超过 k,就不断弹出堆顶;
  3. 最终堆里剩下的就是当前可选集合中最大的 k 个数;
  4. 堆顶就是这 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 个数",堆顶就是答案所需的最小值。

相关推荐
z20348315202 小时前
17届蓝桥杯嵌入式赛道开发板外设使用教程——按键、蜂鸣器、LCD屏幕
mongodb·职场和发展·蓝桥杯
逆境不可逃2 小时前
LeetCode 热题 100 之 763.划分字母区间
算法·leetcode·职场和发展
wuqingshun3141592 小时前
蓝桥杯 无影之谜
算法·职场和发展·蓝桥杯
逆境不可逃3 小时前
【从零入门23种设计模式17】行为型之中介者模式
java·leetcode·microsoft·设计模式·职场和发展·中介者模式
Eward-an3 小时前
LeetCode 1009. 十进制整数的反码(详细技术解析)
算法·leetcode·职场和发展
筱昕~呀3 小时前
冲刺蓝桥杯-BFS板块(第八天)
职场和发展·蓝桥杯·宽度优先
仰泳的熊猫3 小时前
题目2086:蓝桥杯算法提高VIP-最长公共子序列
数据结构·c++·算法·蓝桥杯·动态规划
2301_8008951012 小时前
BFS--备战蓝桥杯版h
算法·蓝桥杯·宽度优先
郝学胜-神的一滴12 小时前
力扣86题分隔链表:双链表拆解合并法详解
开发语言·数据结构·算法·leetcode·链表·职场和发展