图论(二):图的四种最短路径算法

本文总结了图的几种最短路径算法的实现:深度或广度优先搜索算法,弗洛伊德算法,迪杰斯特拉算法,Bellman-Ford算法

1),深度或广度优先搜索算法(解决单源最短路径)
从起始结点开始访问所有的深度遍历路径或广度优先路径,则到达终点结点的路径有多条,取其中路径权值最短的一条则为最短路径。

下面是核心代码:

void dfs(int cur, int dst){
    /***operation***/  

    /***operation***/
    if(minPath < dst) return;//当前走过路径大于之前最短路径,没必要再走下去
    if(cur == n){//临界条件
        if(minPath > dst) minPath = dst;
        return;
    }
    else{
        int i;
        for(i = 1; i <= n; i++){
            if(edge[cur][i] != inf && edge[cur][i] != 0 && mark[i] == 0){
                mark[i] = 1;
                dfs(i, dst+edge[cur][i]);
                mark[i] = 0;  //需要在深度遍历返回时将访问标志置0
            }
        }
        return;
    }
}  

例1:下面是城市的地图,注意是单向图,求城市1到城市5的最短距离。(引用的是上次总结的图论(一)中1)的例2)

/***先输入n个结点,m条边,之后输入有向图的m条边,边的前两元素表示起始结点,第三个值表权值,输出1号城市到n号城市的最短距离***/
/***算法的思路是访问所有的深度遍历路径,需要在深度遍历返回时将访问标志置0***/
#include <iostream>
#include <iomanip>
#define nmax 110
#define inf 999999999
using namespace std;
int n, m, minPath, edge[nmax][nmax], mark[nmax];//结点数,边数,最小路径,邻接矩阵,结点访问标记
void dfs(int cur, int dst){
    /***operation***/  

    /***operation***/
    if(minPath < dst) return;//当前走过路径大于之前最短路径,没必要再走下去
    if(cur == n){//临界条件
        if(minPath > dst) minPath = dst;
        return;
    }
    else{
        int i;
        for(i = 1; i <= n; i++){
            if(edge[cur][i] != inf && edge[cur][i] != 0 && mark[i] == 0){
                mark[i] = 1;
                dfs(i, dst+edge[cur][i]);
                mark[i] = 0;
            }
        }
        return;
    }
}  

int main(){
    while(cin >> n >> m && n != 0){
        //初始化邻接矩阵
        int i, j;
        for(i = 1; i <= n; i++){
            for(j = 1; j <= n; j++){
                edge[i][j] = inf;
            }
            edge[i][i] = 0;
        }
        int a, b;
        while(m--){
            cin >> a >> b;
            cin >> edge[a][b];
        }
        //以dnf(1)为起点开始递归遍历
        memset(mark, 0, sizeof(mark));
        minPath = inf;
        mark[1] = 1;
        dfs(1, 0);
        cout << minPath << endl;
    }
    return 0;
}  

程序运行结果如下:

2),弗洛伊德算法(解决多源最短路径):时间复杂度O(n^3),空间复杂度O(n^2)
基本思想:最开始只允许经过1号顶点进行中转,接下来只允许经过1号和2号顶点进行中转......允许经过1~n号所有顶点进行中转,来不断动态更新任意两点之间的最短路程。即求从i号顶点到j号顶点只经过前k号点的最短路程。

分析如下:1,首先构建邻接矩阵Floyd[n+1][n+1],假如现在只允许经过1号结点,求任意两点间的最短路程,很显然Floyd[i][j] = min{Floyd[i][j], Floyd[i][1]+Floyd[1][j]},代码如下:

for(i = 1; i <= n; i++){
	for(j = 1; j <= n; j++){
		if(Floyd[i][j] > Floyd[i][1] + Floyd[1][j])
			Floyd[i][j] = Floyd[i][1] + Floyd[1][j];
	}
}

