最短路径

1. 单源最短路问题(Dijkstra 算法)

算法代码

伪代码

//G为图,一般设为全局变量,数组d为源点到达各个点的最短路径长度,s为起点
Dijkstra(G, d[], s) {
    初始化;
    for(循环n次) {
        u = 使d[u]最小且还未被访问的顶点的标号;  //暴力搜索 or 堆结构
        标记u已被访问;
        for(从u出发能到达的所有顶点v) {
            if (v未被访问 && 以u为中介点 使 s到顶点v的最短距离d[v]更优) {
                优化d[v];
	            //可以在此处保存路径 把u保存为v的前驱即可
	            pre[v] = u;
            }
        }
    }//for
}//Dijkstra

实现差异: 主要区别主要在于如何枚举从u出发到达的顶点v上;邻接矩阵需要枚举所有结点查看顶点v能否到达u,而邻接表则可以直接得到这些顶点v;

注: 若题目所给为无向图,把它转化为两条有向边即可!

const int maxn = 1010;
const int INF = 0x7fffffff;

int n; //结点数
int d[maxn]; //起点到各顶点的最短距离
bool vis[maxn] = {false}; //标记数组,标记是否已经访问
int pre[maxn]; //保存结点前驱,用于获取最短路径

邻接矩阵版(O(n^2))

int G[maxn][maxn]; //顶点数,图

//邻接矩阵版本
void Dijkstra(int s) {
    fill(d, d + maxn, INF); //初始为不可达(慎用memset)
    d[s] = 0;
    
    for(int i = 0; i < n; i++) { //循环n次(第一次找到的肯定是起点本身,正好完成初始化)

	     //找到未访问结点中d[]最小的 
        int u = -1, MIN = INF;
        for(int j = 0; j < n; j++) {
            if(vis[j] == false && d[j] < MIN) {
                u = j;
                MIN = d[j];
            }
        }

        if(u == -1)  //找不到小于INF的d[u],说明剩下的顶点和起点s不连通
            return;

        vis[u] = true; //标记已访问
        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] = G[u][v] + d[u];
	            pre[v] = u;
            }
        }

    }//for - i
}//Dijkstra

邻接表版(O(V^2+E))

struct node{
    int v, dis; //v为边的目标结点,dis为边权
};
vector<node> Adj[maxn]; //邻接表; Adj[u]保存从u出发能到达的所有顶点(结构体中还保存了其间的边权)

//邻接表版本
void Dijkstra(int s) {
    fill(d, d + maxn, INF);
    d[s] = 0;
    for(int i = 0; i < n; i++) {
        
        int u = -1, MIN = INF;
        for(int j = 0; j < n; j++) {
            if(vis[j] == false && d[j] < MIN) {
                u = j;
                MIN = d[j];
            }
        }
        
        if(u == - 1) return;
        
        vis[u] = true;
        for(int j = 0; j < Adj[u].size(); j++){ //注意vector<>保存的是结构体
            int v = Adj[u][j].v; 
            if(vis[v] == false && Adj[u][j].dis + d[u] < d[v]){
                d[v] = d[u] + Adj[u][j].dis;
	            pre[v] = u;
            }
        }
        
    }//for - i
}//Dijkstr

注意点

(1)路径保存

新增一个数组pre[]pre[v]表示最短路径上v的前驱;
每次在更新优化时 把u保存为v的前驱即可(见伪代码);
Dijkstra()结束后,从目标点DFS()回溯即可得到最短路径;

void DFS(int s, int v) {
    if(v == s){ //如果已经到达起点,则输出并返回
        printf("%d\n", s);
        return;
    }
    DFS(s, pre[v]);  //回溯
    printf("%d\n", v); //等返回后在输出
}

(2)算法优化

Dijkstra 优化
最外层的循环O(V)是无法避免的,但是寻找最小距离d[u]可以用堆结构优化,是内部复杂度降到O(logV),整体复杂度可以到O(VlogV + E)
【堆结构可以直接用STLpriority_queue实现】

(3)算法变体

很多时候最短路径不止一条,就需要题目所给的其他条件选择其中一条;一般有一下三种考法:
1、给每条边再增加一个边权(比如花费),然后要求最短路径有多条时,选择花费之和最小的;
2、给每个点增加一个点权,有多条最短路径时,选择点权之和最大(最小)的;
3、直接问有多少条最短路径;
这三种出法都只需增加一个数组,存放新增的边权或点权或最短路径条数然后在Dijkstra()中修改 更新d[v] 的那一步操作即可;其他无需改变;

