Loading

<数据结构>图的最短路径问题

最短路径问题

  1. 单源最短路径问题:Dijstra[不带负权边]\Bell_Ford(SPFA)[可带负权边]
  2. 多源最短路径问题:Floy

Dijstra算法:中介点优化

解决不带负权边的单源最短路问题
通过与源点s最短路径已知的点Vi为中介,优化Vi的邻接点与源点s的距离

基本步骤

MAXV:最大顶点数,源点s
数组d[MAXV]:各个顶点到源点s的距离。初始化为INF(无穷大,一般设为100,000,000),d[s]初始化为0。
数组vis[MAXV]:标记数组。 vis[Vi] == true 表示Vi结点已被访问过

  1. 取出未被标记过的顶点中与源点距离最短的顶点Vi,标记Vi
  2. 以Vi为中介点,更新Vi的邻接点到源点s的距离
  3. 将1.2循环n次(n为顶点数)

伪代码

//G为图,一般设成全局变量;数组d为源点到达各点的最短路径长度,s为起点
Dijstra(G, d[], s){
    初始化;
    for(循环n次){
        u = 使d[u]最小的还未被访问的顶点的标号;
        记u已被访问;
        for(从u出发能到达的所有顶点v){
            if(v未被访问 && 以u为中介点使s到顶点v的最短距离d[v]更优){
                优化d[v];
            }
        }
    }
}

在实现过程中的关键问题

  1. “u = 使d[u]最小的还未被访问的顶点的标号;”————>如何找到d[u]最小值
    遍历d:O(MAXV)
    将d构建为Min_Heap:O(logMAXV)
  2. "v未被访问 && 以u为中介点使s到顶点v的最短距离d[v]更优"————>前者用标记数组vis解决;后者即"d[u] + G[u][v] < d[v]"(邻接矩阵实现时)

代码实现

邻接矩阵版

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

#define MAXV 100      //最大顶点数
#define INF 100000000 //一个很大的数
int n, G[MAXV][MAXV]; //n为顶点数
int d[MAXV];         //起点到各点的最短路径长度
bool vis[MAXV] = {false};//标记数组,vis[i] == false 表示已访问。初值均为false

void Dijstra(int s){  //s为起点
    fill(d, d+MAXV*MAXV, INF);  //fill函数将整个d数组赋为INF(慎用memset)
    d[s] = 0;  //起点s到自身的距离为0
    for(int i = 0; i < n; i++){ //循环n次
        int u = -1, MIN = INF;  //u是d[u]最小,MIN存放最小的d[u]
        for(int j = 0; j < n; j++){ //找到未访问的顶点中d[u]最小的,时间复杂度为O(MAXV)
            if(vis[j] == false && d[j] < MIN){
                u = j;
                MIN = d[j];
            }
        }

        //找不到小于INF的 d[u], 说明剩下的顶点和起点s不连通
        if(u == -1) return ;
        vis[u] = true; //标记u为已访问
        for(int v = 0; v < n; v++){
            //如果v未访问 && u能到达v && 以u为中介点可以使d[v]更优
            if(vis[v] == false && G[u][v] != INF && d[u] + G[u][v] < d[v]){
                d[v] = d[u] + G[u][v];
            }
        }
    }
}

邻接表版

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

#define MAXV 100      //最大顶点数
#define INF 100000000 //一个很大的数
struct Node
{
    int v, dis;  //v表示目标顶点, dis表示边权
};
vector<Node> Adj[MAXV];

int n; //n为顶点数
int d[MAXV];         //起点到各点的最短路径长度
bool vis[MAXV] = {false};//标记数组,vis[i] == false 表示已访问。初值均为false

