图的最短路径问题 Dijkstra+ Floyd

前言

博客记录基础算法设计中求图最短路径的两种经典算法,迪杰斯特拉算法(Dijkstra)和弗洛伊德算法(Floyd)。

Dijkstra算法

算法归类:贪心算法
应用领域:不含负权重的图
解决问题:图的单源最短路径问题
算法效率:O(|V|2) 采用权重矩阵表示图,无序数组表示优先队列;O(|E|log|V|)采用邻接链表表示图,最小堆表示优先队列

算法思想

算法属于贪心算法。
将顶点集合V分为两组,S 已经求出顶点的集合(初始时只含有源点V0)和T (T=V-S)尚未确定的顶点集合。
将T中的顶点按照递增的次序加入到S中,保证:1.从源点V0到S中其他各顶点的长度都不大于从V0到T中任意顶点的最短路径长度;2.每个顶点对应一个距离值。S中的顶点:从V0到此顶点的长度;T中的顶点:从V0到次顶点的只包括S中顶点作为中间顶点的最短路径长度。
依据:可以证明V0到T中顶点Vk的,或是从V0到Vk的直接路径的权值;或是从V0经S中顶点到Vk的路径权值之和。

算法设计(邻接矩阵表示图,无序数组表示优先队列)
#include<iostream>
#define maxv 101
#define maxe 100
#define INF 1000000
using namespace std;

void Dij(int arc[][maxv],int n,int path[maxv]){
	//定义源点到其他节点的路径的最小值 
	int shortest[maxv];
	//利用flag标记区分 S和 T集合 S为1 T为0 
	int finished[maxv];
	
	for(int i=0;i<n;i++){
		//定义T集合 
		finished[i]=0;
		//V0节点到其他节点的最短路径 均为V0到Vi的数值 
		shortest[i]=arc[0][i];
		//定义中间顶点路径 
		path[i] = -; 
	} 
	//将V0加入到S中
	finished[0]=1; 
	//临时标记 
	int k;
	for(int i=1;i<n;i++){
		int min = INF;
		for(int j=0;j<n;j++){
			if(!finished[j]&&min>shortest[j]){
				min = shortest[j];
				k = j;
			} 
		}
		//将T中距离V0最近的下一个节点找到 并加入到S中 
		finished[k] = 1;
		
		for(int j=0;j<n;j++){
			if(!finished[j]&&shortest[j]>min+arc[k][j]){
				//通过K顶点的方案更短 修正路径方案 
				shortest[j] = min+arc[k][j];
				//记录中间路径
				path[j] = k; 
			}
		}	
	}
	//输出格式的控制 
	for(int i=1;i<n;i++){
		int j=path[i];
		cout<<"0-"<<i<<" 距离值:"<<shortest[i]<<" 经由:"<<i<<" "<<j;
		while(j){
			cout<<" "<<path[j];
			j= path[j];			
		} 
		cout<<endl;
	}
} 
/*
int main(){
	int n,m;
	int arc[maxv][maxv];
	int path[maxv];
	
	cin>>n>>m;
	for(int i=0;i<n;i++)
		for(int j=0;j<n;j++){
			if(i==j) 
				arc[i][j] = 0;
			else
				arc[i][j] = INF;
		}
	int u,v,w;
	for(int i=0;i<m;i++){
		cin>>u>>v>>w;
		arc[u][v] = w;
	}
	Dij(arc,n,path);
	return 0;
} 

带权有向图 
6 9
0 2 5
0 3 30
1 0 2
1 4 8
2 5 7
2 1 15
4 3 4
5 3 10
5 4 18

输出结果
从0到1距离是:20   0->2->1
从0到2距离是: 5   0->2
从0到3距离是:22   0->2->5->3
从0到4距离是:28   0->2->1->4
从0到5距离是:12   0->2->5 
*/

