图--最短路径(四种算法详解)

最短路径

  • dijkstra算法
    • 邻接矩阵实现
    • 邻接表实现
    • 最小堆优化
    • 应用:多权值+多路径+路径输出
  • Floyd算法
  • Bellman ford算法
  • SPFA算法

dijkstra算法

单源最短路径算法
伪码描述:
图--最短路径(四种算法详解)_第1张图片
图--最短路径(四种算法详解)_第2张图片

邻接矩阵实现

  1. 基本代码
#include
using namespace std;
#define MAXSIZE 1000
#define NoEdge 1000
int n,m;
int G[MAXSIZE][MAXSIZE];  //权重
int dist[MAXSIZE];    //单源点最短路径 
bool visited[MAXSIZE];
int FindMinAdj()
{
	int min_i=-1,min=NoEdge;  //找离已经有的最短路径结点最近的点 
	for(int i=0;i<n;i++)
	{
		if(!visited[i]&&dist[i]<min)
		{
			min_i=i;min=dist[i];
		}
	}
	return min_i;
}
void Solve(int s)
{
	for(int i=0;i<n;i++)   //初始化就当做只有s结点到各结点的情况,考虑路径,路径个数等 
	{
		dist[i]=G[s][i];
		visited[i]=false;	
	}
	visited[s]=true;
	while(true)
	{
		int min_i=FindMinAdj();    //找离已经确定的最短路径最近的结点 
		if(min_i==-1) break;
	//	cout<
		visited[min_i]=true;
	
		for(int i=0;i<n;i++)
		{
			if(!visited[i]&&G[min_i][i]!=NoEdge)
			{
				if(dist[min_i]+G[min_i][i]<dist[i])   //最短路径优先考虑 
				{
					dist[i]=dist[min_i]+G[min_i][i];  //更新最短路径长度 
				}
			}
		}
	}
}
int main()
{
	int s;
	cin>>n>>m>>s;
	for(int i=0;i<n;i++)
	{
		for(int j=0;j<n;j++)
		G[i][j]=NoEdge;
	}
	for(int i=0;i<m;i++)
	{
		int v1,v2,len;cin>>v1>>v2>>len;
		G[v1][v2]=len;
		G[v2][v1]=len; 
	}
	Solve(s);  //求起点s到其他所有顶点的最短路径 
	return 0;
	
} 
  1. 时间复杂度:O(n^2)
  2. 用来求所有顶点每两个顶点间的最短距离,对每个顶点调用Dijistra,时间复杂度为O(n^3)

邻接表实现

1.代码

#include
using namespace std;
#define MAXSIZE 10005
#define INFI 100000000
int n,m;
typedef struct Node
{
	int id;
	int W;
}AdjNode,*Adj;
vector<Adj>List[MAXSIZE];  //vector数组实现邻接表 
int dist[MAXSIZE];
bool visited[MAXSIZE];
int FindMinAdj()
{
	int min_i=-1,min=INFI;  //找离已经有的最短路径结点最近的点 
	for(int i=0;i<n;i++)
	{
		if(!visited[i]&&dist[i]<min)
		{
			min_i=i;min=dist[i];
		}
	}
	return min_i;
}
void Dijistra()   
{
	for(int i=1;i<=n;i++)
	{
			for(int j=1;j<=n;j++) //初始化 
			{
				dist[j]=INFI;
				visited[j]=false;
			}
			dist[i]=0;
			
			while(true)  
			{
				int min_i=FindMinAdj();    //找离已经确定的最短路径最近的结点 
				if(min_i==-1) break;
				
				visited[min_i]=true;  //纳入集合 
				int len=List[min_i].size();
				for(int j=0;j<len;j++)   //判断该点受影响的邻接点 
				{
					int t=List[min_i][j]->id;
					if(!visited[t]&&dist[min_i]+List[min_i][j]->W<dist[t])
					{
						dist[t]=dist[min_i]+List[min_i][j]->W;
					}
				}
			} 
			
			for(int j=1;j<=n;j++)   
			{                        
				cout<<dist[j]<<" ";
			}
			
	}
}
int main()
{
	scanf("%d %d",&n,&m);
	for(int i=0;i<m;i++)
	{
		int u,v,w;
		//cin>>u>>v>>w;
		scanf("%d %d %d",&u,&v,&w);
		if(u==v) continue;
		Adj a=new AdjNode;  //无向图,两个顶点都要进行操作 
		a->id=v;
		a->W=w;
		List[u].push_back(a);
		Adj b=new AdjNode;
		b->id=u;
		b->W=w;
		List[v].push_back(b);
	}
	Dijistra(); 
	return 0;
}
  1. 时间复杂度:O(E+V)*V ----->O(n^2)
  2. 求多源最短路径,时间复杂度:O(E+V) * V * V----->O(n^3)

