最短路初步理解

版权声明:转载请注明 https://blog.csdn.net/li13168690086/article/details/81913886

写在前面:其实最短路问题已经学习很久了,现在正式将它记录下来。

概念

  最短路属于图论范畴的问题,主要是求两个顶点之间,即起点和终点之间的最短(权值最小)路径。而最基础的问题有两种:1、单源路径最短问题;2、任意两点间最短路问题。

  先说第二种,任意两点间最短路问题,就是求所有两点间的最短路问题。现在一般只用一种解法:Floyd-Warshall 算法,简称FW算法。属于暴力求解,所以比较好理解,具体见下文。

  那关于第一种,求指定起点和终点之间的距离,解法一般是先求出起点到其他所有顶点的最短路径,最后再直接输出到指定终点的最短距离。现常用的解法有两种:1、Bellman-Ford算法;2、Dijkstea算法,详情见下文。

三种解法

1、Floyd-Warshall

    这个算法思想理解起来有点复杂,但实现却很简单,所以我先重点把它的概念讲清楚,再来给出代码实现。

   它是用于求图中所有顶点之间的最短路,那么我们要考虑何为最短路?能够形成最短路的情况,无非有两种:1、直接点对点,两点之间就是最短路;2、需要经过其他点来得到最短路。通俗点讲:你要从A地去B地,直线走是2个小时,可以。但你发现如果先从A地到C地,花费半个小时,再从C地到B地花费半个小时,那么总共就只要1个小时,所以你只要经过C地,就可以得到最短路。

  那FW算法的核心思想就是,通过不断寻找中间点,来比较看看是不是最短路。其中对应第一种直达的情况,只要将中间点看做是终点或起点就行了。接下来我将用图表的形式更加具体地展现,请看表1。

表1
  1 2 3 4
1 0 2 6 4
2 X 0 3 X
3 7 X 0 1
4 5 X 12 0

  先说明下:此表的值有4行4列,其中横向看是目标顶点1~4,纵向看是出发顶点1~4,蓝色是起点,红色是终点,每格表示对应起点到终点的权值。其中 'X',是道路不通的意思,在代码里可以看做是一个极大值(求最短路情况下)。

  接下来按照FW算法的思想,我们需要一个中间点变量K,从1~4,即将每一个顶点都要作为中间点来比较。然后再用i和j表示从顶点i到顶点j的最短路,二重循环。所以再加上变量K,就形成了三重循环。下表将展示如何用FW算法来求最短路的全过程,因为表1有4个顶点,那么就有64次比较。还有就是这里的最短路,就是最小的权值。

表2
k=1

1 2 3 4
1 0 2 6 4
2 X 0 3 X
3 7 9(1) 0 1
4 5 7(1) 11(1) 0

    表格最左侧是变量K的值,然后i值为起点,j值为终点,每次循环都是从顶点i到所有顶点j进行的。先看i=1时,对应了j=1、2、3、4这四个格,也就是出发顶点1到目标顶点1、2、3、4的最小权值。黑色的值表示点到点直线的权值,也就是没有经过其他点。橘黄色的值表示,对原有的值进行了更新,后面接的括号表示经过了哪些中间点,比如(3,2)顶点3到顶点2,原来直接连是无法连通的,但如果顶点3先到顶点1,然后再从顶点1到顶点2,就有路了,权值是7+2=9,以此类推。每次循环都只能通过一个中间点,就是K值表示的顶点,表2现在K = 1, 所以中间点是顶点1

表3
k=2

1 2 3 4
1 0 2 5(2) 4
2 X 0 3 X
3 7 9(1) 0 1
4 5 7(1) 10(1,2) 0

  表3出现了蓝色的值,并且括号内有两个值,这表示(4,3)顶点4到顶点3由上一次循环的(4--1,1--3)权值更新为(4--1,1--2,2--3)的权值,即5+2+3=10。这里就可以看出,中间点的用处。

表4
k=3

1 2 3 4
1 0 2 5(2) 4
2 10(3) 0 3 4(3)
3 7 9(1) 0 1
4 5 7(1) 10(1,2) 0
表5
k=4