/*
int main(){
	//输入为 图的邻接矩阵存储方式 
	int n,arc[maxv][maxv];
	//定义一个存储路径的path矩阵 
	int path[maxv];
	//这里 n是该图的顶点个数 
	cin>>n;
	for(int i=0;i<n;i++)
		for(int j=0;j<n;j++)
			cin>>arc[i][j];
	Dij(arc,n,path);
	return 0;
} 
输入带权无向图 
5
0 3 1000000 7 1000000
3 0 4 2 1000000
1000000 4 0 5 6 
7 2 5 0 4 
1000000 1000000 6 4 0

输出结果:0作为起始点 
0-1 距离值:3 经由:1 0
0-2 距离值:7 经由:2 1 0
0-3 距离值:5 经由:3 1 0
0-4 距离值:9 经由:4 3 1 0
*/

运行结果下图:
在这里插入图片描述

上述算法中算法的效率为O(n^2) ,这里可以利用最小堆的方法来对优先队列进行优化,使效率达到O(mlogn),这里m指的是图的边,n指的是图的顶点。稀疏图的情况下效率更高。(代码参考刘汝佳《算法竞赛入门经典第2版》)将整个的dijkstra算法封装成一个结构体,可以直接调用,重用效率较高!

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
#include<stack>
#include<map>
#include<sstream>
using namespace std;

typedef long long ll;
const int maxn = 1e3 + 10;
const int INF = 1 << 30;
int T, n, m;

struct Edge{
	int from,to,dist;
	Edge(int u,int v, int d):from(u),to(v),dist(d) {} 
}; 

struct HeapNode{
	int d,u;//d为距离 u为起点 
	bool operator<(const HeapNode& rhs) const{
		return d>rhs.d;//这样优先队列先取出d小的
	}
};

struct Dijkstra{
	int n,m;//n个顶点 m条边 
	vector<Edge> edges;//存边的信息
	vector<int> G[maxn];//G[i]表示起点为i的边的序号集
	bool done[maxn];//是否已经永久标号 
	int d[maxn];//S点到个顶点的距离 
	int p[maxn];//最短路中的上一条弧  逆向记录路径 
	
	void init(int n){
		this->n = n;
		for(int i=0; i<n; i++) G[i].clear();
		edges.clear();
	} 
	
	void AddEdge(int from,int to,int dist){
		edges.push_back(Edge(from,to,dist));
		m = edges.size();
		G[from].push_back(m-1);//存以from为起点的下一条边
	}
	//以s为起点 获取最短路径 
	void dijkstra(int s){
		priority_queue<HeapNode> Q;
		for(int i=0;i<n;i++) d[i] = INF;
		d[s] = 0;
		memset(done,0,sizeof(done));
		memset(p,-1,sizeof(p));
		Q.push((HeapNode){0,s});
		while(!Q.empty()){
			HeapNode now = Q.top();
			Q.pop();
			int u= now.u;//当前起点
			if(done[u]) continue;//如果已经加入集合,continue
			done[u] = true;
			for(int i=0;i<G[u].size();i++){
				Edge& e = edges[G[u][i]];//引用节省代码
				if(d[e.to]>d[u]+e.dist){
					d[e.to] = d[u]+e.dist;
					p[e.to] = G[u][i];//记录e.to前的边的编号,p存的是边的下标,这样可以通过边找出之前的点以及每条路的路径,如果用邻接矩阵存储的话这里可以直接存节点u
					Q.push((HeapNode){d[e.to],e.to});
				}
			}
		}
	}
	//输出控制
	void output(int u)
    {
        for(int i = 0; i < n; i++)
        {
            if(i == u)continue;
            printf("从%d到%d距离是:%2d   ", u, i, d[i]);
            stack<int>q;//存的是边的编号
            int x = i;//x就是路径上所有的点
            while(p[x] != -1)
            {
                q.push(x);
                x = edges[p[x]].from;//x变成这条边的起点
            }
            cout<<u;
            while(!q.empty())
            {
                cout<<"->"<<q.top();
                q.pop();
            }
            cout<<endl;
        }
    } 
};
//定义一个结构体变量 
Dijkstra demo;