最小堆优化

1.代码

#include
using namespace std;
#define MAXSIZE 10005
#define INFI 100000000
int n,m;
typedef struct Node
{
	int id;
	int W;
}AdjNode,*Adj;
struct cmp{   //自定义优先级,返回true表示前者优先级更低(与排序刚好相反) 
    bool operator()(const Adj a,const Adj b){
        return a->W > b->W;
    }
};
vector<Adj>List[MAXSIZE];  //vector数组实现邻接表 
int dist[MAXSIZE];
bool visited[MAXSIZE];
priority_queue<Adj,vector<Adj>,cmp>que;   //按边权值从小到大排序队列 
void Dijistra()   
{
	for(int i=1;i<=n;i++)
	{
			for(int j=1;j<=n;j++) //初始化 
			{
				dist[j]=INFI;
				visited[j]=false;
			}
			dist[i]=0;
			Adj t=new AdjNode;
			t->id=i;
			t->W=0;
			que.push(t);      //将起点纳入最小堆中,堆顶元素为到当前到起点最近的点 
			while(!que.empty())  
			{
				Adj t_adj=que.top();  //获得堆顶元素(未收纳的顶点距离起点最近的点) 
				que.pop();   
				if(visited[t_adj->id]) continue;  //该点已经收纳到结果集合了,获取下一个 
				
				int min_i=t_adj->id;
				visited[min_i]=true;  //纳入集合 
				int len=List[min_i].size();
				for(int j=0;j<len;j++)   //判断该点受影响的邻接点 
				{
					int t=List[min_i][j]->id;
					if(!visited[t]&&dist[min_i]+List[min_i][j]->W<dist[t])
					{
						dist[t]=dist[min_i]+List[min_i][j]->W;
						Adj p=new AdjNode;
						p->id=t;
						p->W=dist[t];
						que.push(p);   //将离起点距离变短的点纳入最小堆 
					}
				}
			} 
			
			for(int j=1;j<=n;j++)   
			{                        
				cout<<dist[j]<<" ";
			}
			cout<<endl;
	}
}
int main()
{
	//cin>>n>>m;
	scanf("%d %d",&n,&m);
	for(int i=0;i<m;i++)
	{
		int u,v,w;
		//cin>>u>>v>>w;
		scanf("%d %d %d",&u,&v,&w);
		if(u==v) continue;
		Adj a=new AdjNode;  //无向图,两个顶点都要进行操作 
		a->id=v;
		a->W=w;
		List[u].push_back(a);
		Adj b=new AdjNode;
		b->id=u;
		b->W=w;
		List[v].push_back(b);
	}
	Dijistra();
	return 0;
}

  1. 时间复杂度:O(E+V)*logV
  2. 算多源最短路径: O(E+V)* logV *V

应用:多权值+多路径+路径输出

PTA题目链接:
https://pintia.cn/problem-sets/15/problems/862