1 2 3 4
1 0 2 5(2) 4
2 9(3,4) 0 3 4(3)
3 6(4) 8(1,4) 0 1
4 5 7(1) 10(1,2) ·0

  通过观察表2~5,可以发现FW算法的几个特点:1、如果中间点刚好是当前起点或终点,那么它的值就是直连或者不变;2、如果图中没有自圈,那么顶点A到顶点A就是0,可以观察对角线;3、FW算法复杂度是O(V^3),即顶点数的三次方,是一个比较庞大的量;4、此算法当然也可以用作求单源最短路,如果在时间和量小的情况下;5、对于负圈的情况,FW算法也可以求解。

  代码实现

int d[MAX_V][MAX_V];   //d[u][v]表示边e=(u,v)的权值(不存在时设为INF,不过d[i][i]=0)
int V;       //顶点数

void warshall_floyd(){
    for(int k = 0; k < V; k++)
        for(int i = 0; i < V; i++)
            for(int j = 0; j < V; j++)
                d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}

2、Bellman-Ford

  BF算法是用来求单源最短路问题的解法之一,不过通常我们只要求指定两点之间的最短路。但要知道指定两点之间的最短路,必须先求出从某点出发到其他所有点的最短路,才能获得两点之间的最短路。因此两种求解的复杂度一致,所以都统称为单源最短路问题,也就是从某点出发,到其他所有点的最短路。

  BF算法思想从连接边(顶点之间的连边)出发,在这些已知的连边中,不断找出某点与源点之间的最短路,先看下面的代码实现

  1.   首先确定源点S,给它默认赋值为0,然后在所有边里找以源点为出发点的边。
  2.   找到后,作比较:是否 “出发点的值+边的权值” 小于 “连接点的值”。
  3.   如果是,就给“ 连接点” 赋值=出发点的值+边的权值,并做更新标记。
  4.   在所有边里寻找出发点(必须是被更新过的点)

    做完步骤1后,不断循环2、3、4,直到没有更新标记。

 说明:

  •   步骤1是很重要的,因为它确定了,所有找到的最短路是从哪个点出发的。
  •   步骤2是核心思想之一,举个例子吧:点A可以直接连接源点S,且边SA的权值=8,所以点A的值为8;这时遍历找到了点B,点B也可以直接连接源点S,被赋值为4;然后作比较,发现点B到点A的权值为2,那么点B的值+边的权值=4+2=6,比点A的值要小,意思就是相比较源点S直接连接点A而言,不如源点S先连接点B,再从点B连接点A,所形成的路径更短。
  •   步骤3是核心思想之二,赋值很明白,就是更新路径;而做更新标记的意义在于:如果再也找不到可以更新路径的点,就代表没有较优的路径了,也就是源点到其他所有点的最短路已经找齐,退出循环即可。
  •   步骤4的意义和步骤1交相辉映,步骤1确定所有找到的路径,最开始的出发点是条件给的源点S;而步骤4就是在源点出发后,连接其他点,并从这些点再去寻找其他的点,像是一个接龙游戏。

下面通过代码来实际演示BF算法的思想,结合文字描述,会更加清晰

代码实现

struct edge{
    int from;   //出发点
    int to;     //连接点
    int cost;   //边的权值
};

edge es[MAX_E];     //用结构体数组存储所有边

int d[MAX_V];   //用数组存储到每个顶点的最短路,而至于是从哪个顶点出发的最短路,关键看S是谁
int V,E;        //V是顶点数,E是边数

//求解从顶点s出发到所有点的最短路径
void shortest_path(int s){
    for(int i = 0; i < V; i++)
        d[i] = INF;     //INF是一个极大值,默认到每个顶点的最短路是极大值,在求最短路的情况下
    d[s] = 0;   //给源点S的最短路赋值为零,也就是S的值为0,这是步骤1
    while(true){
        bool update = false;    //更新标记,如果update没有更新,则退出循环
        for(int i = 0; i < E; i++){     //步骤4
            edge e = es[i];
            if(d[e.from] != INF && d[e.to] > d[e.from] + e.cost){   //步骤2
                d[e.to] = d[e.from] + e.cost;       //步骤3
                update = true;
            }
        }
        if(!update)
            break;
    }
}

  最后关于BF算法再讲下负圈问题,如果图带有负圈求单元最短路径,我们的做法是找出带负圈的点,然后排除它,求剩下点的单源最短路。而BF算法有检查负圈的思想,具体见下面代码。

检查负圈代码实现

