首页 > 其他分享 >关于最短路

关于最短路

时间:2024-08-05 14:52:29浏览次数:4  
标签:结点 dist int 短路 路径 算法 关于

定义

  • 单源最短路:从一个点出发,到其他所有点的最短距离
  • 多源最短路:从图中任意一点出发,到其他所有点的最短距离

记号

  • \(n~\)为图上点的数目,\(m~\)为图上边的数目;
  • \(s~\)为最短路的源点;
  • \(D(u)~\)为\(~s~\)点到\(~u~\)点的 实际 最短路长度;
  • \(~dis(u)~\)为 s 点到 u 点的 估计 最短路长度。任何时候都有\(~dis(u) \geq D(u)~\)。特别地,当最短路算法终止时,应有\(~dis(u)=D(u)~\)。
  • \(~w(u,v)~\)为\(~(u,v)~\)这一条边的边权。

性质

  • 对于边权为正的图,任意两个结点之间的最短路,不会经过重复的结点。
  • 对于边权为正的图,任意两个结点之间的最短路,不会经过重复的边。
  • 对于边权为正的图,任意两个结点之间的最短路,任意一条的结点数不会超过\(~n\),边数不会超过\(~n-1\)。

那么,现在将开始对于最短路的探索。

单源最短路实现

\(Dijkstra\)

介绍

迪杰斯特拉算法(Dijkstra)是由荷兰计算机科学家狄克斯特拉于1959年提出的,因此又叫狄克斯特拉算法。是从一个顶点到其余各顶点的最短路径算法,解决的是有权图中最短路径问题。迪杰斯特拉算法主要特点是从起始点开始,采用贪心算法的策略,每次遍历到始点距离最近且未访问过的顶点的邻接节点,直到扩展到终点为止。

定义

Dijkstra算法一般的表述通常有两种方式,一种用永久和临时标号方式,一种是用OPEN, CLOSE表的方式,这里均采用永久和临时标号的方式。注意该算法要求图中不存在负权边。

原理

  1. 首先,\(D~\)的每个元素\(~D_i~\)表示当前所找到的从起始点\(~u\) (即源点\(~u\))到其它每个顶点\(~u_i~\)的长度。
  2. \(D~\)的初始状态为:若从\(~u~\)到\(~v_i~\)有弧(即从\(~u~\)到\(~v_i~\)存在连接边),则\(~D_i~\)为弧上的权值(即为从\(~u~\)到\(~v_i~\)的边的权值);否则置\(~D_i~\)为∞。
  3. 那么,下一条长度次短的是哪一条呢?也就是找到从源点\(~u~\)到下一个顶点的最短路径长度所对应的顶点,且这条最短路径长度仅次于从源点\(~u~\)到顶点\(~v_j~\)的最短路径长度。假设该次短路径的终点是 ,则可想而知,这条路径要么是(\(u,~v_k\)),或者是(\(u,~v_j,~v_k\))。它的长度或者是从\(~u~\)到\(~v_k~\)的弧上的权值,或者是\(~D_j~\)加上从\(~v_j~\)到\(~v_k~\)的弧上的权值。
  4. 一般情况下,假设\(~S~\)为已求得的从源点\(~u~\)出发的最短路径长度的顶点的集合,则可证明:下一条次最短路径(设其终点为\(~x\))要么是弧(\(u,~x\)),或者是从源点\(~u~\)出发的中间只经过\(~S~\)中的顶点而最后到达顶点\(~x~\)的路径。
    因此,下一条长度次短的的最短路径长度必是\(~D = Min\{D~|~∈V-S \}\),其中\(~D_i~\)要么是弧(\(u,~v_i\))上的权值,要么是\(D_k\)(\(v_k~\)∈\(~S\))和弧(\(v_k, v_i\))上的权值之和。

过程

将结点分成两个集合:已确定最短路长度的点集(记为\(~S~\)集合)的和未确定最短路长度的点集(记为\(~T~\)集合)。一开始所有的点都属于\(~T~\)集合。

初始化\(~dis(s)=0\),其他点的\(~dis~\)均为\(~+\infty\)。

然后重复这些操作:

  1. 从\(~T~\)集合中,选取一个最短路长度最小的结点,移到\(~S~\)集合中。
  2. 对那些刚刚被加入\(~S~\)集合的结点的所有出边执行松弛操作。
    直到\(~T~\)集合为空,算法结束。