int main(){
	//输入该图的顶点数目和边的数目 
	cin>>n>>m;
	demo.init(n);
	for(int i=0;i<m;i++){
		// 一条边的起点 终点 权值 
		int u,v,w;
		cin>>u>>v>>w;
		demo.AddEdge(u,v,w);		
	} 
	int u = 0;//定义源点为 0节点
	demo.dijkstra(u);
	demo.output(u);
	return 0;
} 
/*
带权有向图 
6 9
0 2 5
0 3 30
1 0 2
1 4 8
2 5 7
2 1 15
4 3 4
5 3 10
5 4 18

输出结果
从0到1距离是:20   0->2->1
从0到2距离是: 5   0->2
从0到3距离是:22   0->2->5->3
从0到4距离是:28   0->2->1->4
从0到5距离是:12   0->2->5 
*/
 

运行结果
在这里插入图片描述

Floyd算法

算法归类:动态规划
应用领域:不含负权重的图
解决问题:图的单源最短路径问题
算法效率:O(n^3)

算法思想

算法属于动态规划类算法。
通过一个图的权值矩阵求出它的每两点间的最短路径矩阵。
从图的带权邻接矩阵A=[a(i,j)] n×n开始,递归地进行n次更新,即由矩阵D(0)=A,按一个公式,构造出矩阵D(1);又用同样地公式由D(1)构造出D(2);……;最后又用同样的公式由D(n-1)构造出矩阵D(n)。矩阵D(n)的i行j列元素便是i号顶点到j号顶点的最短路径长度,称D(n)为图的距离矩阵,同时还可引入一个后继节点矩阵path来记录两点间的最短路径。
采用松弛技术(松弛操作),对在i和j之间的所有其他点进行一次松弛。所以时间复杂度为O(n^3)。

状态转移方程: map[i,j]:=min{map[i,k]+map[k,j],map[i,j]};
map[i,j]表示i到j的最短距离,K是穷举i,j的断点,map[n,n]初值应该为0,或者按照题目意思来做。
当然,如果这条路没有通的话,还必须特殊处理,比如没有map[i,k]这条路。

算法设计
#include<iostream>
#define INF 1000000
#define maxv 101
#define maxe 100
using namespace std;
void Floyd(int cost[][101],int n)
{
	int A[maxv][maxv];
	int path[maxv][maxv];
	for(int i=0;i<n;i++)
	{
		for(int j=0;j<n;j++)
		{
			A[i][j]=cost[i][j]; 
			if(A[i][j]==INF)
				path[i][j]=-1;
			else
				path[i][j]=i;
		}
	}
	for(int k=0;k<n;k++)
		for(int i=0;i<n;i++)
			for(int j=0;j<n;j++)
			{
				if(A[i][j]>A[i][k]+A[k][j])
				{
					A[i][j]=A[i][k]+A[k][j];
					path[i][j]=k;
				}
			}
	//输出结果
	cout<<"矩阵每行的结果:"<<endl; 
	for(int i=0;i<n;i++)
	{
		for(int j=0;j<n;j++)
			cout<<"Start node:"<<i<<" "<<"End node:"<<j<<" "<<"Dis:"<<A[i][j]<<endl;	
		cout<<endl;
	}
	cout<<"整个矩阵的计算结果:"<<endl; 
	for(int i=0;i<n;i++){
		for(int j=0;j<n;j++)
			cout<<A[i][j]<<"	";
		cout<<endl;
	} 
	 
		
}
int main()
{
	int cost[maxv][maxv];
	int n;
	while(cin>>n)
	{
		for(int i=0;i<n;i++)
			for(int j=0;j<n;j++)
				cin>>cost[i][j];
		//调用Floyd算法 
		Floyd(cost,n); 
	}
	
	return 0;
}
/*
6
0 35 18 7 23 42
8 0 22 34 1000000 21
24 19 0 40 27 11
1000000 8 32 0 16 38
19 25 10 28 0 16
36 17 1000000 12 5 0
*/

运行结果
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值