【图论】单源最短路

前言

今天,我们来讲最短路,首先看只有一个起点(单源)的情况。

为了书写方便,我们约定以下内容:

template<class W>
using Graph = vector<vector<pair<int, W>>>;  // 邻接表(vector实现)

template<class W>
using AdjMatrix = vector<vector<W>>;   // 邻接矩阵

template<class W>
struct Edge{
    int u, v;
    W w;
};
  • 图的顶点数记为n边数记为m
  • 最短路的源点记为s
  • w(u,v)为边(u,v)权值
  • d_u为点s到点u实际最短路长度。
  • dis_u为点s到点u估计最短路长度。任何时候都有dis_u \ge d_u,特别地,当最短路算法结束时,dis_u=d_u

Bellman-Ford 算法

Bellman–Ford 算法是一种基于松弛(relax)操作的最短路算法,可以求出有负权的图的最短路,并可以对最短路不存在(有负环)的情况进行判断。

过程

先介绍松弛操作:对于边u\to v,松弛操作对应下式:

dis_v=min(dis_v,dis_u+w(u,v))

这么做很容易理解:尝试用s\to u\to vs \to u取最短路径)去更新v点最短路的长度,如果这条路径更优,就进行更新。

我们不断尝试对图上每一条边进行松弛。每进行一轮循环,就对图上所有的边都松弛一遍,当一次循环中没有成功的松弛操作时,算法停止。

那么,需要循环多少次呢?

如果最短路存在,那么每一次松弛都会让最短路至少加上一条边。而一条简单路径最多只有n-1条边,所以最多松弛n-1轮就能得到最短路。

如果第n轮松弛操作仍然成功,那么说明从s出发,能到达一个负环。

综上,时间复杂度为O(nm)

判负环的坑点

需要注意的是,跑Bellman-Ford算法时,如果没有给出存在负环的结果,那么只能说明从s出发不能到达一个负环,不能保证图上没有负环(除了连通图)。

所以,如果需要判断图上是否有负环,最严谨的做法是建立一个超级源点,向图上每个节点连一条权值为0的边,然后以超级源点为起点跑 Bellman–Ford

代码

说明:返回的pair第一个元素表示图是否无负环,第二个元素才是最短路数组。

// edge - 边集
// n - 顶点数
// s - 源点
template<class W>
pair<bool, vector<W>> bellman(const vector<Edge<W>>& edge, int n, int s) {
    W inf = numeric_limits<W>::max();
    vector<W> dis(n, inf);
    dis[s] = 0;

    bool flag = false;
    for (int i = 0; i < n; i++) {
        flag = false;
        for (auto e : edge) {
            if (dis[e.u] == inf) continue;
            if (dis[e.v] > dis[e.u] + e.w) {
                    dis[e.v] = dis[e.u] + e.w;
                    flag = true;
            }
        }

        if (!flag) return make_pair(true, dis);
    }
    return make_pair(false, vector<W>());
}

SPFA 算法

Shortest Path Faster Algorithm,是对于 Bellman-Ford 的优化。

思路

很多时候,我们不需要那么多无用的松弛操作。

很显然,只有上一次被松弛的结点,所连接的边,才有可能引起下一次的松弛操作。

那么我们用队列来维护哪些结点可能会引起松弛操作,就能只访问必要的边了。

如果要判负环,可以设cnt_u表示s \to u的最短路经过了多少条边.

cnt_u \ge n时,说明从s点出发,可以抵达一个负环。

代码

// G - 图
// s - 源点
template<class W>
pair<bool, vector<W>> spfa(const Graph<W>& G, int s) {
    int n = G.size();
    W inf = numeric_limits<W>::max();

    queue<int> q;
    vector<W> dis(n, inf);
    vector<int> cnt(n, 0);
    vector<bool> vis(n, false);

    q.push(s);
    dis[s] = 0;
    vis[s] = true;

    while (q.size()) {
        int u = q.front();
        q.pop();
        vis[u] = false;

        for (auto [v, w] : G[u]) {
            if (dis[v] > dis[u] + w) {
                dis[v] = dis[u] + w;
                cnt[v] = cnt[u] + 1;

                if (cnt[v] >= n) return make_pair(false, vector<W>());
                if (!vis[v]) {
                    vis[v] = true;
                    q.push(v);
                }
            }
        }
    }

    return make_pair(true, dis);
}

Dijkstra 算法

过程

将节点分成两个集合:

  • 已确定最短路长度的点集(记为S集合)。
  • 未确定最短路长度的点集(记为T集合)。

初始时,所有节点都属于集合T

dis_s=0,其他点的dis\infty

接下来,重复以下操作:

  • T集合中,选择一个dis最小的节点,移到S集合中。
  • 对这个节点的所有出边进行松弛dis_v=min(dis_v,dis_u+w(u,v))

注意,带负权的图不能使用 Dijkstra 算法。

代码

// G - 图
// s - 源点
template<class W>
vector<W> dijkstra(const Graph<W>& G, int s) {
    int n = G.size();
    W inf = numeric_limits<W>::max();

    vector<W> dis(n, inf);
    vector<bool> vis(n, false);
    dis[s] = 0;

    for (int i = 0; i < n; i++) {
        int u = 0;
        W mind = inf;
        for(int j = 0; j < n; j++)
            if (!vis[j] && dis[j] < mind) {
                u = j;
                mind = dis[j];
            }

        vis[u] = true;
        for (auto [v, w] : G[u]) {
            dis[v] = min(dis[v], dis[u] + w);
        }
    }
    return dis;
}

优化

