Dijkstra算法解决最短路径问题


title: Dijkstra算法解决最短路径问题
date: 2020-01-07 16:18:25
tags: 数据结构

单源最短路径问题:计算源点到其他各个顶点的最短路径长度

全局最短路径问题:图中任意两个节点之间的最短路径

以上两个问题均可以归结为最短路径问题(事实上对每个结点进行求单源最短路径就可以解决全局最短路径)

Dijkstra算法用于解决单源最短路径,但是不能求带负边权的最短路径。对于Dijkstra算法的基本含义,可以参考浙大数据结构的慕课的讲解,本文主要解决如何编程实现,以及编程模板,以及变式。

Dijkstra + DFS用于打印最短路径

先上伪代码

//G[maxv][maxv]为图 设置为全局变量 dis为一维数组,为源点到各个顶点的最短路径长度(累积最优距离),s为起点
//inf为无穷大一般设置为999999999 maxv为顶点的个数(看题目要求)
Dijkstra(G, dis[], s) {
	初始化(一般G[i][j]任意两个点均为inf 以及对其他数据的初始化多用fill函数)
	for(循环n次n为题目给出的顶点个数 一般与输入值有关) 
	{
		u = 使得dis[u]为最小值的且还未被访问的顶点标号;
		//使用一个for循环找u
		记u已经被访问;	//用一个bool数组记录
		for(从u出发能够达到的所有顶点v) 
		{
			if(v未被访问过 && 以u为中介点使得s到顶点v的最短距离dis[v]更优)
            优化dis[v];
		}
	}
}

图有两种存储格式,邻接矩阵或者邻接表,主要区别在对v结点的寻找注意区别。

需要提前定义全局变量

const int maxv = 520;	//具体要看题目顶点数量的规模 一般要比最大的至少多20否则有可能会段错误
const int inf = 999999999;

接下来为两种模板:

①邻接矩阵版(笔者较为常用领接矩阵法):

//领接矩阵版
int n, G[maxv][maxv];	//n为顶点数量fill(G[0], G[0] + maxv * maxv, inf);对图的初始化注意二维数组fill的写法
int dis[maxv];	//当前起点到各个顶点累积最短距离
int pre[maxv];	//用于记录当前结点的前一个结点
bool vis[maxv] = {false};	//用于记录当前节点是否被访问过 vis[i] == true为访问过 初值均为未被访问

void Dijkstra(int s) {	//s为起点
    fill(dis, dis + MAXV, inf);	//fill函数将一维数组赋值为inf(注意一维数组的写法dis后不加括号)
	dis[s] = 0;	//起始点到自身的距离为0 务必牢记 为算法的起点
    for(int i = 0; i < n; i++) {	//循环n次
        int u = -1, min = inf;
        for(int j = 0; j < n; j++) {	//使得dis[u]为最小值的且还未被访问的顶点标号;
            if(vis[j] == false && dis[u] < min) {
                u = j;
                min = d[j];
            }
        }
        if(u == -1) return;	//如果没有找到小于inf的d[u] 说明剩下的剩余顶点和起点s不连通
        vis[u] = true;	//标记访问过u 即u已经被收进了已经访问的结点当中
        for(int v = 0; v < n; v++) {
            //如果v结点未被访问且经过u结点到达v可以使得dis[v]更加优(小)则更新dis[v]
            if(vis[v] == false && G[u][v] != inf) {
               if(dis[u] + G[u][v] < dis[v]) {	
                	dis[v] = dis[u] + G[u][v];
                   	pre[v] = u;	//v的前一个结点为u
            	}
                //此处是关键后序会修改此处进行有多个标准时修改或者添加更新结点规则
            }
        }
    }
}

②领接表版(主要区别在于存储使用vector 对v的更新上也有些不同)

struct Node {
	int v, d;		//v为目标顶点, Adj[i][j].d从i到Adj[i][j].v结点的边权 使用的存储结构不同
}
vector<Node> Adj[maxv];		//图G的领接表存储结构 Adj[u]存放从顶点u出发可以到达的所有顶点 Adj[u][j].v代表u可以到达该顶点标号 Adj[u][j].d为从i到Adj[i][j].v结点的边权
int n;	//n为顶点数目(输入数据中) maxv为最大顶点数目(题目规定的规模)
int dis[maxv];	//起点到各个顶点的最短距离长度(累积最短)
bool vis[maxv] = {false};	//标记数组 vis[i] == true 表示已经访问 初值为false
int pre[maxv];	//用于记录前一个结点

