算法竞赛里面的STL——堆和priority_queue

目录

1.堆

[2.优先级队列 (priority_queue的中文翻译)](#2.优先级队列 (priority_queue的中文翻译))

3.创建priority_queue-初阶

4.priority_queue常用的接口函数

5.priority_queue里面存的是结构体的情况

[题目一------第 k 小](#题目一——第 k 小)

题目二------除2!

[题目三------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篇文章看看

  1. 堆的介绍,堆的向下调整算法,堆的向上调整算法_堆调整-CSDN博客
  2. 堆的基本操作(c语言实现)_c语言堆的基本操作-CSDN博客
  3. 堆的应用1------堆排序-CSDN博客
  4. 堆的应用2------TOPK问题-CSDN博客

但是我还是要简单的介绍一下,什么是堆?

堆在计算机科学中有广泛的应用,尤其是在实现优先队列和堆排序算法中。优先队列是一种数据结构,其中元素的优先级决定了它们的出队顺序。堆可以作为一种高效的优先队列实现方式,因为堆顶元素总是优先级最高(最大或最小)的元素。堆排序算法则利用堆的性质,通过构建最大堆或最小堆,并反复取出堆顶元素来实现排序。

将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。

堆的性质:

  1. 堆中某个节点的值总是不大于或不小于其父节点的值;
  2. 堆总是一棵完全二叉树。

堆的分类

堆主要分为两种类型:最大堆(Max Heap)和最小堆(Min Heap)。

  1. 在最大堆中,父节点的值总是大于或等于其子节点的值,因此堆顶元素是整个堆中的最大值。
  2. 相反,在最小堆中,父节点的值总是小于或等于其子节点的值,堆顶元素是整个堆中的最小值。

我们先练习一下什么是大根堆?什么是小根堆?

2.优先级队列 (priority_queue的中文翻译)

普通的队列是⼀种先进先出的数据结构,即元素插⼊在队尾,⽽元素删除在队头。

⽽在优先级队列中,元素被赋予优先级,当插⼊元素时,同样是在队尾,但是会根据优先级进⾏位置调整,优先级越⾼,调整后的位置越靠近队头;

同样的,删除元素也是根据优先级进⾏,优先级最⾼ 的元素(队头)最先被删除。

其实可以认为,优先级队列就是堆实现的⼀个数据结构。priority_queue 就是C++提供的,已经实现好的优先级队列,底层实现就是⼀个堆结构。

在算法竞赛 中,如果是需要使⽤堆的题⽬,⼀般就直接⽤现成的priority_queue,很少⼿写⼀个堆,因为省事~

3.创建priority_queue-初阶

优先级队列的创建结果有很多种,因为需要根据实际需求,可能会创建出各种各样的堆。这些堆可能包括:

  1. 简单内置类型的大根堆或小根堆:比如存储int类型的大根堆或小根堆。
  2. 存储字符串的大根堆或小根堆。
  3. 存储自定义类型的大根堆或小根堆:比如堆里面的数据是一个结构体。

关于每一种创建结果,都需要有与之对应的写法。在初阶阶段,先用简单的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的模板参数有三个:

  1. 数据类型:这是堆中存储的元素类型。
  2. 存数据的结构 :这是底层容器类型,用于实际存储堆中的元素。priority_queue默认使用std::vector作为底层容器,但你也可以指定其他容器类型(只要它支持随机访问迭代器)。
  3. 数据之间的比较方式 :这是一个函数对象(通常是一个仿函数或函数指针),用于比较堆中的元素,以确定它们的优先级。默认情况下,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函数

  1. size:返回优先级队列中元素的个数。

  2. empty:检查优先级队列是否为空。

O(1)时间复杂度:sizeempty操作都可以在常数时间内完成。


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 大的元素。这个类有两个主要方法:

  1. 构造函数 KthLargest(int k, int[] nums):接受一个整数 k 和一个整数数组 nums 作为初始化参数。k 表示你想找到的是排序后的第 k 大的元素。
  2. 方法 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是函数的数量,Mx的可能取值范围(或者是一个预设的上限),这样的复杂度在题目给定的数据规模下很可能会超时。

尽管我们利用了堆这种数据结构来优化查找最小值的操作,但暴力计算所有可能结果的方法仍然不够高效。我们需要转变一下思路。

由题意可知,所有的二次函数在x>0的区间内都是递增的(因为A>0, BC为任意实数,但二次项系数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 舞蹈课 - 洛谷 | 计算机科学教育新生态

在处理寻找舞蹈技术差值最小的异性配对问题时,我们自然而然地想到了利用堆结构来优化求解。具体做法是,构建一个包含所有相邻异性配对的小根堆,堆中的元素基于技术差值进行排序。然而,在实际操作过程中,我们还会遇到一些挑战和细节问题,需要妥善处理:

  1. 相邻异性配对出列后的序列调整

    当一对相邻的异性从序列中移除后,他们原本的位置将由其他元素填补,但剩余元素的相对顺序应保持不变。为了高效实现这一操作,我们可以选择使用双向链表来存储所有数据。当需要删除一对相邻的异性时,我们只需调整相应节点的指针即可。此外,双向链表还能迅速定位到删除操作后新相邻的元素,便于我们判断它们是否为未出列的异性,并据此更新堆中的配对信息。

  2. 技术差值相等时的配对选择

    若存在多个技术差值相等的异性配对,我们应优先输出位置靠前的配对。为实现这一目标,我们可以在堆中存储一个结构体,该结构体包含配对双方的左编号、右编号以及技术差值。在重载比较运算符时,我们需进行特殊处理以遵循以下排序规则:

    • 当技术差值不相等时,按照技术差值构建小根堆;
    • 当技术差值相等但左编号不相等时,按照左编号构建小根堆;
    • 当技术差值与左编号均相等时,按照右编号构建小根堆。
  3. 处理堆中已出列的个体

    为了避免在堆中操作已出列的个体,我们可以创建一个布尔数组来标记每个人的出列状态。在从堆中取出异性配对之前,我们需先检查配对中的个体是否已被标记为出列。若已标记,则应跳过该配对并继续处理堆中的下一个配对。

综上所述,通过结合双向链表与小根堆的优势,并妥善处理上述细节问题,我们可以高效地解决寻找舞蹈技术差值最小的异性配对问题。

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)

说时候,谈到频率,我第一个想到的是哈希表。

事实上,我们只需按照下面这样子做即可。

  1. 处理原数组
    • 统计单词频次:首先,我们需要知道每个单词在原数组中出现的频次。这可以通过遍历原数组并使用哈希表来实现。
    • 选择前k大单词 :由于原数组中存在重复的单词,而哈希表中每个单词只会出现一次且附带其频次,因此我们选择从哈希表中选出前k大的单词。
  2. 使用堆选择前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(); // 由于左堆始终不大于右堆,所以这里直接返回左堆顶即可
    }
};
相关推荐
知识分享小能手2 分钟前
Java学习教程,从入门到精通,JDBC数据库连接语法知识点及案例代码(92)
java·大数据·开发语言·数据库·学习·java开发·java后端开发
Flocx4 分钟前
联合体(Union)
开发语言·网络·c++·stm32
迂幵myself12 分钟前
14-1C++STL的初始
开发语言·c++
冰茶_14 分钟前
C# 并发和并行的区别--16
开发语言·学习·c#
Allen Bright16 分钟前
【JVM-9】Java性能调优利器:jmap工具使用指南与应用案例
java·开发语言·jvm
羑悻的小杀马特35 分钟前
【狂热算法篇】探秘图论之 Floyd 算法:解锁最短路径的神秘密码(通俗易懂版)
c++·算法·图论·floyd算法
QuiteCoder39 分钟前
【c++】哈希
c++·哈希算法
冰茶_41 分钟前
C#中常见的锁以及用法--18
开发语言·学习·c#
ByteBlossom6661 小时前
Ruby语言的数据库交互
开发语言·后端·golang
淮淮淮淮淮1 小时前
代码随想录day10
java·开发语言