上述代码时间复杂度是O(n^2),能否优化呢?

我们发现,只能优化找最小点的过程。

我们用一个小根堆来维护(按第一关键字)。初始时候插入(0,s),计算时,直接从堆顶取出的节点即是最优,然后弹出该节点。

松弛的时候,如果dis_v发生改变,那么就插入(dis_v,v)

时间复杂度O(m \log m)

代码

template<class W>
vector<W> dijkstra(const Graph<W>& G, int s) {
    int n = G.size();
    W inf = numeric_limits<W>::max();

    priority_queue<pair<W, int>, vector<pair<W, int>>, greater<pair<W, int>>> q;

    vector<W> dis(n, inf);
    vector<bool> vis(n, false);
    dis[s] = 0;
    q.emplace(0, s);

    while (q.size()) {
        int u = q.top().second;
        q.pop();

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

        for (auto& [v, w] : G[u]) {
            if (dis[v] > dis[u] + w) {
                dis[v] = dis[u] + w;
                q.emplace(dis[v], v);
            }
        }
    }

    return dis;
}

下一次,我们将学习多源最短路。 

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Amber 大牛关于图论的总结 ,1.1M 大小.... 1. 图论 Graph Theory 1.1. 定义与术语 Definition and Glossary 1.1.1. 图与网络 Graph and Network 1.1.2. 图的术语 Glossary of Graph 1.1.3. 路径与回路 Path and Cycle 1.1.4. 连通性 Connectivity 1.1.5. 图论中特殊的集合 Sets in graph 1.1.6. 匹配 Matching 1.1.7. 树 Tree 1.1.8. 组合优化 Combinatorial optimization 1.2. 图的表示 Expressions of graph 1.2.1. 邻接矩阵 Adjacency matrix 1.2.2. 关联矩阵 Incidence matrix 1.2.3. 邻接表 Adjacency list 1.2.4. 弧表 Arc list 1.2.5. 星形表示 Star 1.3. 图的遍历 Traveling in graph 1.3.1. 深度优先搜索 Depth first search (DFS) 1.3.1.1. 概念 1.3.1.2. 求无向连通图中的桥 Finding bridges in undirected graph 1.3.2. 广度优先搜索 Breadth first search (BFS) 1.4. 拓扑排序 Topological sort 1.5. 路径与回路 Paths and circuits 1.5.1. 欧拉路径或回路 Eulerian path 1.5.1.1. 无向图 1.5.1.2. 有向图 1.5.1.3. 混合图 1.5.1.4. 无权图 Unweighted 1.5.1.5. 有权图 Weighed — 中国邮路问题The Chinese post problem 1.5.2. Hamiltonian Cycle 哈氏路径与回路 1.5.2.1. 无权图 Unweighted 1.5.2.2. 有权图 Weighed — 旅行商问题The travelling salesman problem 1.6. 网络优化 Network optimization 1.6.1. 最小生成树 Minimum spanning trees 1.6.1.1. 基本算法 Basic algorithms 1.6.1.1.1. Prim 1.6.1.1.2. Kruskal 1.6.1.1.3. Sollin(Boruvka) 1.6.1.2. 扩展模型 Extended models 1.6.1.2.1. 度限制生成树 Minimum degree-bounded spanning trees 1.6.1.2.2. k小生成树 The k minimum spanning tree problem(k-MST) 1.6.2. 最短路Shortest paths 1.6.2.1. 单源最短路 Single-source shortest paths 1.6.2.1.1. 基本算法 Basic algorithms 1.6.2.1.1.1. Dijkstra 1.6.2.1.1.2. Bellman-Ford 1.6.2.1.1.2.1. Shortest path faster algorithm(SPFA) 1.6.2.1.2. 应用Applications 1.6.2.1.2.1. 差分约束系统 System of difference constraints 1.6.2.1.2.2. 有向无环图上的最短路 Shortest paths in DAG 1.6.2.2. 所有顶点对间最短路 All-pairs shortest paths 1.6.2.2.1. 基本算法 Basic algorithms 1.6.2.2.1.1. Floyd-Warshall 1.6.2.2.1.2. Johnson 1.6.3. 网络流 Flow network 1.6.3.1. 最大流 Maximum flow 1.6.3.1.1. 基本算法 Basic algorithms 1.6.3.1.1.1. Ford-Fulkerson method 1.6.3.1.1.1.1. Edmonds-Karp algorithm 1.6.3.1.1.1.1.1. Minimum length path 1.6.3.1.1.1.1.2. Maximum capability path 1.6.3.1.1.2. 预流推进算法 Preflow push method 1.6.3.1.1.2.1. Push-relabel 1.6.3.1.1.2.2. Relabel-to-front 1.6.3.1.1.3. Dinic method 1.6.3.1.2. 扩展模型 Extended models 1.6.3.1.2.1. 有上下界的流问题 1.6.3.2. 最小费用流 Minimum cost flow 1.6.3.2.1. 找最小费用路 Finding minimum cost path 1.6.3.2.2. 找负权圈 Finding negative circle 1.6.3.2.3. 网络单纯形 Network simplex algorithm 1.6.4. 匹配 Matching 1.6.4.1. 二分图 Bipartite Graph 1.6.4.1.1. 无权图-匈牙利算法 Unweighted - Hopcroft and Karp algorithm 1.6.4.1.2. 带权图-KM算法 Weighted –Kuhn-Munkres(KM) algorithm 1.6.4.2. 一般图General Graph 1.6.4.2.1. 无权图-带花树算法 Unweighted - Blossom (Edmonds) 1.

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值