#include
using namespace std;
#define MAXSIZE 1000
#define NoEdge 1000
int n,m;
int G[MAXSIZE][MAXSIZE];  //权重1 
int Num[MAXSIZE];     //权重2 
int dist[MAXSIZE];    //单源点最短路径 
int Cnt[MAXSIZE];     //记录最短路径的条数 
int Path[MAXSIZE];    //存储最短路径 
bool visited[MAXSIZE];
int Que[MAXSIZE];     //权重2最优 
int FindMinAdj()
{
	int min_i=-1,min=NoEdge;  //找离已经有的最短路径结点最近的点 
	for(int i=0;i<n;i++)
	{
		if(!visited[i]&&dist[i]<min)
		{
			min_i=i;min=dist[i];
		}
	}
	return min_i;
}
void Solve(int s,int d)
{
	for(int i=0;i<n;i++)   //初始化就当做只有s结点到各结点的情况,考虑路径,路径个数等 
	{
		dist[i]=G[s][i];
		if(G[s][i]!=NoEdge)
		{	
			Path[i]=s;
			Cnt[i]=1;
			Que[i]=Num[i]+Num[s];
		} 
		else
		{
			Cnt[i]=0;
			Que[i]=Num[i]; 
		} 
		visited[i]=false;	
	}
	Cnt[s]=1;
	visited[s]=true;
	while(true)
	{
		int min_i=FindMinAdj();    //找离已经确定的最短路径最近的结点 
		if(min_i==-1) break;
	//	cout<
		visited[min_i]=true;
	
		for(int i=0;i<n;i++)
		{
			if(!visited[i]&&G[min_i][i]!=NoEdge)
			{
				if(dist[min_i]+G[min_i][i]<dist[i])   //最短路径优先考虑 
				{
					dist[i]=dist[min_i]+G[min_i][i];  //更新最短路径长度 
					Que[i]=Que[min_i]+Num[i];         //更新最优权值2 
					Path[i]=min_i;       //更新最短路径 
					Cnt[i]=Cnt[min_i];   //更新最短路径条数 
				}
				else if(dist[min_i]+G[min_i][i]==dist[i])  //路径长度相等考虑第二个权值 
				{	
					Cnt[i]+=Cnt[min_i];  //更新最短路径条数 
					if(Que[min_i]+Num[i]>Que[i])
					{
						Que[i]=Que[min_i]+Num[i];
						Path[i]=min_i;   //更新最短路径 
					}
				}
			}
		}
}
	cout<<Cnt[d]<<" "<<Que[d]<<endl;
	int r=Path[d];
	cout<<d<<" ";
	while(r!=s)
	{
		cout<<r<<" ";
		r=Path[r];
	}
	cout<<s<<endl;
}
int main()
{
	int s,d;
	cin>>n>>m>>s>>d;
	for(int i=0;i<n;i++)
	cin>>Num[i];
	for(int i=0;i<n;i++)
	{
		for(int j=0;j<n;j++)
		G[i][j]=NoEdge;
	}
	for(int i=0;i<m;i++)
	{
		int v1,v2,len;cin>>v1>>v2>>len;
		G[v1][v2]=len;
		G[v2][v1]=len; 
	}
	Solve(d,s);
	return 0;
	
} 

Floyd算法

求多源最短路径的算法,只能用邻接矩阵实现

  1. 代码
