题解:洛谷 P4779 单源最短路径(标准版)

【题目来源】

洛谷:P4779 【模板】单源最短路径(标准版) - 洛谷

【题目描述】

给定一个 \(n\) 个点,\(m\) 条有向边的带非负权图,请你计算从 \(s\) 出发,到每个点的距离。

数据保证你能从 \(s\) 出发到任意点。

【输入】

第一行为三个正整数 \(n,m,s\)。第二行起 \(m\) 行,每行三个非负整数 \(u_i,v_i,w_i\),表示从 \(u_i\)\(v_i\) 有一条权值为 \(w_i\) 的有向边。

【输出】

输出一行 \(n\) 个空格分隔的非负整数,表示 \(s\) 到每个点的距离。

【输入样例】

4 6 1
1 2 2
2 3 2
2 4 1
1 3 5
3 4 3
1 4 4

【输出样例】

0 2 4 3

【核心思想】

  1. 问题分析:给定一个 \(n\) 个点、\(m\) 条有向边的非负权图,以及一个起点 \(s\),保证从 \(s\) 能到达所有点。要求输出 \(s\) 到每个点的最短距离。这是单源最短路的标准模板题,边权非负,\(n\)\(m\) 均可达到 \(10^5\) 级别,需要使用 \(O(m \log n)\) 的算法。

  2. 算法选择

    • 堆优化的 Dijkstra:适用于所有边权非负的单源最短路问题。利用优先队列(小根堆)维护当前未确定最短距离的节点,每次取出距离最小的节点进行松弛,每个节点最多入堆一次(实际可能多次,但总复杂度 \(O(m \log n)\))。
    • 链式前向星/邻接表:采用邻接表存储图,以支持高效遍历出边。
  3. 关键步骤

    • 初始化:距离数组 dist[1..n] 初始化为无穷大,dist[s] = 0;标记数组 vis[1..n] 初始为 false;优先队列 pq 压入 (0, s)
    • 主循环:当 pq 非空时:
      • 取出堆顶 (d, u),若 d > dist[u] 则跳过(表示该记录已过时)。
      • vis[u] 为真,跳过;否则标记 vis[u] = true
      • 遍历 u 的每条出边 (v, w)
        • dist[v] > dist[u] + w,则更新 dist[v],并将 (dist[v], v) 压入 pq
    • 输出:按顺序输出 dist[1..n]
  4. 时间/空间复杂度

    • 时间复杂度:\(O(m \log n)\),堆操作每次 \(O(\log n)\),每条边最多被松弛一次。
    • 空间复杂度:\(O(n + m)\),存储邻接表和距离数组。
  5. Dijkstra 算法的核心思想

    • 贪心策略:每次从未确定最短路的点中选取距离最小的点,因为边权非负,该点的最短距离已经确定。
    • 松弛操作:通过当前点更新其所有邻接点的距离,是动态规划思想的体现。
    • 堆优化:将朴素 Dijkstra 中找最小距离的 \(O(n)\) 步骤优化为 \(O(\log n)\),使得算法能处理大规模数据。
    • 适用场景:所有边权非负的单源最短路问题,是图论中最基础且最重要的算法之一。

【算法标签】

普及 #SPFA

【代码详解】

//堆优化Dijkstra
#include <cstring>
#include <iostream>
#include <algorithm>
#include <queue>
using namespace std;

const int N = 100010;  // 最大顶点数
int n, m, s;           // n: 顶点数, m: 边数, s: 起点
int a, b, c;           // 临时变量: 起点, 终点, 权重

struct edge
{
    int v;  // 邻接顶点
    int w;  // 边权重
};
vector<edge> e[N];  // 邻接表存储图
int d[N];           // 最短距离数组
int vis[N];         // 标记顶点是否已出队

/**
 * Dijkstra算法实现(堆优化)
 * 求从起点s到所有其他顶点的最短路径
 * 注意:这里使用大根堆,通过存入负距离实现小根堆功能
 * @param s 起始顶点
 */
