定义:
优先队列(Priority Queue):是一种抽象数据类型,它类似于常规队列或栈,但每个元素都有与其相关的优先级。在优先队列中,元素的出队顺序是根据它们的优先级来确定的,而不是按照它们进入队列的顺序。具有最高优先级的元素最先出队,而具有最低优先级的元素最后出队。
优先队列的基础框架通常包括以下几个主要操作:
插入元素(Insert):
将一个新元素添加到优先队列中。这个操作的时间复杂度通常取决于实现方式,但在许多情况下,它可以在对数时间(O(log n))内完成。
删除最高优先级元素(Delete Max):
从优先队列中移除并返回具有最高优先级的元素。这个操作同样可以在对数时间内完成。
查看最高优先级元素(Peek Max):
返回具有最高优先级的元素,但不从队列中移除它。这个操作通常比对数时间更快,因为它不需要移动或删除任何元素。
修改元素优先级(Change Priority):
更改队列中某个元素的优先级。这个操作可能需要重新调整队列以保持其有序性。
合并优先队列(Merge):
将两个优先队列合并成一个新的优先队列。这个操作的时间复杂度取决于实现方式,但在许多情况下,它可以在线性时间内完成。
实现方式多元:
优先队列的实现方式有多种,包括基于数组、链表、堆(Heap)等数据结构。
其中,基于堆的实现方式是最常见和高效的。堆是一种特殊的完全二叉树,它满足堆属性:对于最大堆,父节点的值总是大于或等于其子节点的值;对于最小堆,父节点的值总是小于或等于其子节点的值。通过使用堆来实现优先队列,可以在对数时间内完成插入、删除和查找最高优先级元素的操作。这种属性使得堆的根节点总是具有最大(或最小)的优先级。
以下是基于堆的优先队列实现的关键步骤:
- 初始化堆:
- 创建一个空数组或指定大小的数组来存储堆元素。
- 设置一个变量来跟踪堆的大小(即当前存储的元素数量)。
- 插入元素:
- 将新元素添加到数组的末尾。
- 对新元素进行上浮调整(上滤),通过与其父节点比较并交换位置(如果需要),直到满足堆属性。
- 删除最高优先级元素:
- 移除根节点(即具有最高优先级的元素)。
- 将数组的最后一个元素移动到根节点的位置。
- 对根节点进行下沉调整(下滤),通过与其子节点比较并交换位置(如果需要),直到满足堆属性。
- 查看最高优先级元素:
- 直接返回根节点的值,不需要修改堆。
- 修改元素优先级:
- 找到要修改的元素在堆中的位置。
- 修改该位置的值。
- 可能需要进行上浮或下沉调整,以确保堆属性仍然满足。
- 合并优先队列:
- 合并两个堆通常比较复杂,但可以通过创建一个新的堆,并将两个要合并的堆的元素依次插入新堆来实现。
- 也可以使用特定的合并算法(如斐波那契堆)来更高效地合并堆。
在基于堆的优先队列实现中,上浮和下沉调整是关键操作,它们确保了堆属性在插入和删除元素后仍然得到维护。这些调整操作的时间复杂度通常为 O(log n),其中 n 是堆中元素的数量。因此,基于堆的优先队列可以在对数时间内完成插入和删除最高优先级元素的操作,非常高效。
实现代码:
//在C++的容器库中,已经实现好了优先队列这种结构。
//头文件<queue>
//定义优先队列priority_queue<int>que
//这个表示存储整数类型,数值越大优先级越高(大根堆)
优先队列que的操作:
que.push(x) //将元素X插入优先队列
que.pop() //将优先级最高的元素出队
que.top() //返回优先级最高的元素
que.empty() //判断优先队列是否为空
que.size() //返回队列中元素数量
题目练习:
题目描述
给定一个 n 个点,m 条有向边的带非负权图,请你计算从 s 出发,到每个点的距离。
数据保证你能从 s 出发到任意点。
输入格式
第一行为三个正整数 n,m,s。 第二行起 m 行,每行三个非负整数 ui,vi,wi,表示从 ui 到 vi 有一条权值为 wi 的有向边。
输出格式
输出一行 n 个空格分隔的非负整数,表示 s 到每个点的距离。
输入输出样例
输入 #1复制
4 6 1 1 2 2 2 3 2 2 4 1 1 3 5 3 4 3 1 4 4
输出 #1复制
0 2 4 3
说明/提示
样例解释请参考 数据随机的模板题。
1≤n≤105;
1≤m≤2×105;
s=1;
1≤ui,vi≤n;
0≤wi≤109,
0≤∑wi≤109。
本题数据可能会持续更新,但不会重测,望周知。
代码:
#include<bits/stdc++.h>
#define M(x,y) make_pair(x,y)
using namespace std;
// 定义图的邻接表存储结构
int fr[210000], to[210000], v[210000], t;
// fr为邻接表的头指针数组,to为邻接表的点数组,nex为邻接表的下一个指针数组,v为边的权值数组,tl为当前边的数量
int d[210000];
// d数组用于存储从源点到每个点的最短距离
bool b[210000];
// b数组用于标记某个点是否已经被处理过
// 添加一条从x到y权值为w的边
int net[210000];
void zjia(int x, int y, int w) {
to[++t] = y;// 增加新边,y为边的终点
net[t] = fr[x];// 新边的下一个指针指向x点当前的第一条边
v[t] = w;// 设置新边的权值
fr[x] = t;// 更新x点的第一条边的指针
}
priority_queue<pair<int, int>>q;
// 优先队列,存储待处理的点和它们的距离(用负数表示,以便实现最小堆)
int main() {
// n为点数,m为边数,x,y,z为边的起点、终点和权值,s为源点
int n, m, x, y, z, s;
// 读入点数、边数和源点
cin >> n >> m >> s;
// 读入每条边的信息并添加到图中
for (int i = 1; i <= m; i++) {
// 添加一条从x到y权值为z的边
cin >> x >> y >> z;
zjia(x, y, z);
}
// 初始化所有点到源点的距离为无穷大,源点到自己的距离为0
for (int i = 1; i <= n; i++)d[i] = INT_MAX;
d[s] = 0;
q.push(M(0, s));
// 将源点加入优先队列
// Dijkstra算法主循环
while (!q.empty()) {
// 取出当前距离最小的点
// 从队列中移除该点
int a = q.top().second;
q.pop();
// 如果该点已经被处理过,则跳过
if (b[a])continue;
b[a] = 1;
// 标记该点已被处理
for (int i = fr[a]; i; i = net[i]) {
int y = to[i], p = v[i];
// 遍历该点所有出边
// y为边的终点,l为边的权值
if (d[y] > d[a] + p) {
d[y] = d[a] + p;
q.push(M(-d[y], y));
}
// 如果通过该边可以到达一个更近的点,则更新该点的距离,并将其加入优先队列
// 使用负数表示距离,以便实现最小堆
}
}
for (int i = 1; i <= n; i++)printf("%d ", d[i]);
// 输出所有点到源点的最短距离
return 0;
}