void Dijstra(int s){  //s为起点
    fill(d, d+MAXV*MAXV, INF);  //fill函数将整个d数组赋为INF(慎用memset)
    d[s] = 0;  //起点s到自身的距离为0
    for(int i = 0; i < n; i++){ //循环n次
        int u = -1, MIN = INF;  //u是d[u]最小,MIN存放最小的d[u]
        for(int j = 0; j < n; j++){ //找到未访问的顶点中d[u]最小的
            if(vis[j] == false && d[j] < MIN){
                u = j;
                MIN = d[j];
            }
        }
        //找不到小于INF的 d[u], 说明剩下的顶点和起点s不连通
        if(u == -1) return ;
        vis[u] = true; //标记u为已访问
        /**********************只有下面这个与邻接矩阵不同**************************/
        for(int i = 0; i < Adj[u].size(); i++){
            int v = Adj[u][i].v;   //通过邻接表直接获得u能到达的顶点v
            if(d[u] + Adj[u][v].dis < d[v]){
                d[v] = d[u] + Adj[u][v].dis; //优化d[v]
            }
        }
    }
}

时间复杂度:O(VlogV+E)

外循环:O(V)无法避免
影响因素:

  1. d[MAXV]最小值的获取方式
    遍历d:O(V)
    最小堆:O(logV)
  2. 图G的实现方式
    邻接矩阵:内循环O(V)
    邻接表:与外循环共同作用等价于遍历了所有边,故内外循环的总时间复杂度为O(E)

总结:

邻接表 邻接矩阵
遍历d O(V* V+E) O(V* (V+V))
最小堆 O(V* logV+E) O(V*(logV+V))

算法存在的问题:存在负权边时会失效

一个简单的例子:

在该例中,如果以A顶点为源点,那么A先被标。而后由于 A到B的距离-1 < A到C的距离1,故B到源点的最短距离更新为1,B被标记。

由于被标记过的顶点的距离值不会再被更新,所以根据Dijstra算法得出B到源点A的最短距离为-1,但实际上应该为-4,可见Dijstra算法在存在负权边的图中就出错了。

Bell_Ford和SPFA算法:遍历边优化

解决单源最短路问题,可检测负权边
存在负权边时函数返回false, 不存在时返回true且d[]存放源点到各顶点的最短路径长
Dijstra算法相比:Dijstra主要操作对象是结点,无法处理负权边;BF主要操作对象是边,可以通过一轮循环判断是否存在负权边

基本步骤

构建d[]数组

  1. 对边执行n-1轮操作
  2. 每轮操作遍历所有边
  3. 对每条边u->v,如果以u为中介使得d[v]更小,则更新d[v] 【边松弛】

寻找负权环
再对所有边执行一轮操作,判断是否仍有边满足 d[u]+G[u][v] < d[v]
有则说明存在负权边,无则说明不存在,d数组中所有值已达最优

伪代码

//构建d[]数组
for(int i = 0; i < n-1; i++){  //执行n-1轮操作,其中n为顶点数
    for(each edge u->v){  //每轮操作都遍历所有边
        if(d[u] + length[u->v] < d[v])  //以u为中介点可以使得d[v]更小
            d[v] = d[u] + length[u->v];  //松弛操作
    }
}

//检查负环
for(each edge u->v){  //对每条边进行判断
    if(d[u] + length[u->v] < d[v]){  //如果仍可以被松弛
        return false; //说明图中有从原点可达的负环
    }
    return true; //数组d的所有值已达最优
}

代码本质:构建最短路径树

  • 最短路径树:如果将从源点s到其他各点的最短路径连接起来,必然会构成一棵树(整体连通&&无环),该树称为最短路径树【别和最小生成树搞混了】。原图和源点一旦确定,最短路径树就唯一确定了
  • BF算法的每一轮循环实际上就是在确定树的一层,树的层数不超过V-1,故循环V-1次

代码实现

/*Bellman_Ford:可处理带有负权环的单源最短路径问题 O(V^2)*/
#include<stdio.h>
#include<vector>
#include<algorithm>
using namespace std;
#define MAXV 100
#define INF 100000000
struct Node{
    int v, dis;
};
vector<Node> Adj[MAXV];
int n;
int d[MAXV];