//考法一:边增加花费
int cost[maxn][maxn]; //存储边的额外信息
int c[maxn];   //存储到每个点最短路径的累计花费
for(int v = 0; v < n; v++) {
    if(vis[v] == false && G[u][v] != INF) {
        if(d[u] + G[u][v] < d[v]){
            d[v] = G[u][v] + d[u];
            c[v] = cost[u][v] + c[u];
        } else if (d[u] + G[u][v] == d[v] && c[u] + cost[u][v] < c[v]) { //最短距离相同时,若花费更小,则更新c[v]
            c[v] = c[u] + cost[u][v]; 
        }
    }     
}

//考法二:顶点增加权值
int weight[maxn]; //存储每个点的权值
int w[maxn]; //存储到每个点最短路径的累计权重
for(int v = 0; v < n; v++) {
     if(vis[v] == false && G[u][v] != INF) {
         if(d[u] + G[u][v] < d[v]){
             d[v] = G[u][v] + d[u];
             w[v] = weight[v] + w[u];
         } else if (d[u] + G[u][v] == d[v] && w[u] + weight[v] > w[v]) {
             w[v] = w[u] + weight[v]; //最短距离相同时,若权重更大,则更新w[v]
         }
     }     
 }

//考法三:输出最短路径条数
int num[maxn]; //记录到每个点的最短路径条数
for(int v = 0; v < n; v++) {
     //如果v未访问 && u能到达v && 以u为中介点可以是d[v]更优
     if(vis[v] == false && G[u][v] != INF) {
         if(d[u] + G[u][v] < d[v]){
             d[v] = G[u][v] + d[u];
             num[v] = num[u];
         } else if (d[u] + G[u][v] == d[v]) {
            num[v] += num[u]; //最短距离相同时,累加num!!!
         }
     }     
 }

DIJKSTRA+DFS模版(A(1030))

用Dijkstra + DFS组合解题的情景是解脱出在Dijikstra时要处理的逻辑较为复杂,这里的说的复杂逻辑不是DIjkstra本身,Dijkstra的框架是非常简洁优美且代码好写的。

如果只在Dijkstra中记录所有最短路径,然后再在DFS中根据其他标尺求出最优路径,这种做法也非常符合关注点分离的思想。

#include <cstdio>
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
const int maxn = 505;
vector<int> pre[maxn];
int G[maxn][maxn],C[maxn][maxn], d[maxn];
bool vis[maxn] = {false};
int n, m, si, di;
int INF = 1e8;
int opt = INF;
vector<int> path,temp;

void dij(int s) {
    fill(d, d+maxn, INF);
    d[s] = 0;

    for(int i = 0; i < n; i++) { //遍历n次,每次取一个点
        //选择最近点
        int u = -1, MIN = INF;
        for(int j = 0; j < n; j++) {
            if(!vis[j] && d[j] < MIN) {
                u = j;
                MIN = d[j];
            }
        }
        if(u == -1) return;
        vis[u] = true;
        //更新最短距离 & 花费
        for(int v = 0; v < n; v++) {
            if(!vis[v] && G[u][v] != INF) {
                if(d[u] + G[u][v] < d[v]){
                    d[v] = d[u] + G[u][v];
                    pre[v].clear();
                    pre[v].push_back(u);
                } else if(d[u] + G[u][v] == d[v]) {
                    pre[v].push_back(u);
                }
            }
        }//for - v
    }//for - i

}//Dijkstra



void DFS(int v){
    if(v == si){ // 这里s是路径的起点
        temp.push_back(v);
        int value = 0;
        //(1)求边权和
        for(int i=temp.size()-1; i>0; i--) {//tempPath路径是倒的
            int id = temp[i], idNext = temp[i-1];
            value += C[id][idNext];
        }
        if(value < opt){
            opt = value;
            path = temp;
        }
        temp.pop_back();
        return;
    }

    temp.push_back(v);
    for(int i = 0;i < pre[v].size();i++){
        DFS(pre[v][i]);
    }
    temp.pop_back();
}