void Dijkstra(int s) {		//s为起点
    fill(dis, dis + maxv, inf);		//fill函数将整个d数组赋值为inf
    dis[s] = 0;	//起始结点为0 为递归起点
    for(int i = 0; i < n; i++) {
        int u = -1, min = inf;
        for(int j = 0; j < n; j++) {
            if(vis[j] == false && dis[j] < min) {
                u = j;
                min = dis[j];
            }
        }
        //找不到u说明与剩下结点不连通
        if(u == -1) return;
        vis[u] = true;	//标记本节点已经被访问 收入已访问结点中
        for(int j = 0; j < Adj[u].size(); j++) {	
            //此处遍历时直接获得u可以到达的结点为领接表优势注意从0-Adj[u].size();
            int v = Adj[u][j].v;
            if(vis[v] == false && dis[u] + Adj[u][j].d < dis[v]) {	//优化规则
                dis[v] = dis[u] + Adj[u][j].d;
                pre[v] = u;	//v的前一个结点为u
            }
        }
    }
}

两种结构打印最短路径pre[]数组中数据均使用dfs打印

void dfs(int s, int v) {	//s为起点 v为当前访问的顶点编号 我们从终点开始递归 因为pre中存的都是前一个结点当全局变量中存放了其实结点s则可以直接写void dfs(int v)
	if(v == s) {//如果当前已经到达起始结点s 则输出并且返回
		printf("%d\n", s);
		return;
	}
	dfs(s, pre[v]);	//递归访问v结点的前一个结点
	printf("%d\n", v);	//从最终起点层返回回来后执行上一层(次起点层)的顶点 就可以获得从起点开始的顺序序列
}

以下总结三种考察方法:两种标尺进行对多种路径的淘汰和筛选使得结果唯一。即对于题目有两种及以上的路径满足第一标尺时候,题目会给出第二个甚至更多等级的标尺进行对重复的路径筛选。以第二标尺为例,更多重可以类推。

对于第二标尺的出题方法,我们都只需要增加一个数组来存放新增的边权或者点权或者是最短路径的条数,然后只要在Dijkstra算法中修改对d[v]的步骤即可,其他不用动按模板写就可以。

常见三种类型:

①给每条边增加一个边权(cost 如距离为第一标尺 则添加路费为第二标尺),题目则要求在距离最短的路径中有多条的时候要求在路径上花费之和最小。(如果有其他边权含义也可以是总cost最大,例如可以添加在每条路径上路过的树的数量最多…)

实现的时候增加cost[maxv][maxv]代表代表u→v的花费图(题目输入)并且增加一个c[]用于存放从起点u到各个顶点的累积花费。(其实原理和距离的很像G[u][v]为距离图 dis[]为源点到各个顶点的累积距离)

**重点是对值的初始化问题:初始化时c[s]为0、其余c[u]为inf 对花费图cost[u][v]初试为inf 再把题目输入的去更新对应的数值即可(求花费最少为第二标尺时)。**代码如下:

for(int v = 0; v < n; v++) {
	//如果v未被访问 && u可以到达v
	if(vis[v] == false && G[u][v] != inf) {
       if(dis[u] + G[u][v] < dis[v]){
			dis[v] = dis[u] + G[u][v];		
			c[v] = c[u] + cost[u][v];//距离是第一标尺当第一标尺无冲突的时第二标尺无条件服从第一标尺
			pre[v] = u;
       }else if(d[u] + G[u][v] == d[v] && c[u] + cost[u][v] < c[v]){ 
        //最短距离相同的时候看是否可以让c[v]更佳可以则更新c[v]
			c[v] = c[u] + cost[u][v];
        	pre[v] = u;
	  }
    }
}

②新增点权。当给定每个点权(第二标尺) 要求在最短路径上有多条的时候要求点权最大。如每个顶点有物资,要求最短路径有多条的时候要求路径上点权之和最大(如果点权是其他含义也可以是最小)

增加一个点权的几何weigh[u]代表城市u的点权大小,并且增加w[]代表从源点到达目标城市收集的最大物资(最大累积点权和)

**初始化规则:只有w[s]为weight[s]其他均为w[u]=0。**更新规则如下展示

for(int v = 0; v < n; v++) {
    if(vis[v] == false && G[u][v] != inf) {
        if(dis[u] + G[u][v] < dis[v]) {
            dis[v] = dis[u] + G[u][v];
            w[v] = weight[v] + w[u];	//当第一标尺无冲突 无条件修改点权
            pre[v] = u;
        } else if(dis[u] + G[u][v] == dis[v] && weight[v] + w[u] > w[v]){
            w[v] = w[u] + weight[v];	//第一标尺最短距离相同的时候 可以使得w[v]更优更新
            pre[v] = u;
        }
    }
}