2,接下来继续求在只允许经过1和2号两个顶点的情况下任意两点之间的最短距离,在已经实现了从i号顶点到j号顶点只经过前1号点的最短路程的前提下,现在再插入第2号结点,来看看能不能更新更短路径,故只需在步骤1求得的Floyd[n+1][n+1]基础上,进行Floyd[i][j] = min{Floyd[i][j], Floyd[i][2]+Floyd[2][j]};......
3,很显然,需要n次这样的更新,表示依次插入了1号,2号......n号结点,最后求得的Floyd[n+1][n+1]是从i号顶点到j号顶点只经过前n号点的最短路程。故核心代码如下:

#define inf 99999999
for(k = 1; k <= n; k++){
	for(i = 1; i <= n; i++){
		for(j = 1; j <= n; j++){
			if(Floyd[i][k] < inf && Floyd[k][j] < inf && Floyd[i][j] > Floyd[i][k] + Floyd[k][j])
				Floyd[i][j] = Floyd[i][k] + Floyd[k][j];
		}
	}
}

例1:寻找最短的从商店到赛场的路线。其中商店在1号结点处,赛场在n号结点处,1~n结点中有m条线路双向连接。

/***先输入n,m,再输入m个三元组,n为路口数,m表示有几条路其中1为商店,n为赛场,三元组分别表起点,终点,该路径长,输出1到n的最短路径***/
#include <iostream>
using namespace std;
#define inf 99999999
#define nmax 110
int edge[nmax][nmax], n, m;
int main(){
	while(cin >> n >> m && n!= 0){
		//构建邻接矩阵
		int i, j;
		for(i = 1; i <= n; i++){
			for(j = 1; j <= n; j++){
				edge[i][j] = inf;
			}
			edge[i][i] = 0;
		}
		while(m--){
			cin >> i >> j;
			cin >> edge[i][j];
			edge[j][i] = edge[i][j];
		}
		//使用弗洛伊德算法
		int k;
		for(k = 1; k <= n; k++){
			for(i = 1; i <= n; i++){
				for(j = 1; j <= n; j++){
					if(edge[i][k] < inf && edge[k][j] < inf && edge[i][j] > edge[i][k] + edge[k][j])
						edge[i][j] = edge[i][k] + edge[k][j];
				}
			}
		}
		cout << edge[1][n] << endl;
	}
	return 0;
}

程序运行结果如下:

3),迪杰斯特拉算法(解决单源最短路径)
基本思想:每次找到离源点(如1号结点)最近的一个顶点,然后以该顶点为中心进行扩展,最终得到源点到其余所有点的最短路径。
基本步骤:1,设置标记数组book[]:将所有的顶点分为两部分,已知最短路径的顶点集合P和未知最短路径的顶点集合Q,很显然最开始集合P只有源点一个顶点。book[i]为1表示在集合P中;
2,设置最短路径数组dst[]并不断更新:初始状态下,令dst[i] = edge[s][i](s为源点,edge为邻接矩阵),很显然此时dst[s]=0,book[s]=1。此时,在集合Q中可选择一个离源点s最近的顶点u加入到P中。并依据以u为新的中心点,对每一条边进行松弛操作(松弛是指由结点s-->j的途中可以经过点u,并令dst[j]=min{dst[j], dst[u]+edge[u][j]}),并令book[u]=1;
3,在集合Q中再次选择一个离源点s最近的顶点v加入到P中。并依据v为新的中心点,对每一条边进行松弛操作(即dst[j]=min{dst[j], dst[v]+edge[v][j]}),并令book[v]=1;
4,重复3,直至集合Q为空。
以下是图示:

核心代码如下所示:

#define inf 99999999
/***构建邻接矩阵edge[][],且1为源点***/
for(i = 1; i <= n; i++) dst[i] = edge[1][s];
for(i = 1; i <= n; i++) book[i] = 0;
book[1] = 1;
for(i = 1; i <= n-1; i++){
	//找到离源点最近的顶点u,称它为新中心点
	min = inf;
	for(j = 1; j <= n; j++){
		if(book[j] == 0 && dst[j] < min){
			min = dst[j];
			u = j;
		}
	}
	book[u] = 1;
	//更新最短路径数组
	for(k = 1; k <= n; k++){
		if(edge[u][k] < inf && book[k] == 0){
			if(dst[k] > dst[u] + edge[u][k])
				dst[k] = dst[u] + edge[u][k];
		}
	}
}