证明(摘自 OI wiki)

下面用数学归纳法证明,在 所有边权值非负 的前提下,\(Dijkstra\) 算法的正确性。

简单来说,我们要证明的,就是在执行 1 操作时,取出的结点 \(u\) 最短路均已经被确定,即满足 \(D(u) = dis(u)\)。

初始时 \(S = \varnothing\),假设成立。

接下来用反证法。

设\(~u\) 点为算法中第一个在加入\(~S~\)集合时不满足\(~D(u) = dis(u)\) 的点。因为\(~s\) 点一定满足\(~D(u)=dis(u)=0\),且它一定是第一个加入\(~S\) 集合的点,因此将\(~u\) 加入\(~S\) 集合前\(~S \neq \varnothing\),如果不存在\(~s\) 到\(~u\) 的路径,则\(~D(u) = dis(u) = +\infty\),与假设矛盾。

于是一定存在路径\(~s \to x \to y \to u\),其中\(~y\) 为 \(s \to u\) 路径上第一个属于\(~T\) 集合的点,而\(~x\) 为\(~y\) 的前驱结点(显然\(~x \in S\))。需要注意的是,可能存在\(~s = x\) 或\(~y = u\) 的情况,即\(~s \to x\) 或 \(y \to u\) 可能是空路径。

因为在\(~u\) 结点之前加入的结点都满足\(~D(u) = dis(u)\),所以在\(~x\) 点加入到\(~S\) 集合时,有\(~D(x) = dis(x)\),此时边\(~(x,y)\) 会被松弛,从而可以证明,将\(~u\) 加入到\(~S\) 时,一定有\(~D(y)=dis(y)\)。

下面证明\(~D(u) = dis(u)\) 成立。在路径\(~s \to x \to y \to u\) 中,因为图上所有边边权非负,因此\(~D(y) \leq D(u)\)。从而\(~dis(y) \leq D(y) \leq D(u)\leq dis(u)\)。但是因为\(~u\) 结点在 1 过程中被取出\(~T\) 集合时,\(~y\) 结点还没有被取出\(~T\) 集合,因此此时有\(~dis(u)\leq dis(y)\),从而得到\(~dis(y) = D(y) = D(u) = dis(u)\),这与\(~D(u)\neq dis(u)\) 的假设矛盾,故假设不成立。

因此我们证明了,1 操作每次取出的点,其最短路均已经被确定。命题得证。

实现(仅 C++)

int g[N][N];  // 存储每条边
int dist[N];  // 存储1号点到每个点的最短距离
bool st[N];   // 存储每个点的最短路是否已经确定

// 求1号点到n号点的最短路,如果不存在则返回-1
int dijkstra()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;

    for (int i = 0; i < n - 1; i ++ )
    {
        int t = -1;     // 在还未确定最短路的点中,寻找距离最小的点
        for (int j = 1; j <= n; j ++ )
            if (!st[j] && (t == -1 || dist[t] > dist[j]))
                t = j;

        // 用t更新其他点的距离
        for (int j = 1; j <= n; j ++ )
            dist[j] = min(dist[j], dist[t] + g[t][j]);

        st[t] = true;
    }

    if (dist[n] == 0x3f3f3f3f) return -1;
    return dist[n];
}

堆优化

如果边数远小于\(~n^2\),对此可以考虑用堆这种数据结构进行优化,取出最短路径的复杂度降为\(~O(1)\);每次调整的复杂度降为\(~O(mlogn)\);\(m~\)为该点的边数。

具体步骤:

  1. 将源点加入堆,并调整堆。
  2. 选出堆顶元素\(~u\)(即代价最小的元素),从堆中删除,并对堆进行调整。
  3. 处理与\(~u~\)相邻的,未被访问过的,满足三角不等式的顶点。
    1): 若该点在堆里,更新距离,并调整该元素在堆中的位置。
    2): 若该点不在堆里,加入堆,更新堆。
  4. 若取到的u为终点,结束算法;否则重复步骤2、3。

堆优化实现

typedef pair<int, int> PII;

int n;      // 点的数量
int h[N], w[N], e[N], ne[N], idx;       // 邻接表存储所有边
int dist[N];        // 存储所有点到1号点的距离
bool st[N];     // 存储每个点的最短距离是否已确定

