#7299. 2026/6/7/周日下午三人笔记(Dij优先队列优化)

2026/6/7/周日下午三人笔记(Dij优先队列优化)

堆优化 Dijkstra 算法笔记

适合学完优先队列、图的邻接表后复习使用。
核心用途:求 非负边权图 中,从一个起点到其他所有点的最短路。


一、代码模板

先直接看完整模板。

#include <bits/stdc++.h>
using namespace std;

struct Edge {
    int to;              // 边的终点
    long long weight;    // 边权
};

struct Node {
    long long dist;      // 当前从起点到这个点的距离
    int id;              // 点的编号
};

// 小根堆:距离更小的点优先出队
bool cmp(Node a, Node b) {
    return a.dist > b.dist;
}

const int N = 1000005;
const long long INF = 1e18;

int n, m, startNode;
vector<Edge> graph[N];       // 邻接表存图
long long dist[N];           // dist[i] 表示起点到 i 的最短距离
bool visited[N];             // visited[i] 表示 i 是否已经确定最短路

priority_queue<Node, vector<Node>, decltype(&cmp)> pq(cmp);

int main() {
    cin >> n >> m >> startNode;

    // 读入有向图:x -> y,边权为 z
    for (int i = 1; i <= m; i++) {
        int x, y;
        long long z;
        cin >> x >> y >> z;
        graph[x].push_back({y, z});
    }

    // 初始化所有距离为无穷大
    for (int i = 1; i <= n; i++) {
        dist[i] = INF;
    }

    // 起点到自己的距离为 0
    dist[startNode] = 0;
    pq.push({0, startNode});

    // 堆优化 Dijkstra
    while (!pq.empty()) {
        Node current = pq.top();
        pq.pop();

        int u = current.id;

        // 如果这个点已经确定过最短路,就跳过
        if (visited[u]) {
            continue;
        }

        visited[u] = true;

        // 用当前点 u 去更新它能到达的点
        for (Edge edge : graph[u]) {
            int v = edge.to;
            long long w = edge.weight;

            // 松弛操作:如果经过 u 到 v 更短,就更新 dist[v]
            if (dist[u] + w < dist[v]) {
                dist[v] = dist[u] + w;
                pq.push({dist[v], v});
            }
        }
    }

    // 输出起点到每个点的最短距离
    for (int i = 1; i <= n; i++) {
        cout << dist[i] << ' ';
    }

    return 0;
}

二、这个算法解决什么问题?

Dijkstra 算法用来解决:

给定一个带边权的图,求从一个起点到其他所有点的最短距离。

常见输入形式:

n m s
x1 y1 z1
x2 y2 z2
...
xm ym zm

含义:

  • n:点数
  • m:边数
  • s:起点
  • x y z:从 xy 有一条边,长度是 z

如果是无向图,需要存两条边:

graph[x].push_back({y, z});
graph[y].push_back({x, z});

如果是有向图,只存一条边:

graph[x].push_back({y, z});

三、什么时候可以用 Dijkstra?

Dijkstra 适用于:

  • 单源最短路
  • 边权非负
  • 图可以是有向图,也可以是无向图

不能用于:

  • 有负边权的图
  • 需要处理负环的图

如果边权可能为负,通常要考虑 Bellman-Ford 或 SPFA。


四、核心思想

Dijkstra 的核心思想是:

每次从还没确定最短路的点中,找出当前距离起点最近的点。
这个点的最短路就可以确定了。

然后用这个点去更新它周围的点。

这个更新过程叫做 松弛


五、几个重要数组和结构体

1. 邻接表 graph

vector<Edge> graph[N];

表示一张图。

如果有一条边:

1 -> 3,边权为 5

就存成:

graph[1].push_back({3, 5});

2. 距离数组 dist

long long dist[N];

含义:

dist[i] = 起点到 i 的当前最短距离

初始化时:

dist[i] = INF;

起点:

dist[startNode] = 0;

3. 标记数组 visited

bool visited[N];

含义:

visited[i] = true;

表示点 i 的最短路已经确定,不需要再处理。


4. 优先队列 pq

priority_queue<Node, vector<Node>, decltype(&cmp)> pq(cmp);

作用:

每次快速取出当前距离起点最近的点。

因为 C++ 默认的 priority_queue 是大根堆,所以要写比较函数,让它变成小根堆。

bool cmp(Node a, Node b) {
    return a.dist > b.dist;
}

这里的意思是:

dist 小的优先级更高。


六、松弛操作

松弛是 Dijkstra 最重要的操作。

如果现在已经知道:

dist[u]

表示起点到 u 的最短距离。

又有一条边:

u -> v,边权为 w

那么从起点到 v 可以尝试走:

起点 -> u -> v

距离就是:

dist[u] + w

如果这个距离比原来的 dist[v] 更小,就更新:

if (dist[u] + w < dist[v]) {
    dist[v] = dist[u] + w;
    pq.push({dist[v], v});
}

这就是松弛。


七、算法流程

  1. 建图。
  2. 所有距离初始化为 INF
  3. 起点距离设为 0
  4. 把起点放入优先队列。
  5. 每次取出距离最小的点 u
  6. 如果 u 已经处理过,跳过。
  7. 否则标记 u 已确定最短路。
  8. u 更新所有邻接点。
  9. 队列为空时,算法结束。

八、正确性证明

1. 要证明什么?

我们要证明:

当一个点 u 第一次从优先队列中取出并被标记 visited[u] = true 时,dist[u] 就是起点到 u 的最短距离。


2. 为什么当前取出的点一定最短?

优先队列每次取出的是当前 dist 最小的点。

假设当前取出的点是 u