例1:给你n个点,m条无向边,每条边都有长度d和花费p,给你起点s,终点t,要求输出起点到终点的最短距离及其花费,如果最短距离有多条路线,则输出花费最少的。
输入:输入n,m,点的编号是1~n,然后是m行,每行4个数 a,b,d,p,表示a和b之间有一条边,且其长度为d,花费为p。最后一行是两个数s,t;起点s,终点 t。n和m为 0 时输入结束。(1<n<=1000, 0<m<100000, s != t)
输出:输出一行,有两个数, 最短距离及其花费。
分析:由于每条边有长度d和花费p,最好构建边结构体存放,此外可以使用邻接链表,使用邻接链表时需要将上面的核心代码修改几个地方:

1,初始化dst[]时使用结点1的邻接链表;
2,更新最短路径数组时,k的范围由1~n变为1~edge[u].size()。先采用邻接矩阵解决此题,再使用邻接表解决此题,两种方法的思路都一样:初始化邻接矩阵或邻接链表,并
初始化最短路径数组dst ----> n-1轮边的松弛中,先找到离新源点最近的中心点u,之后根据中心点u为转折点来更新路径数组。

使用邻接矩阵求解:

/***对于无向图,输入n,m,点的编号是1~n,然后是m行,每行4个数 a,b,d,p,表示a和b之间有一条边,且其长度为d,花费为p。最后一行是两个数s,t;起点s,终点 t。***/
/***n和m为 0 时输入结束。(1<n<=1000, 0<m<100000, s != t)     输出:输出一行,有两个数, 最短距离及其花费。***/
#include <iostream>
#include <iomanip>
using namespace std;
#define nmax 1001
#define inf 99999999
struct Edge{
	int len;
	int cost;
};
Edge edge[nmax][nmax];
int dst[nmax], spend[nmax], book[nmax], n, m, stNode, enNode;
int main(){
	while(cin >> n >> m && n != 0 && m != 0){
		int a, b, i, j;
		//构建邻接矩阵和最短路径数组
		for(i = 1; i <= n; i++){
			for(j = 1; j <= n; j++){
				edge[i][j].cost = 0;
				edge[i][j].len = inf;
			}
			edge[i][i].len = 0;
		}
		while(m--){
			cin >> a >> b;
			cin >> edge[a][b].len >> edge[a][b].cost;
			edge[b][a].len = edge[a][b].len;
			edge[b][a].cost = edge[a][b].cost;
		}
		cin >> stNode >> enNode;
		for(i = 1; i <= n; i++){
			dst[i] = edge[stNode][i].len;
			spend[i] = edge[stNode][i].cost;
		}
		memset(book, 0, sizeof(book));
		book[stNode] = 1;
		//开始迪杰斯特拉算法,进行剩余n-1次松弛
		int k;
		for(k = 1; k <= n-1; k++){
			//找离源点最近的顶点u
			int minNode, min = inf;
			for(i = 1; i <= n; i++){
				if(book[i] == 0 && min > dst[i] /* || min == dst[i]&& edge[stNode][min].cost > edge[stNode][i].cost*/){
					min = dst[i];
					minNode = i;
				}
			}
			//cout << setw(2) << minNode;
			book[minNode] = 1;//易错点1,错写成book[i]=1
			//以中心点u为转折点来更新路径数组和花费数组
			for(i = 1; i <= n; i++){
				if(book[i] == 0 && dst[i] > dst[minNode] + edge[minNode][i].len || dst[i] == dst[minNode] + edge[minNode][i].len && spend[i] > spend[minNode] + edge[minNode][i].cost){
					dst[i] = dst[minNode] + edge[minNode][i].len;//易错点2,错写成dst[i]+
					spend[i] = spend[minNode] + edge[minNode][i].cost;
				}
			}
		}
		cout << dst[enNode] << setw(3) << spend[enNode] << endl;
	}
	return 0;
}

程序运行结果如下:

使用邻接链表求解:

/***对于无向图,输入n,m,点的编号是1~n,然后是m行,每行4个数 a,b,d,p,表示a和b之间有一条边,且其长度为d,花费为p。最后一行是两个数s,t;起点s,终点 t。***/
/***n和m为 0 时输入结束。(1<n<=1000, 0<m<100000, s != t)     输出:输出一行,有两个数, 最短距离及其花费。***/
#include <iostream>
#include <iomanip>
#include <vector>
using namespace std;
#define nmax 1001
#define inf 99999999
struct Edge{
	int len;
	int cost;
	int next;
};
vector<Edge> edge[nmax];
int dst[nmax], spend[nmax], book[nmax], n, m, stNode, enNode;
int main(){
	while(cin >> n >> m && n != 0 && m != 0){
		int a, b, i, j;
		//构建邻接表和最短路径数组
		for(i = 1; i <= n; i++) edge[i].clear();
		while(m--){
			Edge tmp;
			cin >> a >> b;
			tmp.next = b;
			cin >> tmp.len >> tmp.cost;
			edge[a].push_back(tmp);
			tmp.next = a;
			edge[b].push_back(tmp);
		}
		cin >> stNode >> enNode;
		for(i = 1; i <= n; i++) dst[i] = inf; //注意2,别忘记写此句来初始化dst[]
		for(i = 0; i < edge[stNode].size(); i++){//注意1,从下标0开始存元素,误写成i <= edge[stNode].size()
			dst[edge[stNode][i].next] = edge[stNode][i].len;
			//cout << dst[2] << endl;
			spend[edge[stNode][i].next] = edge[stNode][i].cost;
		}
		memset(book, 0, sizeof(book));
		book[stNode] = 1;
		//开始迪杰斯特拉算法,进行剩余n-1次松弛
		int k;
		for(k = 1; k <= n-1; k++){
			//找离源点最近的顶点u
			int minnode, min = inf;
			for(i = 1; i <= n; i++){
				if(book[i] == 0 && min > dst[i] /* || min == dst[i]&& edge[stnode][min].cost > edge[stnode][i].cost*/){
					min = dst[i];
					minnode = i;
				}
			}
			//cout << setw(2) << minnode;
			book[minnode] = 1;//易错点1,错写成book[i]=1
			//以中心点u为转折点来更新路径数组和花费数组
			for(i = 0; i < edge[minnode].size(); i++){
				int t = edge[minnode][i].next;//别忘了加此句,表示与结点minnode相邻的点
				if(book[t] == 0 && dst[t] > dst[minnode] + edge[minnode][i].len || dst[t] == dst[minnode] + edge[minnode][i].len && spend[t] > spend[minnode] + edge[minnode][i].cost){
					dst[t] = dst[minnode] + edge[minnode][i].len;
					spend[t] = spend[minnode] + edge[minnode][i].cost;
				}
			}
		}
		cout << dst[enNode] << setw(3) << spend[enNode] << endl;
	}
	return 0;
}

程序运行结果如下:


使用邻接表时,注意更新dst[],book[]时要使用邻接表元素对应下标中的next成员,而涉及到权值加减时时需要使用邻接表中的对应下标来取得权值;而使用邻接矩阵就没这么多顾虑了,因为这时候邻接矩阵对应下标和dst[]要更新元素的下标正好一致,都是从1开始编号。

4),Bellman-Ford算法(解决负权边,解决单源最短路径,前几种方法不能求含负权边的图)::时间复杂度O(nm),空间复杂度O(m)
主要思想:对所有的边进行n-1轮松弛操作,因为在一个含有n个顶点的图中,任意两点之间的最短路径最多包含n-1边。换句话说,第1轮在对所有的边进行松弛后,得到的是从1号顶点只能经过一条边到达其余各定点的最短路径长度。第2轮在对所有的边进行松弛后,得到的是从1号顶点只能经过两条边到达其余各定点的最短路径长度,......
以下是图示:

此外,Bellman_Ford还可以检测一个图是否含有负权回路:如果在进行n-1轮松弛后仍然存在dst[e[i]] > dst[s[i]]+w[i]。算法核心代码如下:

#define inf 999999999
for(i = 1; i <= n; i++) dst[i] = inf;
dst[1] = 0;
for(k = 1; k <= n-1; k++){
	for(i = 1; i <= m; i++){
		if(dst[e[i]] > dst[s[i]] + w[i])
			dst[e[i]] = dst[s[i]] + w[i];
	}
}
//检测负权回路
flag = 0;
for(i = 1; i <= m; i++){
	if(dst[e[i]] > dst[s[i]] + w[i])
		flag = 1;
}
if(flag) cout << "此图含有负权回路";

例1:对图示中含负权的有向图,输出从结点1到各结点的最短路径,并判断有无负权回路。

/***先输入n,m,分别表结点数和边数,之后输入m个三元组,各表起点,终点,边权,输出1号结点到各结点的最短路径****/
#include <iostream>
#include <iomanip>
using namespace std;
#define nmax 1001
#define inf 99999999
int n, m, s[nmax], e[nmax], w[nmax], dst[nmax];
int main(){
	while(cin >> n >> m && n != 0 && m != 0){
		int i, j;
		//初始化三个数组:起点数组s[],终点数组e[],权值数组w[],最短路径数组dst[]
		for(i = 1; i <= m; i++)
			cin >> s[i] >> e[i] >> w[i];
		for(i = 1; i <= n; i++)
			dst[i] = inf;
		dst[1] = 0;
		//使用Bellman_Ford算法
		for(j = 1; j <= n-1; j++){
			for(i = 1; i <= m; i++){
				if(dst[e[i]] > dst[s[i]] + w[i])
					dst[e[i]] = dst[s[i]] + w[i];
			}
		}
		//测试是否有负权回路并输出
		int flag = 0;
		for(i = 1; i <= m; i++)
			if(dst[e[i]] > dst[s[i]] + w[i])
				flag = 1;
		if(flag) cout << "此图含有负权回路\n";
		else{
			for(i = 1; i <= n; i++){
				if(i == 1)
					cout << dst[i];
				else
					cout << setw(3) << dst[i];
			}
			cout << endl;
		}
	}
	return 0;
}

程序运行结果如下:

时间: 2024-10-16 15:13:49

图论(二):图的四种最短路径算法的相关文章

PHP四种基础算法详解

许多人都说 算法是程序的核心,一个程序的好于差,关键是这个程序算法的优劣.作为一个初级phper,虽然很少接触到算法方面的东西 .但是对于冒泡排序,插入排序,选择排序,快速排序四种基本算法,我想还是要掌握的. 需求:分别用 冒泡排序法,快速排序法,选择排序法,插入排序法将下面数组中 的值按照从小到的顺序进行排序. $arr=array(11,3,56,62,21,66,32,78,36,76,39,88,34); 1.冒泡排序 介绍: 冒泡排序(Bubble Sort,台湾译为:泡沫排序或气泡排

PHP四种基础算法详解(从小到大)

许多人都说 算法是程序的核心,一个程序的好于差,关键是这个程序算法的优劣.作为一个初级phper,虽然很少接触到算法方面的东西 .但是对于冒泡排序,插入排序,选择排序,快速排序四种基本算法,我想还是要掌握的. 需求:分别用 冒泡排序法,快速排序法,选择排序法,插入排序法,归并排序将下面数组中 的值按照从小到大的顺序进行排序. $arr=array(11,3,56,62,21,66,32,78,36,76,39,88,34); 1.冒泡排序 介绍: 冒泡排序(Bubble Sort)是一种简单的排

带负权图的单源最短路径算法:Bellman-Ford算法

算法简介 前面介绍过图的单源最短路径算法Dijkstra算法,然而Dijkstra算法无法判断含负权边的图的最短路.如果遇到负权,在没有负权回路存在时(负权回路的含义是,回路的权值和为负.)即便有负权的边,也可以采用Bellman-Ford算法正确求出最短路径. Bellman-Ford算法能在更普遍的情况下(存在负权边)解决单源点最短路径问题.对于给定的带权(有向或无向)图G=(V,E),其源点为s,加权函数 w是 边集 E 的映射.对图G运行Bellman-Ford算法的结果是一个布尔值,表

php四种基础算法:冒泡,选择,插入和快速排序法

转自:http://www.php100.com/html/php/rumen/2013/1029/6333.html 许多人都说 算法是程序的核心,一个程序的好于差,关键是这个程序算法的优劣.作为一个初级phper,虽然很少接触到算法方面的东西 .但是对于冒泡排序,插入排序,选择排序,快速排序四种基本算法,我想还是要掌握的.下面是我按自己的理解,将四个方法分析一遍. 需求:分别用 冒泡排序法,快速排序法,选择排序法,插入排序法将下面数组中 的值按照从小到的顺序进行排序. $arr(1,43,5

四种排序算法PHP实现类

四种排序算法的PHP实现:1) 插入排序(Insertion Sort)的基本思想是: 每次将一个待排序的记录,按其关键字大小插入到前面已经排好序的子文件中的适当位置,直到全部记录插入完成为止. 2) 选择排序(Selection Sort)的基本思想是: 每一趟从待排序的记录中选出关键字最小的记录,顺序放在已排好序的子文件的最后,直到全部记录排序完毕. 3) 冒泡排序的基本思想是: 两两比较待排序记录的关键字,发现两个记录的次序相反时即进行交换,直到没有反序的记录为止. 4) 快速排序实质上和

