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


在这里插入图片描述

dijkstra算法

单源最短路径算法
伪码描述:
在这里插入图片描述
在这里插入图片描述

邻接矩阵实现

  1. 基本代码
#include<iostream>
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<<min_i<<endl;
		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<bits/stdc++.h>
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)

最小堆优化

思路就是将遍历查找dist数组中的最小值,改成将这些最小值装在最小堆里,直接取堆顶元素,
由于最小堆的查找最小值的时间复杂度是O(logn)的,将原来的O(n)的查找时间缩短了;

typedef pair<int,int>PII;
priority_queue<PII,vector<PII>,greater<PII>>que;   //最小堆简单版声明方法,存储dist中的最小值,first存dist数组值,second存结点编号,默认按照first排序 

1.代码

#include<bits/stdc++.h>
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<iostream>
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<<min_i<<endl;
		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<bits/stdc++.h>
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<<v1<<"-->";
		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算法差
作用:(1)求带有负权值的图的最短路;(2)检测负权环
bellman ford在dijistra算法不能用时才用,比如存在负权边,如果有负权边,dijistra算法求得的最短路的值是错误的;另外如果有负权环,dijistra算法就会陷入负循环(Negative Cycles)中,因为每次总能找到一个更短的路径,就没法得到正确结果,此时往往需要将其检测出来;

  1. 基本思想:每次遍历所有的边,对每条边的到达点进行松弛,一共遍历n-1(n为顶点个数)那么多次,双重循环,外层循环为顶点个数-1,内层循环遍历所有的边;
    这个遍历的次数i,有特殊的含义,遍历到第i次表示求出来了从1号点经过不超过i条边到达每个点的最短路的距离,第n - 1次更新的最短路径的边数一定是n - 1,第n次如果更新了某条最短路的长度,那么这条最短路上就包含n条边,所以就包含n + 1个点,那么必然会有两个点的编号相同,所以必然存在环,再者如果在第n-1次已经把最短路求出来了,第n次更新的时候最短路径的最小值就不会再更新了,再更新就说明存在负环。
    如果存在负权值,第一次操作后(n-1次遍历)能得到基本的最短路径的数组,此时需要重复进行刚才的步骤,判断是否存在负权环,但是在松弛步骤时不进行更新,而是将能进行更新的点的距离值设置为负无穷,表示该点就是负循环中的顶点(负权环或者负权环所影响的那些顶点),并且就表示图中有负权值
  2. 如果图中存在负权环,也就是一个环的总权值和为负值,并且这个负环还在1->n的路径当中的,那最后求得的1->n的最短路径是不存在的,dist[n]=负无穷。
  3. 代码
#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=0;j<m;j++)   //边从下标为0开始存储
        {
            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=0;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,如果题目故意卡,就会导致是O(EV)
  3. 用来算多源最短路径,时间复杂度:O(KE*V)
  4. 代码
#include<bits/stdc++.h>
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;
}

检测负权环的方法:跟Bellman-ford算法一致,就是存一下到达当前更新点的最短路的长度,如果超过了n-1,就说明路径中存在环,再者到达当前点路径最小值还在更新,那说明这个环是负环

#include<iostream>
#include<queue>
using namespace std;
int n,m;
const int N=100005;
const int INF=0x3f3f3f3f;
int dist[N];
queue<int>que;
bool in[N];
int cnt[N];
typedef pair<int,int>PII;
vector<PII>adj[N];
bool spfa(int s)
{
    for(int i=1;i<=n;i++) 
    {
        dist[i]=INF; 
		in[i]=true; 
		que.push(i);    //这个地方不能只存储1,因为负环不一定是从1开始的路径上的,还可能跟1不连通 
    }                   //因此要把所有的点都存进去 
    
    dist[s]=0;
    
    while(!que.empty())
    {
        int min=que.front();
        que.pop();
        
        in[min]=false;  //出队列了
        
        for(int i=0;i<adj[min].size();i++)
        {
            int v=adj[min][i].first;
            if(dist[v]>dist[min]+adj[min][i].second)
            {
              
                dist[v]=dist[min]+adj[min][i].second;
                cnt[v]=cnt[min]+1;    //存在超过n-1条边的最短路径,那肯定就存在环,并且这个地方能再次更新说明这个环是负环 
                if(cnt[v]>=n) return true;  //存在负权环 
                if(!in[v])
                {
                    que.push(v);
                    in[v]=true;
                }
            }
        }
    }
    return false;
}
  1. SPFA算法是Bellman-ford算法的队列优化,比较常用。优化点在于Bellman-ford算法每次对所有的点都进行判断是否可以松弛,但其实在 dist[t]=dist[min_i]+List[min_i][j]->W; 中dist[t]不一定就会被更新,因为他在这个式子中不一定会变小,可以发现只有dist[min_i]变小了,dist[t]才会变小,因此队列里存储的就是之前操作中dist值变小的结点编号,下一次就用它来更新它的邻接结点的dist值。因此队列只是个存储的作用,不一定要用队列,用啥容器都可以,用队列效果比较好
  2. SPFA算法在负边权图上可以完全取代Bellman-ford算法,另外在稀疏图中也表现良好。但是在非负边权图中,为了避免最坏情况的出现,通常使用效率更加稳定的Dijkstra算法,以及它的使用堆优化的版本。通常的SPFA算法在一类网格图中的表现不尽如人意。不是很稳定,不如Dijkstra。
    并且使用SPFA算法是严格要求图中不存在负权环的,使用Bellman-ford算法可以存在负权环并且可以检测出来,但是不管什么算法存在负权环都无法求出包含该负权环的最短路径,不包含该负权环的路径还是能求出来的。
  3. SPFA算法是很快的,一般用Dijkstra能做的正权图它也能做,但是如果出题人卡时间复杂度导致SPFA成了O(nm)就换成优化版的Dijkstra就行。如果包含负权图又要求较低的时间复杂度就只能用SPFA了

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值