// 求1号点到n号点的最短距离,如果不存在,则返回-1
int dijkstra()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    priority_queue<PII, vector<PII>, greater<PII>> heap;
    heap.push({0, 1});      // first存储距离,second存储节点编号

    while (heap.size())
    {
        auto t = heap.top();
        heap.pop();

        int ver = t.second, distance = t.first;

        if (st[ver]) continue;
        st[ver] = true;

        for (int i = h[ver]; i != -1; i = ne[i])
        {
            int j = e[i];
            if (dist[j] > distance + w[i])
            {
                dist[j] = distance + w[i];
                heap.push({dist[j], j});
            }
        }
    }

    if (dist[n] == 0x3f3f3f3f) return -1;
    return dist[n];
}

\(Bellman-Ford\)

介绍

贝尔曼-福特算法(Bellman-Ford)是由理查德·贝尔曼(Richard Bellman) 和 莱斯特·福特 创立的,求解单源最短路径问题的一种算法。有时候这种算法也被称为 Moore-Bellman-Ford 算法,因为 Edward F. Moore 也为这个算法的发展做出了贡献。它的原理是对图进行\(~V-1~\)次松弛操作,得到所有可能的最短路径。其优于迪科斯彻算法的方面是边的权值可以为负数、实现简单,缺点是时间复杂度过高,高达O(VE)。但算法可以进行若干种优化,提高了效率。

过程

先介绍 \(Bellman–Ford\) 算法要用到的松弛操作(\(Dijkstra\) 算法也会用到松弛操作)。

对于边 \((u,v)\),松弛操作对应下面的式子:\(dis(v) = \min(dis(v), dis(u) + w(u, v))\)。

这么做的含义是显然的:我们尝试用 \(S \to u \to v\)(其中 \(S \to u\) 的路径取最短路)这条路径去更新 \(v\) 点最短路的长度,如果这条路径更优,就进行更新。

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

每次循环是 \(O(m)\) 的,那么最多会循环多少次呢?

在最短路存在的情况下,由于一次松弛操作会使最短路的边数至少 \(+1\),而最短路的边数最多为 \(n-1\),因此整个算法最多执行 \(n-1\) 轮松弛操作。故总时间复杂度为 \(O(nm)\)。

但还有一种情况,如果从 \(S\) 点出发,抵达一个负环时,松弛操作会无休止地进行下去。注意到前面的论证中已经说明了,对于最短路存在的图,松弛操作最多只会执行 \(n-1\) 轮,因此如果第 \(n\) 轮循环时仍然存在能松弛的边,说明从 \(S\) 点出发,能够抵达一个负环。

实现

int n, m;       // n表示点数,m表示边数
int dist[N];        // dist[x]存储1到x的最短路距离

struct Edge     // 边,a表示出点,b表示入点,w表示边的权重
{
    int a, b, w;
}edges[M];

// 求1到n的最短路距离,如果无法从1走到n,则返回-1。
int bellman_ford()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;

    // 如果第n次迭代仍然会松弛三角不等式,就说明存在一条长度是n+1的最短路径,由抽屉原理,路径中至少存在两个相同的点,说明图中存在负权回路。
    for (int i = 0; i < n; i ++ )
    {
        for (int j = 0; j < m; j ++ )
        {
            int a = edges[j].a, b = edges[j].b, w = edges[j].w;
            if (dist[b] > dist[a] + w)
                dist[b] = dist[a] + w;
        }
    }

    if (dist[n] > 0x3f3f3f3f / 2) return -1;
    return dist[n];
}

优化后的\(~Bellman-Ford\)——\(SPFA\)

介绍和证明