③新增求最短路径的条数。题目一般会直接问最短路径的条数为多少条。

只需要增加一个数组num[]令从起点s到达顶点u的最短路径的条数为num[u]

**初始化时只需要有num[s]=1、其余num[u]均为0.**即可在dis[u] + G[u][v] < dis[u]的时候更新dis[v]并且让num[v]继承num[u],而当d[u] + G[u][v] == d[v]的时候(最短路径相同的时候)将num[u]加到num[v]上。代码如下:

for(int v = 0; v < n; v++) {
	if(vis[v] == false && G[u][v] != inf) {
		if(dis[u] + G[u][v] < dis[v]) {
            d[v] = d[u] + G[u][v];
            num[v] = num[u];
        }else if(dis[u] + G[u][v] == dis[v]) {
            num[v] = num[v] + num[u];0
        }
	}
}

仔细的读者可能发现此处没有保存每个节点的前驱节点 因为有多条路径下文会介绍一种更好的方法来保存和输出所有最小路径(如果有多条的情况)

先来一个例子汇总一下这三个考法:

e.g. 要求路径最短 又要保证点权之和最大 而且还要输出最短路径的个数 还要输出路径 保证第二权重确定后路径唯一。(考题往往出的比这还复杂)

for(int v = 0; v < n; v++) {
    if(vis[v] == false && G[u][v] != inf) {
        if(dis[u] + G[u][v] < dis[v]) {
            dis[v] = dis[u] + G[u][v];
            w[v] = w[u] + weight[v];
            num[v] = num[u];
            pre[v] = u;
        } else if(dis[u] + G[u][v] == dis[v]) {
            num[v] = num[v] + num[u];
            if(w[v] < w[u] + weight[v]) {	//只有权重更优的时候才更新 题目保证了唯一
            	w[v] = w[u] + weight[v];
            	pre[v] = u;
            }
        }
    }
}

void printPath(int v) {	//逆序存放 递归逆序打印
    if(v == s) {
        printf("%d", v);
        return ;
    }
    printPath(pre[v]);
    printf("%d", v);
}

但这样无法获得多条路径(可能存在多条路径 这种编程方式只是题目确保了唯一)当有多条路径的时候要使用vector数组在存放。

也可以不这么麻烦(当然以上的是必须要牢固背下来理解下来的,但也可以使用。以下要介绍一种更为方便的思路Dijkstra+DFS。即用Dijkstra求出最短路径还有pre数组,然后使用深度优先遍历来求想要的一切,包括点权最大,边权最大、路径个数、打印数据

可能有多条路径所以Dijkstra算法中pre数组用vector存放pre[maxv]。以下写法分两步

①Dijkstra算法求最短路径 存储前驱节点在vector中

在Dijkstra函数中求前驱节点pre[maxv]此处保留相同长度的路径节点

vector<int> pre[maxv]; //事先需要声明 vector<int> pre[maxv];

//Dijkstra部分需要修改使得多种路径的方式存储在vector中
if(dis[u] + G[u][v] < dis[v]) {
    dis[v] = dis[u] + G[u][v];
    pre[v].clear();		//因为有更好的故淘汰原来的最优前驱点
    pre[v].push_back(u);
} else if(dis[i] + G[u][v] == dis[v]) {
    pre[v].push_back(u);	//当距离相同的时候有了相同距离的前驱节点
}

完整的Dijkstra函数如下(要会完整的写出来)

vector<int> pre[maxv];
void Dijkstra(int s) {		//s为起点
	fill(dis, dis + maxv, inf);		//距离第一标尺初始化
	dis[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 && dis[j] < min) {
				u = j;
				min = dis[j];
			}
		}
		if(u == -1) return;
		vis[u] = true;
		for(int v = 0; v < n; v++) {
			if(vis[v] == false && G[u][v] != inf) {
				if(dis[u] + G[u][v] < dis[v]) {
					dis[v] = dis[u] + G[u][v];
					pre[v].clear();	//清空v前驱
					pre[v].push_back(u);	//令v的前驱为u	
				} else if(dis[u] + G[u][v] == d[v]) {
					pre[v].push_back(u);	//相同距离的前驱节点多了一个 故令v的前驱节点添加一个u
				}
			}
		}
	}	
}

②求得了pre数组,就可以得知所有的最短路径,然后使用dfs去遍历所有的最短路径,找出一条使第二标尺最优的路径。