如果存在一条更短的路径能到达 u,那么这条路径一定要经过某个还没有确定的点。

但是由于所有边权都是非负数:

路径继续往后走,距离只会变大,不会变小。

所以一个还没确定的点,不可能绕一圈后得到比当前 dist[u] 更小的距离。

因此,当前 u 的距离已经不可能再变小。

所以:

dist[u]

就是起点到 u 的最短路。


3. 为什么边权不能为负?

如果有负边,后面可能出现:

先走一段比较长的路,再通过负边把总距离变小

这样一个已经被确定的点,之后可能又被更新得更小。

这会破坏 Dijkstra 的正确性。

所以 Dijkstra 要求:

所有边权必须非负。


九、时间复杂度分析

设:

  • n 是点数
  • m 是边数

使用邻接表存图。

每条边最多被检查一次或少数几次。

每次更新距离时,会向优先队列中插入一个新状态。

优先队列每次 pushpop 的复杂度是:

O(log m)

所以总时间复杂度通常写作:

O((n + m) log m)

也常写成:

O(m log m)

或:

O(m log n)

空间复杂度:

O(n + m)

原因:

  • 邻接表存边:O(m)
  • 距离数组、标记数组:O(n)
  • 优先队列:最多存若干个待处理状态

十、常见题型

题型 1:有向图单源最短路

题意:

给出有向边,求从起点到所有点的最短距离。

建图:

graph[x].push_back({y, z});

只存一条边。


题型 2:无向图单源最短路

题意:

道路可以双向通行,求从起点到所有点的最短距离。

建图:

graph[x].push_back({y, z});
graph[y].push_back({x, z});

要存两条边。


题型 3:求起点到终点的最短路

有些题不要求输出所有点,只要求输出起点到终点的距离。

假设终点是 target

最后输出:

cout << dist[target];

也可以在终点第一次出队时提前结束:

if (u == target) {
    break;
}

因为 Dijkstra 中,一个点第一次被确定时,它的最短路已经确定。


题型 4:无法到达的点

如果算法结束后:

dist[i] == INF

说明起点到 i 不可达。

可以按题目要求输出:

if (dist[i] == INF) {
    cout << -1 << ' ';
} else {
    cout << dist[i] << ' ';
}

题型 5:网格图最短路

如果每个格子有代价,或者从一个格子走到另一个格子的代价不同,也可以用 Dijkstra。

例如:

从 (x, y) 走到 (nx, ny) 的代价是 cost[nx][ny]

松弛可以写成:

if (dist[x][y] + cost[nx][ny] < dist[nx][ny]) {
    dist[nx][ny] = dist[x][y] + cost[nx][ny];
    pq.push({dist[nx][ny], id(nx, ny)});
}

如果每一步代价都相同,一般用 BFS 更简单。

如果每一步代价不同且非负,可以考虑 Dijkstra。


十一、常见错误

1. 忘记初始化距离

错误:

dist[startNode] = 0;

但是其他点没有设成无穷大。

正确:

for (int i = 1; i <= n; i++) {
    dist[i] = INF;
}
dist[startNode] = 0;

2. 无向图只存了一条边

如果题目说道路是双向的,要写:

graph[x].push_back({y, z});
graph[y].push_back({x, z});

不能只写一条。


3. int 爆掉

如果边权很大,路径长度可能超过 int

建议距离数组使用:

long long dist[N];

无穷大写:

const long long INF = 1e18;

4. 优先队列方向写反

默认 priority_queue 是大根堆。

Dijkstra 需要每次取出距离最小的点,所以要小根堆。

可以用比较函数:

bool cmp(Node a, Node b) {
    return a.dist > b.dist;
}

也可以用 greater 写法。


5. 没有跳过已经确定的点

由于同一个点可能被多次放进优先队列,所以要写:

if (visited[u]) {
    continue;
}
visited[u] = true;

否则可能重复处理,导致效率变差。


十二、另一种更短的优先队列写法

如果不想写结构体比较函数,也可以使用 pair

priority_queue<pair<long long, int>, vector<pair<long long, int>>, greater<pair<long long, int>>> pq;

含义:

{距离, 点编号}

使用方式:

pq.push({0, startNode});

while (!pq.empty()) {
    auto [d, u] = pq.top();
    pq.pop();

    if (visited[u]) continue;
    visited[u] = true;

    for (auto edge : graph[u]) {
        int v = edge.to;
        long long w = edge.weight;

        if (dist[u] + w < dist[v]) {
            dist[v] = dist[u] + w;
            pq.push({dist[v], v});
        }
    }
}

这种写法更短,但初学时结构体写法更清晰。


十三、复习重点

请重点记住这几句话:

  1. Dijkstra 用来求 非负边权图的单源最短路
  2. dist[i] 表示起点到 i 的当前最短距离。
  3. 优先队列每次取出当前距离最小的点。
  4. 一个点第一次被确定时,它的最短路就不会再变。
  5. 松弛操作是:
if (dist[u] + w < dist[v]) {
    dist[v] = dist[u] + w;
}
  1. 堆优化后复杂度约为:
O(m log n)

十四、背诵版核心代码

最后只背这一段核心即可:

dist[startNode] = 0;
pq.push({0, startNode});

while (!pq.empty()) {
    Node current = pq.top();
    pq.pop();

    int u = current.id;
    if (visited[u]) continue;
    visited[u] = true;

    for (Edge edge : graph[u]) {
        int v = edge.to;
        long long w = edge.weight;

        if (dist[u] + w < dist[v]) {
            dist[v] = dist[u] + w;
            pq.push({dist[v], v});
        }
    }
}