若给定的图存在负权边,类似\(~Dijkstra~\)算法等算法便没有了用武之地,\(~SPFA~\)算法便派上用场了。简洁起见,我们约定加权有向图G不存在负权回路,即最短路径一定存在。用数组\(~d~\)记录每个结点的最短路径估计值,而且用邻接表来存储图\(~G\)。我们采取的方法是动态逼近法:设立一个先进先出的队列用来保存待优化的结点,优化时每次取出队首结点\(~u~\),并且用\(~u~\)点当前的最短路径估计值对离开\(~u~\)点所指向的结点\(~v~\)进行松弛操作,如果v点的最短路径估计值有所调整,且\(~v~\)点不在当前的队列中,就将v点放入队尾。这样不断从队列中取出结点来进行松弛操作,直至队列空为止。
定理:只要最短路径存在,上述\(~SPFA~\)算法必定能求出最小值。证明:每次将点放入队尾,都是经过松弛操作达到的。换言之,每次的优化将会有某个点v的最短路径估计值\(~d_v~\)变小。所以算法的执行会使\(~d~\)越来越小。由于我们假定图中不存在负权回路,所以每个结点都有最短路径值。因此,算法不会无限执行下去,随着\(~d~\)值的逐渐变小,直到到达最短路径值时,算法结束,这时的最短路径估计值就是对应结点的最短路径值。
实际上,如果一个点进入队列达到\(~n~\)次,则表明图中存在负环,没有最短路径。
段凡丁论文中的复杂度证明 (\(O(kE)\),\(~k~\)是小常数)是错误的,在此略去。该算法的最坏时间复杂度为 \(O(VE)\)。
对\(~SPFA~\)的一个很直观的理解就是由无权图的\(~BFS~\)转化而来。在无权图中,\(BFS~\)首先到达的顶点所经历的路径一定是最短路(也就是经过的最少顶点数),所以此时利用数组记录节点访问可以使每个顶点只进队一次,但在带权图中,最先到达的顶点所计算出来的路径不一定是最短路。一个解决方法是放弃数组,此时所需时间自然就是指数级的,所以我们不能放弃数组,而是在处理一个已经在队列中且当前所得的路径比原来更好的顶点时,直接更新最优解。

实现

int n;      // 总点数
int h[N], w[N], e[N], ne[N], idx;       // 邻接表存储所有边
int dist[N];        // 存储每个点到1号点的最短距离
bool st[N];     // 存储每个点是否在队列中