#include
using namespace std;
#define INIFITE 10000
#define MAXSIZE 105
int n,m;
int G[MAXSIZE][MAXSIZE];
int Min_Path[MAXSIZE][MAXSIZE];  //记录i到j的最短路径值, 
int path[MAXSIZE][MAXSIZE];    //记录最短路径,递归输出最短路径 
bool Floyd()    //求任意两点之间的最短距离O(n^3),比对每一个点调用单源点最短路径算法快一点      
{
	for(int i=1;i<=n;i++)  //初始化 
	{
		for(int j=1;j<=n;j++)
		{
			Min_Path[i][j]=G[i][j];
			path[i][j]=j;  //初始化i->j一步到位
		}
		
	}
	
	for(int k=1;k<=n;k++)
	{
		for(int i=1;i<=n;i++)
		{
			for(int j=1;j<=n;j++)
			{
				if(i==j&&Min_Path[i][j]<0)   return false;  //发现负值圈,不能正常解决,返回错误标记 
				if(Min_Path[i][k]+Min_Path[k][j]<Min_Path[i][j])
				{
					Min_Path[i][j]=Min_Path[i][k]+Min_Path[k][j];
					path[i][j]=path[i][k];
				}
				
 			} 
			
		}
	}
	
	for(int i=1;i<=n;i++)  //输出最短路径值矩阵 
	{
		for(int j=1;j<=n;j++)
		{
			if(i==j) Min_Path[i][j]=INIFITE;
			cout<<Min_Path[i][j]<<" ";
		}
		cout<<endl;
	}
	
	cout<<"****"<<endl;
	for(int i=1;i<=n;i++)   //输出最短路径矩阵 
	{
		for(int j=1;j<=n;j++)
		cout<<path[i][j]<<" ";
		cout<<endl;
	}
	return true;
}
int main()
{
	cin>>n>>m;
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=n;j++)
		G[i][j]=INIFITE;
	}
	for(int i=0;i<m;i++)
	{
		int v1,v2,weight;
		cin>>v1>>v2>>weight;
		G[v1][v2]=weight;
		G[v2][v1]=weight;
	}
	if(Floyd()) cout<<"yes"<<endl;  //没有负权值 
	int v1,v2;cin>>v1>>v2;  //输出v1到v2的路径值和路径 
	while(v1&&v2)  //递归输出最短路径
	{
		
		cout<<v1<<"到"<<v2<<"的最短路径值为:"<<Min_Path[v1][v2]<<"   ";
	   // cout<";
		int k=v1;    //输出v1到v2的最短路径 
	    while(k!=v2)
	    {
	    	cout<<k<<"-->";
	    	k=path[k][v2];
		}
		cout<<v2<<endl;
		cin>>v1>>v2;
	}
	
	return 0;
}

  1. 算法复杂度:O(n^3) 无法进行优化

Bellman ford算法

求单源最短路径的算法
时间复杂度高,为O(E*V),比优化后的Dijistra算法差
bellman ford在dijistra算法不能用时才用,比如存在负权边,此时往往需要将其检测出来,如果有负权边,dijistra算法就会陷入负循环(Negative Cycles)中,因为每次总能找到一个更短的路径,就没法得到正确结果

  1. 基本思想:每次遍历所有的边,对每条边的到达点进行松弛,一共遍历顶点个数-1那么多次,双重循环,外层循环为顶点个数,内层循环遍历所有的边
    如果存在负权值,第一次操作后能得到基本的最短路径的数组,此时需要重复进行刚才的步骤,判断是否存在负权值,但是在松弛步骤时不进行更新,而是将能进行更新的点的距离值设置为负无穷,表示该点就是负循环中的顶点(负权环或者负权环所影响的那些顶点),并且就表示图中有负权值
  2. 代码
#define INF 0x3f3f3f3f

struct Edge{
    int u;//起
    int v;//终
    int weight;//长度
};

Edge edge[maxm];//用来存储所有的边
int dis[maxn];//dis[i]表示源点到i的最短距离
int n,m;//n个点,m条边
int s;//源点

bool Bellmen_ford()
{
    for(int i=1;i<=n;i++)//初始化
        dis[i]=INF;

    dis[s]=0;//源节点到自己的距离为0

    for(int i=1;i<n;i++)//松弛过程,计算最短路径
    {
        for(int j=1;j<=m;j++)
        {
            if(dis[edge[j].v]>dis[edge[j].u]+edge[j].weight)//比较s->v与s->u->v大小
                dis[edge[j].v]=dis[edge[j].u]+edge[j].weight;
        }
    }

    for(int j=1;j<=m;j++)//判断是否有负边权的边
    {
        if(dis[edge[j].v]>dis[edge[j].u]+edge[j].weight)
            return false;   //设置为负无穷,最后数组中为负无穷的顶点就是负循环中的点
    }

    return true;
}