php四种基础算法:冒泡,选择,插入和快速排序法PHP基础教程

许多人都说 算法是程序的核心,一个程序的好于差,关键是这个程序算法的优劣.作为一个初级phper,虽然很少接触到算法方面的东西.但是对于冒泡排序,插入排序,选择排序,快速排序四种基本算法,我想还是要掌握的.下面兄弟连PHP培训 小编将四个方法分析一遍. 需求:分别用 冒泡排序法,快速排序法,选择排序法,插入排序法将下面数组中 的值按照从小到的顺序进行排序. $arr(1,43,54,62,21,66,32,78,36,76,39); 1. 冒泡排序法 * 思路分析:法如其名,就是像冒泡一样,每次

四种GC算法

四种GC算法 GC垃圾回收,首先需要判断哪些对象需要回收 判断对象存活 可达性分析: ? 从GC Roots开始向下查询,如果一个对象到任何一个GC Root对象都没有一个引用链相连的话,说明此对象不可用. ? 四个GC Root对象 JVM栈中引用的对象 方法区静态属性引用的对象 方法区中常量引用的对象 本地栈中JNI中引用的对象 四种GC算法 引用计数: ? 每个对象都有一个引用计数属性,多一个引用+1,少一个引用-1,为0的时候代表可以回收. ? 致命缺点:无法解决循环引用 复制算法: ?

图论中四个最短路径算法

(一)单源最短路径算法 1. Dijksta算法 要求图G(V,E)的所有边的权重都为非负值. 运用了贪心算法的思想,但是较好地的是,其找到的解一定是最优解. 算法主要思想: 用数组d[]表示开始节点A到其余节点的路径长度:用w(u,v)表示节点u到v的权值,若两节点无直接路径,则该值为无穷大:矩阵Q保存每次循环每个节点的dv]值,总结点数为n. 初始时,开始节点到自身距离d[A]初值为0,到其余节点d[V]初值为无穷大. 循环:对每个节点进行判断,若d[v]大于d[u]+w(u,v),则令d[

UML中类图的四种关系及其代码实现

在uml图中 最复杂的也就是泛化,实现,依赖,关联,这四种关系了,如果弄清了这几种关系那么在理解UML图的时候就会变得轻车熟路了! 如果你对着几种关系一点都不熟悉的话可以看一下uml中的四种关系,这篇博客简单的介绍了一下这几种关系,本文将重点的介绍一下,这几种关系在代码里如何实现的! 泛化关系 我想这个也可能是最简单的关系了,泛化就是特殊到一半的过程,也就是继承的相反的过程,子类继承自父类,而父类是从子类泛化而来! 泛化(generalization)关系是一个类(称为子类.子接口)继承另外的一