题解:洛谷 P3371 单源最短路径(弱化版)

【题目来源】

洛谷:P3371 【模板】单源最短路径(弱化版) - 洛谷

【题目描述】

如题,给出一个有向图,请输出从某一点出发到所有点的最短路径长度。

【输入】

第一行包含三个整数 \(n,m,s\),分别表示点的个数、有向边的个数、出发点的编号。

接下来 \(m\) 行每行包含三个整数 \(u,v,w\),表示一条 \(u→v\) 的,长度为 \(w\) 的边。

【输出】

输出一行 \(n\) 个整数,第 \(i\) 个表示 \(s\) 到第 \(i\) 个点的最短路径,若不能到达则输出 \(2^{31}−1\)

【输入样例】

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\) 到所有点的最短路径长度。若某点不可达,则输出 \(2^{31}-1\)。这是最经典的单源最短路问题,由于边权均为正(题目未明确说明负权,但模板题通常为非负),使用堆优化的 Dijkstra 算法可在 \(O(m \log n)\) 时间内解决。

  2. 算法选择

    • Dijkstra 算法(堆优化):适用于所有边权非负的单源最短路。使用优先队列(小根堆)维护当前未确定最短距离的节点,每次取出距离最小的节点进行松弛,保证每个节点只被处理一次。
    • SPFA(备选):虽然也能处理正权图,但在无负权时 Dijkstra 效率更稳定,不易被卡。SPFA 通常用于含负权边的情况。
  3. 关键步骤(以堆优化 Dijkstra 为例):

    • 初始化dist[1..n] = INFdist[s] = 0,将 (0, s) 入小根堆。
    • 主循环:当堆非空时:
      • 取出堆顶 (d, u),若 d > dist[u] 则跳过(过时记录)。
      • 标记 u 已确定(vis[u] = true)。
      • 遍历 u 的所有出边 (v, w)
        • dist[v] > dist[u] + w,则更新 dist[v],并将 (dist[v], v) 入堆。
    • 输出:按顺序输出 dist[1..n],若为 INF 则输出 2^{31}-1
  4. 时间/空间复杂度

    • 时间复杂度:\(O(m \log n)\),堆优化 Dijkstra 的标准复杂度。
    • 空间复杂度:\(O(n + m)\),邻接表存储图。
  5. 单源最短路的核心思想

    • Dijkstra 的贪心性质:每次从所有未确定最短路的点中选择距离最小的点,因为所有边权非负,该点的最短距离不会通过其他未确定的点变得更小。
    • 松弛操作:通过当前点更新其邻接点的距离,是动态规划思想的体现。
    • 堆优化的作用:朴素 Dijkstra 每次找最小距离需要 \(O(n)\),堆优化将此过程降为 \(O(\log n)\),适用于 \(n, m\) 较大的情况(如 \(10^5\) 级别)。
    • 适用场景:所有边权非负的单源最短路问题,是图论中最基础且最重要的算法之一。

【算法标签】

普及 #Dijkstra

【代码详解】

// Dijkstra算法实现
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
#define inf 2147483647  // 定义无穷大
using namespace std;

int n, m, s;  // n: 顶点数, m: 边数, s: 起点
int a, b, c;  // 临时变量: 起点, 终点, 权重

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

/**
 * Dijkstra算法实现
 * 求从起点s到所有其他顶点的最短路径
 * @param s 起始顶点
 */
void dijkstra(int s)
{
    // 初始化所有距离为无穷大
    for (int i = 0; i <= n; i++)
    {
        d[i] = inf;
    }

    // 起点到自己的距离为0
    d[s] = 0;

    // 最多需要n-1次松弛
    for (int i = 1; i < n; i++)  // 枚举次数
    {
        int u = 0;  // 当前未访问顶点中距离最小的顶点

        // 在未标记的顶点中寻找距离最小的顶点
        for (int j = 1; j <= n; j++)  // 枚举所有顶点
        {
            if (!vis[j] && d[j] < d[u])
            {
                u = j;
            }
        }

        vis[u] = 1;  // 标记u已找到最短路径

        // 更新u的所有邻接顶点的距离
        for (auto ed : e[u])  // 枚举u的所有邻边
        {
            int v = ed.v;  // 邻接顶点
            int w = ed.w;  // 边权重

            // 松弛操作:如果通过u到v的路径更短
            if (d[v] > d[u] + w)
            {
                d[v] = d[u] + w;  // 更新最短距离
            }
        }
    }
}

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

    // 构建邻接表
    for (int i = 0; i < m; i++)
    {
        cin >> a >> b >> c;
        e[a].push_back({b, c});  // 添加有向边 a->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 = 10005;       // 最大顶点数
const int M = 500005;      // 最大边数

int n, m, s;               // n: 顶点数, m: 边数, s: 起点
int h[N];                  // 邻接表头指针
int e[M], w[M], ne[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到所有顶点的最短路径
 * @param s 起始顶点
 */
void dijkstra(int s)
{
    // 初始化所有距离为无穷大
    // memset(dist, 0x3f, sizeof(dist));
    for (int i = 1; i <= n; i++)
    {
        dist[i] = 2147483647;  // INT_MAX
    }

    dist[s] = 0;                 // 起点到自身距离为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;
}
// 使用spfa再写一遍
#include <bits/stdc++.h>
using namespace std;

const int N = 10005;       // 最大顶点数
const int M = 500005;      // 最大边数
const int INF = (1 << 31) - 1;  // 定义无穷大(INT_MAX)

int n, m, s;               // n: 顶点数, m: 边数, s: 起点
int h[N];                  // 邻接表头指针
int e[M], ne[M], w[M];     // 链式前向星: 终点, 下一条边, 权重
int idx;                   // 边的索引
int dist[N];               // 起点到各点的最短距离
int st[N];                 // 标记顶点是否在队列中(SPFA使用)

/**
 * 添加有向边
 * @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++;      // 更新头指针
}

/**
 * SPFA算法 (Shortest Path Faster Algorithm)
 * 求从起点s到所有顶点的最短路径
 * 可以处理负权边,但不能处理负权环
 */
void spfa()
{
    // 初始化所有距离为无穷大
    // memset(dist, 0x3f, sizeof(st));  // 注释掉的错误代码
    for (int i = 1; i <= n; i++)
    {
        dist[i] = INF;  // 初始化为INT_MAX
    }

    dist[s] = 0;         // 起点到自身距离为0

    queue<int> q;       // SPFA队列
    q.push(s);          // 起点入队
    st[s] = true;       // 标记起点在队列中

    while (!q.empty())
    {
        int t = q.front();  // 取出队首顶点
        q.pop();
        st[t] = false;      // 标记t不在队列中

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

            // 松弛操作:如果通过t到j的路径更短
            if (dist[j] > dist[t] + w[i])
            {
                dist[j] = dist[t] + w[i];  // 更新最短距离

                // 如果j不在队列中,入队
                if (!st[j])
                {
                    q.push(j);
                    st[j] = true;  // 标记j在队列中
                }
            }
        }
    }
}

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

    // 输入顶点数、边数、起点
    cin >> n >> m >> s;

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

    // 执行SPFA算法
    spfa();

    // 输出结果
    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 16:51  团爸讲算法  阅读(14)  评论(0)    收藏  举报