void dijkstra(int s)
{
    // 初始化距离数组
    memset(d, 0x3f, sizeof d);  // 初始化为无穷大
    d[s] = 0;                     // 起点到自身距离为0

    // 使用大根堆存储(-距离, 顶点)对
    // 通过存储负距离,模拟小根堆的效果
    priority_queue<pair<int, int>> q;  // 默认是大根堆

    // 起点入队
    q.push({0, s});  // 距离为0,顶点为s

    // 主循环
    while (q.size())
    {
        // 取出堆顶元素
        auto t = q.top();
        q.pop();

        int u = t.second;  // 获取顶点编号

        // 如果顶点已出队,跳过
        if (vis[u])
        {
            continue;  // 再出队跳过
        }

        vis[u] = 1;  // 标记u已出队

        // 遍历u的所有邻接边
        for (auto ed : e[u])
        {
            int v = ed.v;  // 邻接顶点
            int w = ed.w;  // 边权重

            // 松弛操作
            if (d[v] > d[u] + w)
            {
                d[v] = d[u] + w;  // 更新最短距离

                // 入队,存储负距离以实现小根堆
                q.push({-d[v], v});  // 大根堆,负数使距离小的在堆顶
            }
        }
    }
}

int main()
{
    // 输入顶点数、边数、起点
    cin >> n >> m >> s;

    // 构建邻接表
    for (int i = 0; i < m; i++)
    {
        cin >> a >> b >> c;
        e[a].push_back({b, c});  // 添加有向边
    }

    // 执行Dijkstra算法
    dijkstra(s);

    // 输出结果
    for (int i = 1; i <= n; i++)
    {
        printf("%d ", d[i]);
    }

    return 0;
}
// 使用acwing模板二刷
#include <bits/stdc++.h>
using namespace std;

const int N = 100005;   // 最大顶点数
const int M = 200005;   // 最大边数

int n, m, s;            // n: 顶点数, m: 边数, s: 起点
int h[N];               // 邻接表头指针
int e[M], ne[M], w[M];  // 链式前向星: 终点, 下一条边, 权重
int idx;                // 边的索引
int dist[N];            // 起点到各点的最短距离
bool st[N];             // 标记顶点是否已确定最短路径

typedef pair<int, int> PII;  // 存储(距离, 顶点)
priority_queue<PII, vector<PII>, greater<PII> > heap;  // 小根堆

/**
 * 添加有向边
 * @param a 起点
 * @param b 终点
 * @param c 权重
 */
void add(int a, int b, int c)
{
    e[idx] = b;        // 边指向的顶点
    w[idx] = c;        // 边的权重
    ne[idx] = h[a];    // 指向原链表头
    h[a] = idx++;      // 更新头指针
}

/**
 * 堆优化的Dijkstra算法
 * 计算从起点s到所有顶点的最短路径
 * 注意:第22行代码有错误,应为dist[s] = 0
 * @param s 起始顶点
 */
void dijkstra(int s)
{
    // 初始化所有距离为无穷大
    memset(dist, 0x3f, sizeof(dist));

    // 错误:这里应该设置dist[s] = 0,而不是dist[1] = 0
    dist[1] = 0;  // 错误!应该是dist[s] = 0

    // 起点入堆
    heap.push({0, s});

    while (!heap.empty())
    {
        // 取出堆顶(距离最小的顶点)
        auto t = heap.top();
        heap.pop();

        int veid = t.second;     // 顶点编号
        int distance = t.first;  // 当前距离

        // 如果顶点已确定最短路径,跳过
        if (st[veid] == true)
        {
            continue;
        }

        st[veid] = true;  // 标记为已确定

        // 遍历veid的所有邻接边
        for (int i = h[veid]; i != -1; i = ne[i])
        {
            int j = e[i];  // 邻接顶点

            // 松弛操作:如果通过veid到j的路径更短
            if (dist[j] > distance + w[i])
            {
                dist[j] = distance + w[i];  // 更新最短距离
                heap.push({dist[j], j});    // 新距离入堆
            }
        }
    }
}

int main()
{
    // 输入顶点数、边数、起点
    cin >> n >> m >> s;

    // 初始化邻接表
    memset(h, -1, sizeof(h));

    // 读入边
    for (int i = 1; i <= m; i++)
    {
        int a, b, c;
        cin >> a >> b >> c;
        add(a, b, c);  // 添加有向边
    }

    // 执行Dijkstra算法
    dijkstra(s);

    // 输出结果
    for (int i = 1; i <= n; i++)
    {
        cout << dist[i] << " ";
    }
    cout << endl;

    return 0;
}

【运行结果】

4 6 1
1 2 2
2 3 2
2 4 1
1 3 5
3 4 3
1 4 4
0 2 4 3
posted @ 2026-09-01 22:41  团爸讲算法  阅读(18)  评论(0)    收藏  举报