int main() {
    cin >> n >> m >> si >> di;
    int q, w, e, r;
    fill(G[0], G[0] + maxn * maxn, INF);
    fill(C[0], C[0] + maxn * maxn, INF);

    for(int i = 0; i < m; i++){
        cin >> q >> w >> e >> r;
        G[q][w] = e;
        G[w][q] = e;
        C[q][w] = r;
        C[w][q] = r;
    }
    dij(si);
    DFS(di);
    for(int i = path.size() - 1; i >= 0 ; i--){
        cout << path[i] << " ";
    }
    cout << d[di] << " " << opt;
    return 0;
}

应用练习

Emergency(A1003)

Travel Plan(A1030)

Public Bike Management(A1018)

All Roads Lead to Rome(A1087)

2. 单源最短路问题(SPFA算法)(O(ke))

很多时候,给定的图存在负权边,这时类似Dijkstra等算法便没有了用武之地,而Bellman-Ford算法的复杂度又过高,SPFA算法便派上用场了。SPFA的复杂度大约是O(kE),k是每个点的平均进队次数(一般的,k是一个常数,在稀疏图中小于2)。

但是,SPFA算法稳定性较差,在稠密图中SPFA算法时间复杂度会退化。

实现方法:建立一个队列,初始时队列里只有起始点,在建立一个表格记录起始点到所有点的最短路径(该表格的初始值要赋为极大值,该点到他本身的路径赋为0)。然后执行松弛操作,用队列里有的点去刷新起始点到所有点的最短路,如果刷新成功且被刷新点不在队列中则把该点加入到队列最后。重复执行直到队列为空。

此外,SPFA算法还可以判断图中是否有负权环,即一个点入队次数超过N,如果出现了负权环,则求解失败。

#include "bits/stdc++.h"

using namespace std;
const int maxN = 200010;
struct Edge {
    int to, next, w;
} e[maxN];

int n, m, cnt, p[maxN], Dis[maxN];
int In[maxN];
bool visited[maxN];

void Add_Edge(const int x, const int y, const int z) {
    e[++cnt].to = y;
    e[cnt].next = p[x];
    e[cnt].w = z;
    p[x] = cnt;
    return;
}

bool Spfa(const int S) {
    int i, t, temp;
    queue<int> Q;
    memset(visited, 0, sizeof(visited));
    memset(Dis, 0x3f, sizeof(Dis));
    memset(In, 0, sizeof(In));

    Q.push(S);
    visited[S] = true;
    Dis[S] = 0;

    while (!Q.empty()) {
        t = Q.front();
        Q.pop();
        visited[t] = false;
        for (i = p[t]; i; i = e[i].next) {
            temp = e[i].to;
            if (Dis[temp] > Dis[t] + e[i].w) {
                Dis[temp] = Dis[t] + e[i].w;
                if (!visited[temp]) {
                    Q.push(temp);
                    visited[temp] = true;
                    if (++In[temp] > n)return false;
                }
            }
        }
    }
    return true;
}

int main() {
    int S, T;

    scanf("%d%d%d%d", &n, &m, &S, &T);
    for (int i = 1; i <= m; ++i) {
        int x, y, _;
        scanf("%d%d%d", &x, &y, &_);
        Add_Edge(x, y, _);
    }

    if (!Spfa(S)) printf("FAIL!\n");
    else printf("%d\n", Dis[T]);

    return 0;
}

3. 全源最短路问题(Floyd算法)(O(n^3))

算法理论

伪代码

//伪代码
枚举顶点 k ∈ [1, n]
    以顶点k作为中介点,枚举所有顶点对i和j (i∈[1,n], j∈[1,n])
        如果dis[i][k] + dis[k][j] < dis[i][j]成立
            赋值dis[i][j] = dis[i][k] + dis[k][j]

邻接矩阵版

const int INF = 10000000;
const int MAXV = 200;
int n, m;  //n为顶点数,m为边数
int dis[MAXV][MAXV];  //dis[i][j]表示顶点i和顶点j的最短距离
void Floyd(){
    for(int k=0; k<n; k++){
        for(int i=0; i<n; i++){
            for(int j=0; j<n; j++){
                if(dis[i][k]!=INF && dis[k][j]!=INF && dis[i][k] + dis[k][j] < dis[i][j]){//*****
                    dis[i][j] = dis[i][k] + dis[k][j];  //找到更短的路径
                }
            }
        }
    }
}
//最后输出dis[][]数组

需要注意的是,对于Floyd来说,k必须放在最外面。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值