目录
[2.优先级队列 (priority_queue的中文翻译)](#2.优先级队列 (priority_queue的中文翻译))
[题目一------第 k 小](#题目一——第 k 小)
[题目三------1046. 最后一块石头的重量 - 力扣(LeetCode)](#题目三——1046. 最后一块石头的重量 - 力扣(LeetCode))
[题目四------703. 数据流中的第 K 大元素 - 力扣(LeetCode)](#题目四——703. 数据流中的第 K 大元素 - 力扣(LeetCode))
[题目五------P2085 最小函数值 - 洛谷 | 计算机科学教育新生态](#题目五——P2085 最小函数值 - 洛谷 | 计算机科学教育新生态)
[题目六------P1631 序列合并 - 洛谷 | 计算机科学教育新生态](#题目六——P1631 序列合并 - 洛谷 | 计算机科学教育新生态)
[题目七------P1878 舞蹈课 - 洛谷 | 计算机科学教育新生态](#题目七——P1878 舞蹈课 - 洛谷 | 计算机科学教育新生态)
[题目八------692. 前K个高频单词 - 力扣(LeetCode)](#题目八——692. 前K个高频单词 - 力扣(LeetCode))
[题目九------295. 数据流的中位数 - 力扣(LeetCode)](#题目九——295. 数据流的中位数 - 力扣(LeetCode))
1.堆
首先我们得知道堆是啥?
如果想要详细了解这个堆的话可以去下面这4篇文章看看
- 堆的介绍,堆的向下调整算法,堆的向上调整算法_堆调整-CSDN博客
- 堆的基本操作(c语言实现)_c语言堆的基本操作-CSDN博客
- 堆的应用1------堆排序-CSDN博客
- 堆的应用2------TOPK问题-CSDN博客
但是我还是要简单的介绍一下,什么是堆?
堆在计算机科学中有广泛的应用,尤其是在实现优先队列和堆排序算法中。优先队列是一种数据结构,其中元素的优先级决定了它们的出队顺序。堆可以作为一种高效的优先队列实现方式,因为堆顶元素总是优先级最高(最大或最小)的元素。堆排序算法则利用堆的性质,通过构建最大堆或最小堆,并反复取出堆顶元素来实现排序。
将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
堆的性质:
- 堆中某个节点的值总是不大于或不小于其父节点的值;
- 堆总是一棵完全二叉树。
堆的分类
堆主要分为两种类型:最大堆(Max Heap)和最小堆(Min Heap)。
- 在最大堆中,父节点的值总是大于或等于其子节点的值,因此堆顶元素是整个堆中的最大值。
- 相反,在最小堆中,父节点的值总是小于或等于其子节点的值,堆顶元素是整个堆中的最小值。
我们先练习一下什么是大根堆?什么是小根堆?
2.优先级队列 (priority_queue的中文翻译)
普通的队列是⼀种先进先出的数据结构,即元素插⼊在队尾,⽽元素删除在队头。
⽽在优先级队列中,元素被赋予优先级,当插⼊元素时,同样是在队尾,但是会根据优先级进⾏位置调整,优先级越⾼,调整后的位置越靠近队头;
同样的,删除元素也是根据优先级进⾏,优先级最⾼ 的元素(队头)最先被删除。
其实可以认为,优先级队列就是堆实现的⼀个数据结构。priority_queue 就是C++提供的,已经实现好的优先级队列,底层实现就是⼀个堆结构。
在算法竞赛 中,如果是需要使⽤堆的题⽬,⼀般就直接⽤现成的priority_queue,很少⼿写⼀个堆,因为省事~
3.创建priority_queue-初阶
优先级队列的创建结果有很多种,因为需要根据实际需求,可能会创建出各种各样的堆。这些堆可能包括:
- 简单内置类型的大根堆或小根堆:比如存储int类型的大根堆或小根堆。
- 存储字符串的大根堆或小根堆。
- 存储自定义类型的大根堆或小根堆:比如堆里面的数据是一个结构体。
关于每一种创建结果,都需要有与之对应的写法。在初阶阶段,先用简单的int类型建堆,重点学习priority_queue的用法。
注意:priority_queue包含在<queue>这个头文件中。
看看最简单的int类型的大根堆
cpp
#include <queue>
#include <vector>
#include <iostream>
using namespace std;
int main() {
// 创建一个空的priority_queue,默认是大根堆,基础容器类型是vector
priority_queue<int> pq;
// 插入元素
pq.push(1);
pq.push(4);
pq.push(2);
pq.push(8);
// 输出并移除顶部元素(优先级最高的元素)
while (!pq.empty()) {
cout << pq.top() << " "; // 输出8 4 2 1
pq.pop();
}
return 0;
}
看看另外两种初始化类型
在C++中,priority_queue
是一个模板类,它通常用于实现堆数据结构。堆是一种特殊的完全二叉树,它满足堆性质:对于大根堆(max-heap),每个节点的值都大于或等于其子节点的值;对于小根堆(min-heap),每个节点的值都小于或等于其子节点的值。
priority_queue
的模板参数有三个:
- 数据类型:这是堆中存储的元素类型。
- 存数据的结构 :这是底层容器类型,用于实际存储堆中的元素。
priority_queue
默认使用std::vector
作为底层容器,但你也可以指定其他容器类型(只要它支持随机访问迭代器)。 - 数据之间的比较方式 :这是一个函数对象(通常是一个仿函数或函数指针),用于比较堆中的元素,以确定它们的优先级。默认情况下,priority_queue使用std::less作为比较方式,这创建了一个大根堆。如果你想要一个小根堆,可以使用std::greater。
现在,让我们具体讲解你给出的代码:
创建大根堆
cpp
priority_queue<int, vector<int>, less<int>> heap2; // 也是大根堆
- 数据类型 :
int
。这意味着堆中存储的元素是整数。 - 存数据的结构 :
vector<int>
。这指定了底层容器是一个整数向量。这是可选的,因为priority_queue
默认使用std::vector
。 - 数据之间的比较方式 :
less<int>
。这是一个函数对象,它比较两个整数并返回true
如果第一个整数小于第二个整数(在实际上,less<int>
的实现会返回false
,因为我们是用它来构建大根堆的,但这里的逻辑是反向的------即,如果a < b
则返回false
意味着a
的优先级不低于b
,因此a
应该留在堆顶或更高的位置)。然而,在这个特定的例子中,使用less<int>
实际上是多余的,因为priority_queue
默认就使用std::less
来创建大根堆。
因此,heap2
是一个大根堆,它存储整数,使用向量作为底层容器,并使用默认的(或显式指定的)std::less<int>
比较函数来确定元素的优先级。在这个堆中,优先级最高的元素(即值最大的元素)总是在堆顶。
创建小根堆
如果你想要创建一个小根堆,你可以这样做:
cpp
priority_queue<int, vector<int>, greater<int>> heap3; // 小根堆
在这个例子中,greater<int>
是一个函数对象,它比较两个整数并返回true
如果第一个整数大于第二个整数(实际上,对于小根堆来说,如果a > b
则返回false
意味着a
的优先级不高于b
,因此b
应该留在堆顶或更高的位置)。这样,heap3
就是一个小根堆,其中值最小的元素总是在堆顶。
4.priority_queue常用的接口函数
size 和 empty函数
-
size:返回优先级队列中元素的个数。
-
empty:检查优先级队列是否为空。
O(1)时间复杂度:size
和empty
操作都可以在常数时间内完成。
push函数
- 往优先级队列里面添加一个元素。
时间复杂度:由于底层是一个堆结构,所以添加元素的时间复杂度为O(log n),其中n是优先级队列中的元素数量。
pop函数
- 删除优先级最高的元素。
时间复杂度:由于底层是一个堆结构,所以删除优先级最高的元素的时间复杂度为O(log n),其中n是优先级队列中的元素数量。
top函数
- 获取优先级最高的元素,但不删除它。
时间复杂度:O(1)。获取优先级最高的元素可以在常数时间内完成,因为这个元素总是位于堆的顶端(对于大根堆)或底端(对于小根堆,取决于具体实现)。
接下来我们好好举一个例子来说明
cpp
#include <iostream>
#include <queue>
#include <vector>
int main() {
// 创建一个空的优先级队列(大根堆)
std::priority_queue<int> pq;
// 检查队列是否为空
if (pq.empty()) {
std::cout << "Priority queue is initially empty." << std::endl;
}
// 向队列中添加元素
pq.push(10);
pq.push(20);
pq.push(15);
// 获取队列的大小
std::cout << "Size of priority queue after pushes: " << pq.size() << std::endl;
// 获取并输出优先级最高的元素(但不删除)
int topElement = pq.top();
std::cout << "Top element (highest priority): " << topElement << std::endl; // 输出20
// 删除优先级最高的元素
pq.pop();
// 再次获取队列的大小
std::cout << "Size of priority queue after pop: " << pq.size() << std::endl;
// 检查队列是否为空
if (pq.empty()) {
std::cout << "Priority queue is now empty." << std::endl;
}
else {
std::cout << "Priority queue is not empty." << std::endl;
}
// 输出并删除剩余的元素
while (!pq.empty()) {
std::cout << "Removing element: " << pq.top() << std::endl;
pq.pop();
}
// 最终检查队列是否为空
if (pq.empty()) {
std::cout << "Priority queue is finally empty." << std::endl;
}
return 0;
}
在这个例子中,我们首先创建了一个空的优先级队列pq
。然后,我们检查队列是否为空(它当然是空的),并向队列中添加了三个整数(10, 20, 15)。由于我们使用的是大根堆,所以20是优先级最高的元素。
接下来,我们使用size
函数获取队列的大小,并使用top
函数获取并输出了优先级最高的元素(20)。然后,我们使用pop
函数删除了这个元素,并再次使用size
函数检查了队列的大小。
在删除元素之后,我们再次检查队列是否为空(它当然不是空的),并使用一个while
循环来输出并删除队列中的剩余元素。最后,我们再次检查队列是否为空,并输出相应的消息。
5.priority_queue里面存的是结构体的情况
当优先级队列⾥⾯存的是结构体类型时,需要在结构体中重载<⽐较运算符,从⽽创建出⼤根堆或者 ⼩根堆。
注:这⾥的主要⽬的是把优先级队列创建出来,因此只⽤掌握写法即可。关于其中背后的原理,这⾥ 就不再赘述,因为涉及⽐较复杂的C++语法知识。在这⾥,学习的侧重点就是模仿。
小根堆(使用>)
cpp
#include <iostream> // 包含标准输入输出流库
#include <vector> // 虽然在这个程序中未直接使用vector,但priority_queue内部实现可能会用到
#include <queue> // 包含优先级队列的头文件
using namespace std; // 使用标准命名空间,避免每次调用标准库时都要加std::前缀
// 定义一个结构体node,用于存储三个整型数据a, b, c
struct node
{
int a, b, c;
// 重载<运算符,用于定义node对象在优先级队列中的排序规则
// 这里以b为基准,定义小根堆的排序规则:b值较小的node对象会被视为"较小"的元素
// 但由于std::priority_queue默认是大根堆,所以我们通过让<运算符返回b > x.b来实现小根堆的效果
bool operator<(const node& x) const
{
return b > x.b; // 当当前对象的b值大于另一个对象的b值时,认为当前对象"小于"另一个对象(逻辑上的反转)
}
};
// 定义一个测试函数,用于演示优先级队列的使用
void test()
{
// 创建一个优先级队列heap,队列中的元素类型为node
// 由于我们重载了node的<运算符,所以heap会按照我们定义的规则(b值小为"小")来排序元素
priority_queue<node> heap;
// 向heap中插入10个node对象,每个对象的a值为i,b值为i+1,c值为i+2
for (int i = 1; i <= 10; i++)
{
heap.push({ i, i + 1, i + 2 }); // 使用列表初始化语法创建node对象并插入队列
}
// 当heap不为空时,循环弹出堆顶元素并打印
while (heap.size() != 0) // 使用heap.size() != 0来判断队列是否为空,但更标准的做法是使用!heap.empty()
{
// 使用C++17的结构化绑定来解构heap.top()返回的node对象,得到a, b, c三个值
auto [a, b, c] = heap.top();
heap.pop(); // 弹出堆顶元素
cout << a << " " << b << " " << c << endl; // 打印解构后的a, b, c值
}
}
int main()
{
test(); // 调用测试函数
return 0; // 程序正常结束,返回0
}
大根堆------使用<
cpp
#include <iostream> // 包含标准输入输出流库,用于输入输出操作
#include <vector> // 虽然在这个程序中未直接使用vector,但priority_queue内部实现可能会用到它作为底层容器
#include <queue> // 包含优先级队列的头文件,提供priority_queue类模板
using namespace std; // 使用标准命名空间,简化代码中的标准库对象和函数的引用
// 定义一个结构体node,用于存储三个整型数据a, b, c
struct node
{
int a, b, c; // 定义三个整型成员变量a, b, c
//注意这里大根堆对应<号
bool operator<(const node& x) const
{
return b < x.b; // 当当前对象的b值小于另一个对象的b值时,认为当前对象"小于"另一个对象(逻辑上的反转,但实现大根堆效果)
}
};
// 定义一个测试函数,用于演示优先级队列的使用
void test()
{
// 创建一个优先级队列heap,队列中的元素类型为node
// 由于我们重载了node的<运算符,heap会按照b值从大到小的顺序来排序元素(实际上实现了大根堆效果)
priority_queue<node> heap;
// 向heap中插入10个node对象,每个对象的a值为i,b值为i+1,c值为i+2
for (int i = 1; i <= 10; i++)
{
heap.push({ i, i + 1, i + 2 }); // 使用列表初始化语法创建node对象并插入优先级队列
}
// 当heap不为空时,循环弹出堆顶元素并打印
// 注意:虽然使用heap.size() != 0可以判断队列是否为空,但更标准的做法是使用!heap.empty()
while (heap.size() != 0)
{
// 使用C++17的结构化绑定来解构heap.top()返回的node对象,得到a, b, c三个值
// heap.top()返回堆顶的node对象的引用
auto [a, b, c] = heap.top();
heap.pop(); // 弹出堆顶元素,堆顶元素被移除,下一个最大元素(按b值)成为新的堆顶
cout << a << " " << b << " " << c << endl; // 打印解构后的a, b, c值
}
}
int main()
{
test(); // 调用测试函数,演示优先级队列的使用和自定义排序规则的效果
return 0; // 程序正常结束,返回0表示成功执行
}
接下来来好好学习一下优先级队列的使用吧!!!
题目一------第 k 小
这题直接使用我们的priority_queue即可
其实这题非常简单。它要第k小,如果说我建一个小根堆,那我是不是还要搞一个循环去找第k个小的啊,这样子有点麻烦啊。那我为什么不建一个大根堆,只要我保证这个堆的大小是k,这样子堆顶的元素就会一直是第k小的。
cpp
#include<bits/stdc++.h>
using namespace std;
priority_queue<int>heap;//默认是大根堆
int main()
{
int n,m,k;
cin>>n>>m>>k;
int tmp;
for(int i=n;i>0;i--)
{
cin>>tmp;
heap.push(tmp);
if(heap.size() > k) heap.pop();
}
for(int i=m;i>0;i--)
{
cin>>tmp;
if(tmp==1)
{
int tmp1;
cin>>tmp1;
heap.push(tmp1);
if(heap.size() > k) heap.pop();
}
else if(tmp==2)
{
if(heap.size() == k) cout << heap.top() << endl;
else cout << -1 << endl;
}
}
}
题目二------除2!
其实很简单啊,就是我们找到数组中最大的那几个偶数,然后将它们分别/2即可。
那我们就搞一个大根堆就好了呗!!
cpp
#include<bits/stdc++.h>
using namespace std;
priority_queue<int>heap;
int main()
{
int n,k;
cin>>n>>k;
long long sum=0;
for(int i=n;i>0;i--)
{
int x;
cin>>x;
sum+=x;
if(x%2==0)
{
heap.push(x);
}
}
for(int i=k;i>0&&!heap.empty();i--)
{
int t=heap.top()/2;
sum-=t;
heap.pop();
k--;
if(t % 2 == 0) heap.push(t);
}
cout<<sum<<endl;
return 0;
}
题目三------1046. 最后一块石头的重量 - 力扣(LeetCode)
将所有石头的重量放入最大堆中。每次依次从队列中取出最重的两块石头 a 和 b,必有 a≥b。如果 a>b,则将新石头 a−b 放回到最大堆中;如果 a=b,两块石头完全被粉碎,因此不会产生新的石头。重复上述操作,直到剩下的石头少于 2 块。
最终可能剩下 1 块石头,该石头的重量即为最大堆中剩下的元素,返回该元素;也可能没有石头剩下,此时最大堆为空,返回 0。
cpp
class Solution {
public:
int lastStoneWeight(vector<int>& stones) {
priority_queue<int> heap;
for (int i = 0; i < stones.size(); i++) {
heap.push(stones[i]);
}
while (heap.size() > 1) {
int a = heap.top();
heap.pop();
int b = heap.top();
heap.pop();
if (a != b)
heap.push(abs(b - a));
}
return heap.size()==0?0:heap.top();
}
};
题目四------703. 数据流中的第 K 大元素 - 力扣(LeetCode)
说时候,这题目看的我有点懵。
你需要设计一个类 KthLargest
,它能够处理一个数据流,并在数据流中动态地找到第 k
大的元素。这个类有两个主要方法:
- 构造函数
KthLargest(int k, int[] nums)
:接受一个整数k
和一个整数数组nums
作为初始化参数。k
表示你想找到的是排序后的第k
大的元素。 - 方法
int add(int val)
:向数据流中添加一个新的整数val
,并返回当前数据流中第k
大的元素。
示例
假设我们有一个 KthLargest
对象 kthLargest
:
- 如果我们创建对象时传入
k = 3
和初始数组[4, 5, 8, 2]
,那么在添加一些值后,结果应该是这样的:kthLargest.add(3)
返回4
(当前数据流中第3大的元素是4)kthLargest.add(5)
返回5
(当前数据流中第3大的元素变为5)kthLargest.add(10)
返回5
(10加入后,第3大还是5)kthLargest.add(9)
返回8
(9加入后,第3大变为8)kthLargest.add(4)
返回8
(4加入后,第3大还是8)
所谓第k大,我们完全可以创建一个小根堆,然后保持堆的大小是k,然后这样子就能保证heap.top()就是第k大的元素了。
cpp
class KthLargest {
public:
priority_queue<int,vector<int>,greater<int>>heap;
int k;
KthLargest(int _k, vector<int>& nums) {
k=_k;
for(int i=0;i<nums.size();i++)
{
heap.push(nums[i]);
if(heap.size()>k)
{
heap.pop();
}
}
}
int add(int val) {
heap.push(val);
if(heap.size()>k)
{
heap.pop();
}
return heap.top();
}
};
题目五------P2085 最小函数值 - 洛谷 | 计算机科学教育新生态
最初的想法可能是**直接将所有函数在不同x
值下的结果全部计算出来,并将这些结果放入一个小根堆(最小堆)中。然后,从这个堆中依次取出最小的m
个元素作为答案。**然而,这种方法的时间复杂度为O(N×M)
,其中N
是函数的数量,M
是x
的可能取值范围(或者是一个预设的上限),这样的复杂度在题目给定的数据规模下很可能会超时。
尽管我们利用了堆这种数据结构来优化查找最小值的操作,但暴力计算所有可能结果的方法仍然不够高效。我们需要转变一下思路。
由题意可知,所有的二次函数在x>0
的区间内都是递增的(因为A>0
, B
和C
为任意实数,但二次项系数A
决定函数的开口方向,且题目已隐含A>0
,使得函数在x>0
时递增) 。这意味着,如果我们已经计算了某个函数在x处的值,并且这个值不是当前堆顶的最小值,那么该函数在x+1, x+2, ... 等更大x值处的值也必定不是堆顶的最小值。
因此,我们大可不必一次性计算出所有可能的结果再放入堆中。相反,我们可以采用一种更动态的方法:**初始时,将每个函数在x=1
处的值计算出来,并将这些值以及对应的函数信息和x
值一起放入堆中。然后,每次从堆中取出堆顶元素(即当前最小的函数值)后,我们就计算该函数在x+1
处的值,并将这个新值以及对应的函数信息和新的x
值放入堆中。**由于所有函数都是递增的,这种策略是正确的,并且能确保我们始终在跟踪所有函数产生的最小可能值。
为了实现这一点,堆中需要存储一个结构体,该结构体包含三个信息:**函数值、产生该值的函数标识以及用于计算该值的x
值。**然后,我们可以根据函数值来创建一个小根堆,以便高效地找到和更新最小值。
cpp
#include <iostream>
#include <queue>
using namespace std;
typedef long long LL;
const int N = 1e4 + 10; // 定义常量N,表示函数的最大数量
int n, m; // n表示函数数量,m表示要求的最小函数值数量
LL a[N], b[N], c[N]; // 存储每个二次函数的系数a, b, c
struct node {
LL f; // 函数值
LL num; // 函数编号(即函数在数组中的索引)
LL x; // 代入值(即x的值)
// 重载小于运算符,用于定义小根堆的排序规则
bool operator <(const node& x) const {
return f > x.f; // 小根堆,大元素下坠(即堆顶始终是最小值)
}
};
priority_queue<node> heap; // 定义一个小根堆,用于存储当前最小的函数值及其相关信息
LL calc(LL i, LL x) {
// 计算并返回第i个函数在x处的值
return a[i] * x * x + b[i] * x + c[i];
}
int main() {
cin >> n >> m; // 输入函数数量n和要求的最小函数值数量m
for(int i = 1; i <= n; i++) {
cin >> a[i] >> b[i] >> c[i]; // 输入每个函数的系数a, b, c
}
// 1. 把x = 1的值放入堆中
for(int i = 1; i <= n; i++) {
heap.push({calc(i, 1), i, 1}); // 计算每个函数在x=1处的值,并将其与函数编号和x值一起放入堆中
}
// 2. 依次拿出m个值
while(m--) { // 进行m次迭代,每次从堆中取出最小的函数值
auto t = heap.top(); heap.pop(); // 取出堆顶元素(即当前最小的函数值及其相关信息)
LL f = t.f, num = t.num, x = t.x; // 解构堆顶元素,获取函数值、函数编号和x值
cout << f << " "; // 输出当前最小的函数值
// 把下一个函数值放入堆中
// 注意:这里需要检查是否继续放入新计算的值是有必要的,
// 但在本题中,由于函数是递增的,我们总是可以放入新的值而不必担心它不是最小的。
// 然而,在更一般的情况下,可能需要一个额外的机制来避免放入无效的值。
heap.push({calc(num, x + 1), num, x + 1}); // 计算当前函数在x+1处的值,并将其与函数编号和新的x值一起放入堆中
}
return 0; // 程序结束
}
题目六------P1631 序列合并 - 洛谷 | 计算机科学教育新生态
其实这题和上面那题是一模一样的。
cpp
#include <iostream>
#include <queue>
using namespace std;
const int N = 1e5 + 10; // 定义常量N,表示数组a和b的最大长度
int n; // 定义变量n,表示数组a和b的实际长度
int a[N], b[N]; // 定义数组a和b,用于存储输入的数据
struct node {
int sum; // 当前的和,即a[i] + b[j]的值
int i, j; // a和b的索引(编号)
// 重载小于运算符,用于定义小根堆的排序规则
bool operator < (const node& x) const {
return sum > x.sum; // 小根堆,大元素下坠(即堆顶始终是最小值)
}
};
priority_queue<node> heap; // 定义一个小根堆,用于存储当前最小的和及其相关信息
int main() {
cin >> n; // 输入数组a和b的长度
for(int i = 1; i <= n; i++) cin >> a[i]; // 输入数组a的元素
for(int i = 1; i <= n; i++) cin >> b[i]; // 输入数组b的元素
// 1. 将a[i] + b[1]的值放入堆中
// 这里我们首先将a数组中的每个元素与b数组的第一个元素相加,并将结果及其索引信息放入堆中
for(int i = 1; i <= n; i++) {
heap.push({a[i] + b[1], i, 1});
}
// 2. 依次拿出n个数(实际上是n次从堆中取出最小元素并可能放入新元素的过程)
// 每次从堆中取出当前最小的和(即a[i] + b[j]的最小值),并输出它
// 然后,我们尝试将下一个可能的和(即a[i] + b[j+1])放入堆中,以继续寻找更小的和
for(int k = 1; k <= n; k++) {
node t = heap.top(); heap.pop(); // 取出堆顶元素
int sum = t.sum, i = t.i, j = t.j; // 解构堆顶元素,获取和、a的索引和b的索引
cout << sum << " "; // 输出当前最小的和
// 如果b数组的下一个元素还在范围内,我们将其与a数组的当前元素相加,并将结果放入堆中
if(j + 1 <= n) {
heap.push({a[i] + b[j + 1], i, j + 1});
}
}
cout << endl; // 输出换行符,以美化输出格式
return 0; // 程序正常结束
}
题目七------P1878 舞蹈课 - 洛谷 | 计算机科学教育新生态
在处理寻找舞蹈技术差值最小的异性配对问题时,我们自然而然地想到了利用堆结构来优化求解。具体做法是,构建一个包含所有相邻异性配对的小根堆,堆中的元素基于技术差值进行排序。然而,在实际操作过程中,我们还会遇到一些挑战和细节问题,需要妥善处理:
-
相邻异性配对出列后的序列调整 :
当一对相邻的异性从序列中移除后,他们原本的位置将由其他元素填补,但剩余元素的相对顺序应保持不变。为了高效实现这一操作,我们可以选择使用双向链表来存储所有数据。当需要删除一对相邻的异性时,我们只需调整相应节点的指针即可。此外,双向链表还能迅速定位到删除操作后新相邻的元素,便于我们判断它们是否为未出列的异性,并据此更新堆中的配对信息。
-
技术差值相等时的配对选择 :
若存在多个技术差值相等的异性配对,我们应优先输出位置靠前的配对。为实现这一目标,我们可以在堆中存储一个结构体,该结构体包含配对双方的左编号、右编号以及技术差值。在重载比较运算符时,我们需进行特殊处理以遵循以下排序规则:
- 当技术差值不相等时,按照技术差值构建小根堆;
- 当技术差值相等但左编号不相等时,按照左编号构建小根堆;
- 当技术差值与左编号均相等时,按照右编号构建小根堆。
-
处理堆中已出列的个体 :
为了避免在堆中操作已出列的个体,我们可以创建一个布尔数组来标记每个人的出列状态。在从堆中取出异性配对之前,我们需先检查配对中的个体是否已被标记为出列。若已标记,则应跳过该配对并继续处理堆中的下一个配对。
综上所述,通过结合双向链表与小根堆的优势,并妥善处理上述细节问题,我们可以高效地解决寻找舞蹈技术差值最小的异性配对问题。
cpp
#include <iostream>
#include <vector>
#include <queue>
#include <cmath>
using namespace std;
const int N = 2e5 + 10; // 定义常量N,表示可能的最大人数(稍微大一些以确保足够)
int n; // 人数
int s[N]; // 标记性别,0表示男性,1表示女性
// 双向链表存数据
int e[N]; // 存储每个人的技术值
int pre[N], ne[N]; // pre[i]表示i的前一个元素编号,ne[i]表示i的后一个元素编号
struct node {
int d; // 技术差
int l, r; // 左右编号
// 重载小于运算符,用于构建小根堆
bool operator<(const node& x) const {
if (d != x.d) return d > x.d; // 如果技术差不相等,按技术差升序排列(小根堆)
else if (l != x.l) return l > x.l; // 如果技术差相等但左编号不相等,按左编号升序排列
else return r > x.r; // 如果技术差和左编号都相等,按右编号升序排列
}
};
priority_queue<node> heap; // 存储所有异性配对的小根堆
bool st[N]; // 标记已经出队的人
int main() {
cin >> n; // 输入人数
for (int i = 1; i <= n; i++) {
char ch; cin >> ch; // 输入性别
if (ch == 'B') s[i] = 1; // 'B'表示女性,标记为1
// 'M'表示男性,默认为0,因此不需要显式赋值
}
for (int i = 1; i <= n; i++) {
cin >> e[i]; // 输入每个人的技术值
// 创建双向链表
pre[i] = i - 1; // i的前一个元素是i-1
ne[i] = i + 1; // i的后一个元素是i+1
}
pre[1] = ne[n] = 0; // 链表的头尾指针设为0,表示没有相邻元素
// 1. 先把所有的异性差放进堆中
for (int i = 2; i <= n; i++) {
if (s[i] != s[i - 1]) { // 如果当前人和前一个人是异性
heap.push({abs(e[i] - e[i - 1]), i - 1, i}); // 将他们的技术差和编号加入堆中
}
}
// 2. 提取结果
vector<node> ret; // 暂存结果
while (heap.size()) {
node t = heap.top(); heap.pop(); // 从堆中取出技术差最小的配对
int d = t.d, l = t.l, r = t.r; // 提取配对的信息
if (st[l] || st[r]) continue; // 如果配对中的人已经出队,则跳过
ret.push_back(t); // 将配对加入结果集
st[l] = st[r] = true; // 标记配对中的人为已出队
// 修改指针,还原新的队列
ne[pre[l]] = ne[r]; // 将l的前一个元素的下一个元素指向r的下一个元素
pre[ne[r]] = pre[l]; // 将r的下一个元素的前一个元素指向l的前一个元素
// 判断新的左右是否会成为一对
int left = pre[l], right = ne[r]; // 获取l和r的新相邻元素
if (left && right && s[left] != s[right]) { // 如果新相邻元素存在且为异性
heap.push({abs(e[left] - e[right]), left, right}); // 将他们的技术差和编号加入堆中
}
}
cout << ret.size() << endl; // 输出配对数量
for (auto& x : ret) {
cout << x.l << " " << x.r << endl; // 输出每对配对的编号
}
return 0;
}
题目八------692. 前K个高频单词 - 力扣(LeetCode)
说时候,谈到频率,我第一个想到的是哈希表。
事实上,我们只需按照下面这样子做即可。
- 处理原数组 :
- 统计单词频次:首先,我们需要知道每个单词在原数组中出现的频次。这可以通过遍历原数组并使用哈希表来实现。
- 选择前k大单词 :由于原数组中存在重复的单词,而哈希表中每个单词只会出现一次且附带其频次,因此我们选择从哈希表中选出前k大的单词。
- 使用堆选择前k大元素 :
- 定义自定义排序 :由于我们需要的是前k大,所以自然想到使用小根堆。但是,当两个单词的频次相同时,我们需要的是字典序较小的单词,这类似于大根堆的属性。因此,在定义比较器时,我们需要同时考虑频次和字典序。
- 当两个单词的频次不同时,基于频次进行小根堆的比较。
- 当两个单词的频次相同时,基于字典序进行大根堆的比较。
- 插入堆:定义好比较器后,我们依次将哈希表中的单词插入堆中,同时保持堆中的元素不超过k个。如果堆的大小超过k,则弹出堆顶元素(即当前堆中最小的元素,但考虑到频次相同时的字典序,它可能是频次较小但字典序较大的元素)。
- 遍历哈希表:遍历完整个哈希表后,堆中剩余的元素就是前k大的元素。
- 定义自定义排序 :由于我们需要的是前k大,所以自然想到使用小根堆。但是,当两个单词的频次相同时,我们需要的是字典序较小的单词,这类似于大根堆的属性。因此,在定义比较器时,我们需要同时考虑频次和字典序。
cpp
class Solution {
public:
// 定义一个pair类型,用于存储单词和它的频次
typedef pair<string, int> PAIR;
// 定义一个比较结构,用于最小堆的排序逻辑
struct cmp {
bool operator()(const PAIR& a, const PAIR& b) const {
// 当两个单词的频次相同时,按照字典序逆序排列
// 这意味着在频次相同时,字典序较大的单词会被认为是较大的元素(模拟大根堆效果)
if (a.second == b.second) {
return a.first < b.first; // 注意这里使用了小于,因为我们希望字典序大的排在前面
}
// 当两个单词的频次不同时,按照频次升序排列
// 因为我们使用的是最小堆,所以频次较小的会被认为是较大的元素(这里看起来有些反直觉)
// 但是由于我们只保留前k个元素,因此频次较大的单词最终会被保留下来(当它们存在时)
return a.second > b.second; // 频次大的在这里被认为是"较大",但会被放在堆顶之下,因为我们用的是最小堆
}
};
// 主函数,返回频次最高的k个单词
vector<string> topKFrequent(vector<string>& words, int k) {
// 使用哈希表统计每个单词的频次
unordered_map<string, int> hash;
for (const auto& s : words) {
hash[s]++;
}
// 使用最小堆来维护前k个频次最高的单词
// 注意,由于我们的比较函数在频次相同时按字典序逆序排列,
// 以及在频次不同时通过反向比较实现"大根堆"效果在小堆中的模拟,
// 这使得堆顶始终是当前k个元素中"最小"(按我们定义的逻辑)的一个
priority_queue<PAIR, vector<PAIR>, cmp> heap;
// 遍历哈希表,将每个单词-频次对推入堆中
// 如果堆的大小超过了k,就弹出堆顶元素,保持堆的大小为k
for (const auto& psi : hash) {
heap.push(psi);
if (heap.size() > k) {
heap.pop();
}
}
// 提取堆中的单词到结果向量中
// 由于我们是从堆顶开始逐个弹出,而堆顶是最"小"的元素(按我们定义的逻辑),
// 所以我们需要从后往前填充结果向量,以保持正确的顺序
vector<string> ret(k);
for(int i = k - 1; i >= 0; i--) {
ret[i] = heap.top().first;
heap.pop();
}
return ret;
}
};
题目九------295. 数据流的中位数 - 力扣(LeetCode)
其实这个思路很简单
cpp
class MedianFinder {
// 大根堆,存储较小的一半元素,堆顶为最大值
priority_queue<int> left;
// 小根堆,存储较大的一半元素,堆顶为最小值
priority_queue<int, vector<int>, greater<int>> right;
public:
// 构造函数,初始化 MedianFinder 对象
MedianFinder() {}
// 向数据流中添加一个数字
void addNum(int num) {
// 当左右两个堆的元素个数相同时
if(left.size() == right.size()) {
// 如果左堆为空,或者新数字小于等于左堆顶(即左堆最大值)
if(left.empty() || num <= left.top()) {
// 将数字加入左堆(大根堆)
left.push(num);
} else {
// 否则,将数字加入右堆(小根堆),并将左堆顶(最大值)移到右堆以保持平衡
right.push(num);
left.push(right.top());
right.pop();
}
} else {
// 当左右两个堆的元素个数不同时(左堆元素多于右堆)
if(num <= left.top()) {
// 如果新数字小于等于左堆顶(即左堆最大值),则加入左堆
// 并将左堆顶(最大值)移到右堆以保持平衡
left.push(num);
right.push(left.top());
left.pop();
} else {
// 否则,将数字加入右堆(小根堆)
right.push(num);
}
}
// 注意:这里的逻辑保证了在任何时候,左堆的所有元素都小于等于右堆的所有元素
// 且两个堆的大小差不超过1,从而能够方便地找到中位数
}
// 返回数据流的中位数
double findMedian() {
// 如果左堆和右堆的元素个数相同,则中位数为两个堆顶元素的平均值
if(left.size() == right.size()) return (left.top() + right.top()) / 2.0;
// 否则,中位数为元素较多的那个堆的堆顶元素(因为此时两堆大小差为1)
else return left.top(); // 由于左堆始终不大于右堆,所以这里直接返回左堆顶即可
}
};