每次遍历到达叶子节点就会产生一条完整的最短路径。计算每一条的路径的第二标尺的值。并在每次遍历过程中保存最优的第二标尺值(不断更新最优标尺值),这样遍历完所有的路径就可以获得最优的第二标尺与最优的路径。

考虑些dfs递归函数:

三个部分

(1)作为全局变量的第二标尺最优值 optValue

(2)记录最优路径的数组path(使用vector来存储)因为vector可以直接赋值

(3)临时记录dfs遍历到达叶子节点时的路径tempPath(也用vector存储)

代码如下:

int optValue;	//第二标尺最优值
vector<int> pre[maxv];	//存放结点的前驱结点
vector<int> path, tempPath;	//最优路径 临时路径
void DFS(int v)	{	//v为当前访问结点
	//递归边界
    if(v == s) {	//如果到达s叶子节点(即路径的起始结点)
        tempPath.push_back(v);	//起始结点加入临时路径tempPath最后 到达起始点将v给亚茹堆栈tempPath中 
        int value = tempPath上的value值;
        if(value 优于 opValue) {
            opValue = value;
            path = tempPath;
        }
        tempPath.pop_back();	//刚刚加入的起始结点删除保证了每一层既把本层的结点push也把本层的结点pop
        return;
    }
    //递归式
    tempPath.push_back(v);
    for(int i = 0; i < pre[v].size(); i++) {
        dfs(pre[v][i]);	//pre[v][i]在下一层dfs中被push和pop
    }	//dfs保证了一路算到底到v == s 使得每一条都被算过
    tempPath.pop_back();	//pop为吧本层v结点pop
}

(1)对于递归边界。tempPath为保存一条完整的路径,如果访问到的本层为一个叶子结点就是当前路径的开始结点 说明到达递归边界 把起始结点vpush进入堆栈tempPath 此时tempPath就有了一条完整的路径,如果计算得到的value值大于最大值optValue则path = tempPath 保存路径维护path为为当前的最优路径,并且要把tempPath中最后一个结点(起始) 再return 来进行之后的递归。

(2)对于递归式,每一次为把当前访问的结点压入,然后找本层结点的pre[v][i]进行递归操作,递归完毕以后弹出最后一个结点。

(3)计算当前tempPath边权或者点权之和的代码:(不同value值的算法)

//在dfs中计算value值
//边权之和
int value = 0;	
for(int i = tempPath.size() - 1; i > 0; i--) {
    int id = tempPath[i], idnext = tempPath[i - 1];
    value += v[id][idnext];
}

//点权之和
int value = 0;
for(int i = tempPath.size() - 1; i >= 0; i--) {
    int id = tempPath[i];	//当前结点id
    value += weight[id];	//value增加结点id的点权。
}

最后以一个例子结束本篇文章:

题目: Travel Plan 
A traveler's map gives the distances between cities along the highways, together with the cost of each highway. Now you are supposed to write a program to help a traveler to decide the shortest path between his/her starting city and the destination. If such a shortest path is not unique, you are supposed to output the one with the minimum cost, which is guaranteed to be unique.

Input Specification:
Each input file contains one test case. Each case starts with a line containing 4 positive integers N, M, S, and D, where N (≤500) is the number of cities (and hence the cities are numbered from 0 to N−1); M is the number of highways; S and D are the starting and the destination cities, respectively. Then M lines follow, each provides the information of a highway, in the format:

City1 City2 Distance Cost
    
where the numbers are all integers no more than 500, and are separated by a space.

Output Specification:
For each test case, print in one line the cities along the shortest path from the starting point to the destination, followed by the total distance and the total cost of the path. The numbers must be separated by a space and there must be no extra space at the end of output.

Sample Input:
4 5 0 3
0 1 1 20
1 3 2 30
0 3 4 10
0 2 2 20
2 3 1 20

Sample Output:
0 2 3 3 40

第一种写法(传统直接Dijkstra 第二标尺也在Dijkstra中实现)

#include<iostream>
#include<cstdio>
#include<vector>
using namespace std;
const int inf = 999999999;
const int maxv = 520;

int n, m, s, d;		//n为城市的数量 m为高速公路的数量 s为开始的城市 d为结束的城市 
int G[maxv][maxv];
int w[maxv];
bool vis[maxv] = {false};
int cost[maxv][maxv];
int c[maxv];
int prepath[maxv];
vector<int> disprepath;
void dfsprepath(int vcity) {
	disprepath.push_back(vcity);
	if(vcity == s) return;
	dfsprepath(prepath[vcity]);
}