bellman-ford的思想和dijkstra很像,其关键点都在于不断地对所有边进行松弛,不加以选择。而最大的区别就在于前者能作用于负边权的情况。其实现思路是在求出最短路径后,判断此刻是否还能对便进行松弛,如果还能进行松弛,便说明还有负边权的边。

SPFA算法

最强大,最快的单源最短路径算法

  1. 算法思想 :设立一个队列用来保存待优化的点,优化时每次取出队首结点min_i,并且用min_i点当前的最短路径估计值对min_点所指向的结点t进行松弛操作,如果t点的最短路径估计值有所调整,且t点不在当前的队列中,就将t点放入队尾。这样不断从队列中取出结点来进行松弛操作,直至队列空为止。
    外循环控制出队,内循环控制更新和入队
    更新的点不一定会入队(要满足不在队列中) 在队列中的加进去了也没啥事儿,只是会重复很多无用的计算,导致时间增多
    已经出队了的点也可能再次入队(满足不在队列中)
  2. 时间复杂度:O(KE),K为常数,平均值为2
  3. 用来算多源最短路径,时间复杂度:O(KE*V)
  4. 代码
#include
using namespace std;
#define MAXSIZE 10005
#define INFI 100000000
int n,m,k;
typedef struct Node
{
	int id;
	int W;
}AdjNode,*Adj;
vector<Adj>List[MAXSIZE];  //vector数组实现邻接表
int dist[MAXSIZE];
bool inq[MAXSIZE];
queue<int>node;    
void Spfa()   
{
	for(int i=1;i<=n;i++)
	{
		
			for(int j=1;j<=n;j++) //初始化 
			{
				dist[j]=INFI;
				inq[j]=false;
			}
			dist[i]=0;
			node.push(i);      //将起点纳入队列中 
			inq[i]=true;
			while(!node.empty())  
			{
				int min_i=node.front();  //得到队列元素并出队 
				node.pop();   
				
				inq[min_i]=false;  //元素出队 
				int len=List[min_i].size();
				for(int j=0;j<len;j++)   //对该点有影响的邻接点都进行松弛 
				{
					int t=List[min_i][j]->id;
					if(dist[t]>dist[min_i]+List[min_i][j]->W)
					{
						dist[t]=dist[min_i]+List[min_i][j]->W;
						if(!inq[t])    //把不在队列里的元素入队 
						{
							node.push(t);
							inq[t]=true;
						}
					}
						
				}
			}
			
			for(int j=1;j<=n;j++)  
			{                       
				cout<<dist[j]<<" "; 
			}
	}
}
int main()
{
	//cin>>n>>m>>k;
	scanf("%d %d",&n,&m);
	for(int i=0;i<m;i++)
	{
		int u,v,w;
		//cin>>u>>v>>w;
		scanf("%d %d %d",&u,&v,&w);
		if(u==v) continue;
		Adj a=new AdjNode;  //无向图,两个顶点都要进行操作 
		a->id=v;
		a->W=w;
		List[u].push_back(a);
		Adj b=new AdjNode;
		b->id=u;
		b->W=w;
		List[v].push_back(b);
	}
	Spfa();
	return 0;
}

SPFA算法是Bellman-ford算法的队列优化,比较常用。SPFA算法在负边权图上可以完全取代Bellman-ford算法,另外在稀疏图中也表现良好。但是在非负边权图中,为了避免最坏情况的出现,通常使用效率更加稳定的Dijkstra算法,以及它的使用堆优化的版本。通常的SPFA算法在一类网格图中的表现不尽如人意。不是很稳定,不如Dijkstra。

经典练习题:csp认证的第五题:
csp-201903

一般图用邻接表的形式存储会比邻接矩阵快
使用算法时要尽量进行优化

你可能感兴趣的:(数据结构与算法,算法,数据结构,c++)