//如果返回true则存在负圈
bool find_negative_loop(){
    memset(d,0,sizeof(d));     //默认所有最短路赋值为0
    
    for(int i = 0; i < V; i++){    //循环所有顶点
        for(int j = 0; j < E; j++){    //每次循环所有边
            edge e = es[j];    
            if(d[e.to] > d[e.from] + e.cost){    //如果等式成立,则说明e.cost是负数,即有负圈
                d[e.to] = d[e.from] + e.cost;
                if(i == V-1)        //如果有负圈出现,那么循环次数将大于v-1
                    return true;
            }
        }
    }
    return false;
}

3、Dijkstra

  迪杰斯特拉算法是对BF算法的改进,因为BF算法在做更新时,会不断地检查一遍从i出发的所有边,十分耗时。但在没有负圈的情况下,这种检查方式可以优化,以免重复的检索。迪杰斯特拉算法的思想就是:1、从所有顶点中,找到最短距离已经确定的顶点,然后从它出发,更新相邻顶点的最短距离;2、然后将已经确定的顶点放到一边,不用再看。

  那么一开始,我们确定要找源点S的单源最短路,那么我们将从源点S出发。点S对于到自己的最短路是确定的,为0。那么按照迪的思想,先从源点S更新其相邻顶点的最短距离,然后再从这些相邻顶点中寻找一条最短的路径,将它视为下一个出发点,然后就可以“抛弃”源点S了,从而进入下一次循环。

  所以算法思想比较直观,但证明就略了。于是我们通过算法思想,就要确定实现的方式:1、用什么数据结构存储?2、用什么结构查找?一开始很直接,就用数组存储和查找就好了。这样做是没问题的,但会出现一些时间和空间过多的耗费。1、因为我们要不断在已更新、未确定最短路的顶点中,寻找一个比较短距离的顶点。那么就会通过循环,一一查找。若我们可以在结构里直接找到最小,也就是自动排序就好了;2、在更新过程中,如果用一维数组存储,那么有些边会被重复访问。要是能够从出发点,直接获得它所连接的其他点,会很不错。

  针对第一个问题,我们找到用队列来查找,并且还是优先队列priority_queue,因为队列可以弹出元素,那么我们将用过的顶点,即已经确定最短路的顶点弹出队列,就不会再查找它用到它了;其次优先队列有自动排序的特点,可以从小到大,也能从大到小。

  接下来是第二个问题,要想通过一个顶点的坐标,来确认它有哪些可连接的其他点,就需要用到邻接表啦,所以我们用vector来存储边的值。

代码实现

#include <iostream>
#include <algorithm>
#include <stdio.h>
#include <queue>
#include <vector>
using namespace std;

const int MAX_V(10000);     //极限顶点数
const int INF(1<<21);       //极大值

struct edge{
    int to;     //连接点
    int cost;   //权值
};
typedef pair<int, int> P;   //first是最短距离,second是顶点的编号

int V;      //顶点数
vector<edge> G[MAX_V];      //邻接表存储边的值
int d[MAX_V];   //数组存储最短路

void dijkstra(int s){
    priority_queue<P, vector<P>, greater<P> > que;   //greater<P>为从小到大,less<P>从大到小,重载方式
    fill(d, d+V, INF);      //初始化最短路数组,默认为极大值,在最短路情况下
    d[s] = 0;       //将源点的最短路优先赋值为零,以确认所有最短路的出发点
    que.push(P(0,s));       //将源点的最短距离,记忆编号推进优先队列
    
    while(!que.empty()){    //如果队列里没有元素,则说明所有顶点都被使用过了
        P p = que.top();    //通过优先队列自动排序的特点,从尚未使用过的顶点中选择一个距离最小的顶点
        que.pop();      //然后弹出它,以表示已经被使用
        int v = p.second;   //取顶点的编号
        if(d[v] < p.first)  //如果当前顶点的最短路,大于上一个顶点的最短路,就重新取点
            continue;
        for(int i = 0; i < G[v].size(); i++){   //取得新的距离最小的顶点
            edge e = G[v][i];
            if(d[e.to] > d[v] + e.cost){    //比较它所有可以到达顶点中最小的距离
                d[e.to] = d[v] + e.cost;
                que.push(P(d[e.to], e.to));     //将这个顶点最小的距离,和下一个顶点编号存入优先队列
            }
        }
    }
}

猜你喜欢

转载自blog.csdn.net/li13168690086/article/details/81913886