int main() {
	fill(G[0], G[0] + maxv * maxv, inf);
	fill(cost[0], cost[0] + maxv * maxv, inf);
	fill(w, w + maxv, inf);
	fill(c, c + maxv, inf);
	scanf("%d%d%d%d", &n, &m, &s, &d);
	for(int i = 0; i < m; i++) {
		int c1, c2, Dis, Cost;
		cin >> c1 >> c2 >> Dis >> Cost;
		G[c1][c2] = G[c2][c1] = Dis;
		cost[c1][c2] = cost[c2][c1] = Cost; 
	}
	w[s] = 0;
	c[s] = 0;
	for(int i = 0; i < n; i++) {
		prepath[i] = i;
	}
	for(int i = 0; i < n; i++) {
		int u = -1, min = inf;
		for(int j = 0; j < n; j++) {
			if(vis[j] == false && w[j] < min) {
				u = j;
				min = w[j];
			}
		}
		if(u == -1) break;
		vis[u] = true;
		for(int v = 0; v < n; v++) {
			if(vis[v] == false && G[u][v] < inf) {
				if(G[u][v] + w[u] < w[v]) {
					w[v] = G[u][v] + w[u];
					c[v] = cost[u][v] + c[u];
					prepath[v] = u;
				} else if (G[u][v] + w[u] == w[v] && cost[u][v] + c[u] < c[v]) {
					c[v] = cost[u][v] + c[u];
					prepath[v] = u;
				}
			}
		}
	} 
	dfsprepath(d);
	for(int i = disprepath.size() - 1; i >= 0; i--){
		printf("%d", disprepath[i]);
		if(i != 0) printf(" ");
	}
	printf(" %d %d", w[d], c[d]);
	return 0;
} 

第二种(Dijkstra+DFS 。Dijkstra中计算最短路径 在DFS中计算第二标尺筛选路径)

#include<iostream>
#include<vector>
#include<cstdio>
#include<algorithm>
using namespace std;
const int maxv = 520;
const int inf = 999999999;

int n, m, s, d;
int G[maxv][maxv];
int dis[maxv];
int cost[maxv][maxv];
int c[maxv];
int vis[maxv] = {false};
vector<int> prepath[maxv];
vector<int> path, temppath;
int optivalue = inf;

void Dijkstra(int st) {
	fill(dis, dis + maxv, inf);
	dis[st] = 0;
	for(int i = 0; i < n; i++) {
		int u = -1, min = inf;
		for(int j = 0; j < n; j++) {
			if(vis[j] == false && dis[j] < min) {
				u = j;
				min = dis[j];
			}
		}
		if(u == -1) return;
		vis[u] = true;
		for(int v = 0; v < n; v++) {
			if(vis[v] == false && G[u][v] != inf) {
				if(G[u][v] + dis[u] < dis[v]) {
					dis[v] = G[u][v] + dis[u];
					prepath[v].clear();
					prepath[v].push_back(u);
				}else if(G[u][v] + dis[u] == dis[v]) {
					prepath[v].push_back(u);	//多条路径 
				}
			}
		}
	}
}

void DFS(int v) {
	//递归边界 
	if(v == s) {
		temppath.push_back(v);	//起始结点入栈以后temppath中为完整的路径 
		int value = 0;	//边权最大值 
		for(int i = temppath.size() - 1; i >= 1; i--) {
			int c1, c2;
			c1 = temppath[i];
			c2 = temppath[i - 1];
			value = value + cost[c1][c2];
		} 
		if(value < optivalue) {
			path = temppath;	//为倒叙的序列
			optivalue = value;
		} 
		temppath.pop_back();
		return;
	}
	//递归式
	temppath.push_back(v);
	for(int i = prepath[v].size() - 1; i >= 0; i--) {
		DFS(prepath[v][i]);
	} 
	temppath.pop_back();
}

int main() {
	scanf("%d%d%d%d", &n, &m, &s, &d);
	fill(G[0], G[0] + maxv * maxv, inf);
	fill(cost[0], cost[0] + maxv * maxv, inf);
	for(int i = 0; i < m; i++) {
		int c1, c2, Dis, Cost;
		scanf("%d%d%d%d", &c1, &c2, &Dis, &Cost);
		G[c1][c2] = G[c2][c1] = Dis;
		cost[c1][c2] = cost[c2][c1] = Cost;
	}
	Dijkstra(s);	//获得前驱路径
	DFS(d); 
	for(int i = path.size() - 1; i >= 0; i--) {
		printf("%d", path[i]);
		if(i != 0) printf(" ");
	}
	printf(" %d %d", dis[d], optivalue);
	return 0;
} 

以上 有问题欢迎留言。码字不易 点个赞吧~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值