// 求1号点到n号点的最短路距离,如果从1号点无法走到n号点则返回-1
int spfa()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;

    queue<int> q;
    q.push(1);
    st[1] = true;

    while (q.size())
    {
        auto t = q.front();
        q.pop();

        st[t] = false;

        for (int i = h[t]; i != -1; i = ne[i])
        {
            int j = e[i];
            if (dist[j] > dist[t] + w[i])
            {
                dist[j] = dist[t] + w[i];
                if (!st[j])     // 如果队列中已存在j,则不需要将j重复插入
                {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }

    if (dist[n] == 0x3f3f3f3f) return -1;
    return dist[n];
}

多源最短路实现

\(Floyd\)

介绍

\(Floyd~\)算法又称为插点法,是一种利用动态规划的思想寻找给定的加权图中多源点之间最短路径的算法,与\(~Dijkstra~\)算法类似。该算法名称以创始人之一、\(1978~\)年图灵奖获得者、斯坦福大学计算机科学系教授罗伯特·弗洛伊德命名。

步骤

我们定义一个数组 \(f[k][x][y]\),表示只允许经过结点 \(1\) 到 \(k\)(也就是说,在子图 \(V'={1, 2, \ldots, k}\) 中的路径,注意,\(x\) 与 \(y\) 不一定在这个子图中),结点 \(x\) 到结点 \(y\) 的最短路长度。

很显然,\(f[n][x][y]\) 就是结点 \(x\) 到结点 \(y\) 的最短路长度(因为 \(V'={1, 2, \ldots, n}\) 即为 \(V\) 本身,其表示的最短路径就是所求路径)。

接下来考虑如何求出 \(f\) 数组的值。

\(f[0][x][y]\):\(x\) 与 \(y\) 的边权,或者 \(0\),或者 \(+\infty\)(\(f[0][x][y]\) 什么时候应该是 \(+\infty\)?当 \(x\) 与 \(y\) 间有直接相连的边的时候,为它们的边权;当 \(x = y\) 的时候为零,因为到本身的距离为零;当 \(x\) 与 \(y\) 没有直接相连的边的时候,为 \(+\infty\))。

\(f[k][x][y] = min(f[k-1][x][y], f[k-1][x][k]+f[k-1][k][y])\)(\(f[k-1][x][y]\),为不经过 \(k\) 点的最短路径,而 \(f[k-1][x][k]\)+\(f[k-1][k][y]\),为经过了 \(k\) 点的最短路)。

上面两行都显然是对的,所以说这个做法空间是 \(O(N^3)\),我们需要依次增加问题规模(\(k\) 从 \(1\) 到 \(n\)),判断任意两点在当前问题规模下的最短路。

实现

初始化:
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= n; j ++ )
            if (i == j) d[i][j] = 0;
            else d[i][j] = INF;

// 算法结束后,d[a][b]表示a到b的最短距离
void floyd()
{
    for (int k = 1; k <= n; k ++ )
        for (int i = 1; i <= n; i ++ )
            for (int j = 1; j <= n; j ++ )
                d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}

标签:结点,dist,int,短路,路径,算法,关于
From: https://www.cnblogs.com/Liu-Tao-Chang/p/18343216/Shortiest-Past

相关文章

  • 关于比特率与波特率的定义与区别介绍
    比特率(BitRate)和波特率(BaudRate)是数字通信中两个重要的概念,它们分别用于衡量数字信号的传输速率和信号变化的次数。以下是对比特率和波特率的详细解析:比特率(BitRate)比特率的定义:比特率是指单位时间内传输或处理的比特(bit)的数量,通常以“比特每秒”(bit/s或bps)为单位。在电信和......
  • 补充:关于GRU的详细运作原理以及特殊的优化思路
    1.GRU的基本结构和运作原理1.1GRU的基本概念GatedRecurrentUnit(GRU)是一种简化版的循环神经网络(RNN),它通过引入门控机制来解决长期依赖问题,同时减少参数数量以降低计算复杂度。1.2GRU的结构详解GRU包含两个门控机制:更新门(updategate)和重置门(resetgat......
  • 软件工程专业导论大作业-关于华为自主研发的新编程语言基本原理其应用场景分析
    摘 要在2024年6月21日的华为开发者大会上,华为宣布了其自主研发的全新编程语言——“仓颉”。这一语言的推出旨在为其“升腾”AI芯片和云原生应用开发提供强大支持,并且有助于构建全球技术生态系统。“仓颉”编程语言特别设计以应对华为“升腾”AI芯片的需求,并且专注于硬件和......
  • Android R Settings关于屏保/PowerManagerService欺骗系统不让其进入休眠状态
    //屏保设置界面./packages/apps/Settings/src/com/android/settings/dream/DreamSettings.java//和PowerManagerService建立联系./frameworks/base/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java//系统时钟屏保继承了DreamService./package......
  • http/1.0、http/1.1、http/2关于复用这块的理解
    一概述http/1.0 请求响应模式,请求发送到服务器,服务器响应结果后连接立马关闭。由于Http1.0底层使用的是TCP。 需要完整的经理TCP三次握手和四次挥手。下次发起请求时重复以上步骤。http/1.1 请求响应模式,可共享链接,但是需要一个请求-响应结束后才能发起另一个请求-响应。默......
  • UE5学习笔记3-关于charactor的相机和弹簧臂组件
    一、环境说明,UE5.4+ vs2022 +win11二、相机和弹簧臂的作用    个人理解上,相机的作用相当于一个视角,我将其理解成是一个人在哪个地方朝向哪个方向看,弹簧臂的用用我将其理解成为一个将人的视角和人物模型或其他模型连接的桥梁三、相机和弹簧臂的代码    ......
  • JavaWeb之servlet关于Ajax实现前后端分离
    一、什么是Ajax:AJAX=AsynchronousJavaScriptandXML(异步的JavaScript和XML)。AJAX不是新的编程语言,而是一种使用现有标准的新方法。AJAX最大的优点是在不重新加载整个页面的情况下,可以与服务器交换数据并更新部分网页内容。AJAX不需要任何浏览器插件,但需要用户允......
  • 关于C语言中素数的求解
    什么是素数?一个大于1的自然数,除了1和它自身外,不能被其他自然数整除的数叫做素数。(素数=质数)在C语言中求解素数的几种方法。方法一:直接试数法(从1开始逐一试数)例:求解100到200之间的素数。#include<stdio.h>intmain(){ inti=0; intcount=0; for(i=100;i<=......
  • C语言关于函数的基本介绍
    目录一、前言二、为什么需要函数?三、什么是函数?四、函数的作用及分类。五、函数的基本用法。六、主函数的简单介绍。七、函数原型以及一些常用的系统函数的介绍。一、前言    这些都是作者学习C语言过程中了解到的只是,有的地方可能不是写的特别清楚,同时这也是......
  • 关于SRIO链路复位问题的总结
    1、当SRIO进行初始化时,首先会port-initialized(端口成功初始化)拉高随后link-initialized(链路被成功初始化)拉高。特别注意的是link-initialized表示已接收到七个连续的无差错控制符号,并且已经发送了15个连续符号。如下仿真图所示。2、Link_reset我们应该监视phy_rcvd_link_rese......