bool Bellman(int s){  //s为源点
    fill(d, d+MAXV, INF);
    d[s] = 0;
    for(int i = 0; i < n-1; i++){     //执行n-1 轮操作,n为顶点数
        for(int u = 0; u < n; u++){   //每轮操作都遍历所有边
            for(int j = 0; j < Adj[u].size(); j++){
                int v   = Adj[u][j].v;  //邻接边的顶点
                int dis = Adj[u][j].dis;//邻接边的边权
                if(d[u] + dis < d[v]){ //以u为中介点可以使d[v]更小
                    d[v] = d[u] + dis;
                }
            }
        }
    }

    for(int u = 0; u < n; u++){   //对每条边进行判断
        for(int j = 0; j < Adj[u].size(); j++){
            int v   = Adj[u][j].v;
            int dis = Adj[u][j].dis;
            if(d[u] + dis < d[v]){
                return false;    //说明图中有从源点可达的负权环
            }
        }
    }
    return true;
}

复杂度分析:O(VE)

外循环:执行V-1次,无法改变
内循环(遍历每条边):执行E次(邻接表),执行V^2(邻接矩阵)

邻接表 邻接矩阵
时间复杂度 O(VE) O(V^3)

优化:SPFA(Shortest Path Faster Algorithm)

切入点: 在BF算法中,当某个顶点u的d[u]值改变时,从它出发的边的邻接点v的d[v]值才有可能改变,此时无需遍历所有边。
优化: 建立一个队列,每次将队首元素u取出,对u的所有邻接边u->v进行松弛操作,如果某个顶点v的d[v]改变了,就将v加入队列中。
循环直到 队列为空||某元素v入队次数大于n-1 退出循环。此时,队列为空,说明d数组已经最优,否则,说明存在负权边。
注:入队元素最大入队次数为n-1次: 顶点v最多与n-2个结点连通,如果每次边松弛时v都入队,则入队次数为n-2,如果v由恰好是源点,在初始化时入队1次,则总共入队n-1次。

伪代码

queue<int> q;
源点s入队;
while(队列非空){
    取出队首元素u;
    for(u的所有邻接边u->v){
        if(d[u] + G[u][v] < d[v]){
            if(v不在队列){
                v入队;
                if(v入队次数大于n-1){
                    说明有可达负环;  return;
                }
            }
        }
    }
}

队列用stl的queue实现
v入队次数用数组num[MAXV]记录。(MAXV:最大顶点数)

代码实现:邻接表为例

/*基于Bellman-Ford的优化*/
/*无需遍历所有边,而只需遍历u的发出边(基于BFS思想)  O(kE)*/
#include<stdio.h>
#include<vector>
#include<queue>
#include<algorithm>
using namespace std;
#define MAXV 100
#define INF 100000000
struct Node{
    int v, dis;
};
vector<Node> Adj[MAXV];
int n, d[MAXV], num[MAXV]; //num数组记录入队次数
bool inq[MAXV]; //顶点是否在队列中

bool SPFA(int s){
    //初始化部分
    fill(inq, inq+MAXV, false);
    fill(num, num+MAXV, 0);
    fill(d, d+MAXV, INF);
    //源点入队部分
    queue<int> Q;
    Q.push(s);
    inq[s] = true;  //源点已入队
    num[s]++;       //源点入队次数+1
    d[0] = 0;       //源点的d值为0
    //主体部分
    while(!Q.empty()){
        int u = Q.front();  //队首顶点编号为u
        Q.pop();            //出队
        inq[u] = false;     //设置u不在队列中
        //遍历u所有的邻接边
        for(int j = 0; j < Adj[u].size(); j++){
            int v = Adj[u][j].v;
            int dis = Adj[u][j].dis;
            //松弛操作
            if(d[u] + dis < d[v]){
                d[v] = d[u] + dis;
                if(!inq[v]){   //如果v不在队列中
                    Q.push(v);  //v入队
                    inq[v] = true;  //设置v在队列中
                    num[v]++;  //v的入队次数+1
                    if(num[v] >= n) return false;    //有可达负环
                }
            }
        }
    }
    return true;    //无可达负环
}

复杂度分析:O(kE)

证略。k是一个常数,一般不超过2。

  • 经常优于堆优化的Dijstra算法
  • 存在负权环时退化为O(VE)

Floy算法:待添加

posted @ 2021-11-25 22:36  咪啪魔女  阅读(116)  评论(0编辑  收藏  举报