最短路径算法的总结

Floyd算法的引出

在这里插入图片描述
给出一个有向图,给出任意两点之间的最短路径。

Floyd算法的原理

当每条路径只允许通过两个点的时候,即任意两点之间不允许经过第三个点时,那么这些城市之间最短路径就是初始路径
在这里插入图片描述
例如上图,从2到1的最短路径为无穷大,因为没有从2到1的路,又不能经过第三个点,所以最短路径就是无穷大。从1到2的最短路径就是原始的那条路的长度即为2。

考虑允许经过第三个点的情况(比如点1)
只需判断 e[i][1]+e[1][j] 是否比e[i][j]要小即可。e[i][j]表示的是从i号顶点到j号顶点之间的路程。e[i][1]+e[1][j]表示的是从i号顶点先到1号顶点,再从1号顶点到j号顶点的路程之和。其中 i是1-n循环,j也是1-n循环,代码实现如下。

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

在这里插入图片描述
这时候就用到动态规化的思想,每一条最短路径中的子路径都是最短路径,所以举个例子如下:
1-5的最短路径为1-3-4-5,初始的时候最短路径为1-5,当允许2经过的时候最短路径为1-5,当允许3经过的时候最短路径为1-5,1到4的最短路径为1-3-4,当允许4经过的时候最短路径为1-3-4-5。

当经过1和2点的最短路径,代码如下:

//经过1号顶点
for(i=1;i<=n;i++)  
for(j=1;j<=n;j++)  
if (e[i][j] > e[i][1]+e[1][j])  e[i][j]=e[i][1]+e[1][j];  
//经过2号顶点
for(i=1;i<=n;i++)  
for(j=1;j<=n;j++)  
if (e[i][j] > e[i][2]+e[2][j])  e[i][j]=e[i][2]+e[2][j];

在这里插入图片描述
依次可以推出,最后可以允许经过所有的点的时候,得到的最短路径就是最终的最短路径。
所有最终的核心代码如下:

for(k=1;k<=n;k++)  
    for(i=1;i<=n;i++)  
    	for(j=1;j<=n;j++)  
    	if(e[i][j]>e[i][k]+e[k][j])  
                     e[i][j]=e[i][k]+e[k][j];

代码的核心思想:从i号顶点到i号顶点只经过前k号点的最短路径。
最终完整demo代码:

#include <stdio.h>
    int main()  
    {  
    int e[10][10],k,i,j,n,m,t1,t2,t3;  
    int inf=99999999; //用inf(infinity的缩写)存储一个我们认为的正无穷值
    //读入n和m,n表示顶点个数,m表示边的条数
        scanf("%d %d",&n,&m);  
    //初始化
    for(i=1;i<=n;i++)  
    for(j=1;j<=n;j++)  
    if(i==j) e[i][j]=0;    
    else e[i][j]=inf;  
    //读入边
    for(i=1;i<=m;i++)  
        {  
            scanf("%d %d %d",&t1,&t2,&t3);  
            e[t1][t2]=t3;  
        }  
    //Floyd-Warshall算法核心语句
    for(k=1;k<=n;k++)  
    for(i=1;i<=n;i++)  
    for(j=1;j<=n;j++)  
    if(e[i][j]>e[i][k]+e[k][j] )   
                        e[i][j]=e[i][k]+e[k][j];  
    //输出最终的结果
    for(i=1;i<=n;i++)  
        {  
    for(j=1;j<=n;j++)  
            {  
                printf("%10d",e[i][j]);  
            }  
            printf("\n");  
        }  
    return 0;  
    }

例题分析如下:

HDU1874畅通工程续

用Floyd算法解决

  1. 输入路径为双向路,而且不止一条,所以注意在输入赋值的时候要赋值两次,而且要加判断,比之前短则覆盖过去,正确的初始化(不能通过任何点作为中间点的最短路径)
  2. 依次允许每个城市作为中间点,然后进行判断是否可以松弛,n方的复杂度,n次。
  3. 定义好inf,然后结果判断是否是inf,是inf则说明没有结果,输出-1.
#include<iostream>
using namespace std;
int inf=0x3f3f3f3f;
int main(){
	int n,m,i,j,k,a,b,c,t1,t2;
	
	while(cin>>n>>m){
	int e[n][n];
	//初始化 
	for(i=0;i<n;i++)
	for(j=0;j<n;j++)
	{
	    e[i][j]=inf;
		if(i==j)e[i][j]=0;
	}
	//构建图,一定要记住最刚开始不允许通过第三点,所以有多条路的话只留下一条最短的路。 
	for(i=1;i<=m;i++)
	{
		cin>>a>>b>>c;
		if(e[a][b]>c){//留下最小的那条 
		e[a][b]=c;
		e[b][a]=c;			
		}
	}
	for(i=0;i<n;i++)//遍历每个点(被依次允许作为中间点) 
	for(j=0;j<n;j++)
	for(k=0;k<n;k++)
	{	//对每个点都应该遍历,每两个点之间都查询一次最短路径 
		if(e[j][i]!=inf&&e[i][k]!=inf&&e[j][k]>e[j][i]+e[i][k])//加个判断inf可以节省一些时间 
		e[j][k]=e[j][i]+e[i][k];
	}
	cin>>t1>>t2;
	if(e[t1][t2]!=inf)//是否为inf可以看出是否有一条正确的路径 
	cout<<e[t1][t2]<<endl;
	else
	cout<<"-1"<<endl;
	} 
	
	
	return 0;
} 

用Dijsktra算法解决

  1. 构建图与Floyd算法相同,增加了两个数组,dis数组存放给定点到每个点的最短路径,vis数组存放每个点是否被扩展。
  2. 遍历dis数组找到最小的值以及该点未被访问过,作为下一个扩展节点
  3. 对于该扩展节点,找到指向的所有边,进行松弛。
  4. 重复步骤3,完成n-1次即可。
#include<iostream>
using namespace std;
int inf=0x3f3f3f3f;
int main() {
	int n,m,i,j,k,a,b,c,t1,t2;		
	while(cin>>n>>m){
	int dis[n],vis[n];	//dis[n]存放最短路径 ,vis[n]存放是否被扩展的标志,初始的时候:开始节点为1 
	for(i=0;i<n;i++)vis[i]=0;//访问均为未访问 
	int e[n][n];
	//初始化 
	for(i=0;i<n;i++)
	for(j=0;j<n;j++)
	{
	    e[i][j]=inf;
		if(i==j)e[i][j]=0;
	}
	//构建图,一定要记住最刚开始不允许通过第三点,所以有多条路的话只留下一条最短的路。 
	for(i=1;i<=m;i++)
	{
		cin>>a>>b>>c;
		if(e[a][b]>c){//留下最小的那条 
		e[a][b]=c;
		e[b][a]=c;			
		}
	}
	cin>>t1>>t2;
	for(i=0;i<n;i++)dis[i]=e[t1][i];//dis[b]初始化为边e[a][b] 
	int flag=t1;
	for(i=0;i<n-1;i++){//除了开始节点还有n-1个节点需要扩展 
		int max=inf;
		//然后找到下一个要扩展的点 
		for(j=0;j<n;j++)
		{
			if(dis[j]<max&&!vis[j])//找最短边并且没有被访问过 
			{
				flag=j;
				max=dis[j];
			}
		}
		vis[flag]=1;//flag为当前扩展节点,最开始为起始点

		for(k=0;k<n;k++)
		{//	当前点,指向的边,看看是否可以松弛,可以松弛的话就进行松弛 
			if(dis[k]>e[flag][k]+dis[flag])
			dis[k]=e[flag][k]+dis[flag];	
		} 
	
	}
	if(dis[t2]!=inf)//是否为inf可以看出是否有一条正确的路径 
	cout<<dis[t2]<<endl;
	else
	cout<<"-1"<<endl;
	} 
	
	return 0;
}

用伪邻接表(容器)优化
不再采用二位数组,而是将下一节点以及权值存在一个结构体里,然后添加到每个节点的vector中,这样如果边比较少的话就可以节省很大空间。

#include<iostream>
#include<vector>
using namespace std;
int inf=0x3f3f3f3f;
struct node{
	int d,u;
	node(){}
	node(int d,int u){
		this->d=d;
		this->u=u;
	}
};
int main(){
	int n,m,i,j,k,a,b,c,t1,t2;		
	while(cin>>n>>m){
	vector<node>G[n+1];
	int dis[n],vis[n];	//dis[n]存放最短路径 ,vis[n]存放是否被扩展的标志,初始的时候:开始节点为1 
	for(i=0;i<n;i++)vis[i]=0;//访问均为未访问 
	for(i=0;i<n;i++)dis[i]=inf;//初始化最短路径全部为INF 
	//构建图,一定要记住最刚开始不允许通过第三点,所以有多条路的话只留下一条最短的路。 
	for(i=1;i<=m;i++)
	{
		cin>>a>>b>>c;
		G[a].push_back(node(c,b));
		G[b].push_back(node(c,a));
	}
	cin>>t1>>t2;	
	dis[t1]=0;
	int flag=t1;
	for(i=0;i<n-1;i++){//除了开始节点还有n-1个节点需要扩展 
		int max=inf;
		//然后找到下一个要扩展的点 
		for(j=0;j<n;j++)
		{
			if(dis[j]<max&&!vis[j])//找最短边并且没有被访问过 
			{
				flag=j;
				max=dis[j];
			}
		}
		vis[flag]=1;//flag为当前扩展节点,最开始为起始点

		for(k=0;k<G[flag].size();k++)
		{//	当前点,指向的边,看看是否可以松弛,可以松弛的话就进行松弛 
			int w=G[flag][k].d, u=G[flag][k].u;
			if(dis[u]>w+dis[flag])
			dis[u]=w+dis[flag];	
		} 
	
	}
	if(dis[t2]!=inf)//是否为inf可以看出是否有一条正确的路径 
	cout<<dis[t2]<<endl;
	else
	cout<<"-1"<<endl;
	} 
	
	return 0;
}

那么这种一定能带来性能的优化吗?

对于广义上的稀疏图来说,用容器可以降低 空间的浪费,以及不必要的遍历,例如存在太多的INF,不需要去访问。
用优先队列优化
寻找扩展节点不通过遍历dis数组,而是从开始的节点添加到优先队列里,队列里的优先级则可以找到扩展点,这个复杂度只是log。

#include<cstdio>  
#include<cstring>  
#include<algorithm>
#include<vector>
#include<iostream> 
#include<queue>
using namespace std;
#define INF 1e8  
const int maxn = 200 + 5;
int m,n;//图节点数目,从1到n编号  
int d[maxn];//单源最短距离  
bool done[maxn];//done[i]表示d[i]是否已经计算完   
struct node {
	int d, u;
	node() {}
	node(int d, int u) :d(d), u(u) {}
	bool operator < (const node &x)const
	{
		return x.d<d;
	}
};
vector<node>G[maxn];//表示i到j的有向边长 
void addedge(int x, int y, int d) {//添加边的
//	list.push_back(edge(x, y, d));
	G[x].push_back(node(d,y));
}
void dijkstra(int s)
{
  

	for (int i = 0; i<m; i++) d[i] = INF;//初始化 
	
	d[s] = 0;
	priority_queue<node>Q;

	Q.push(node(0, s));

	while (!Q.empty()) {//添加点的过程 

		node it = Q.top(); Q.pop();
		int u = it.u;
		if (done[u]){
		continue;	
		}
		else
			done[u] = true;
		for (int i = 0; i<G[u].size(); i++) {
			node& e = G[u][i];
			if (d[e.u]>d[u] + e.d)
			{
				d[e.u] = d[u] + e.d;
				Q.push(node(d[e.u], e.u));
			}
		}
	}

}
int main()
{
	memset(done, 0, sizeof(done));
	int start,end;
	while (cin >> m >> n) {
		int x1, x2, x3;
		int k = n;
		while (k--) {

			cin >> x1 >> x2 >> x3;
			addedge(x1, x2, x3);
			addedge(x2, x1, x3);

		}
       cin>>start>>end;
		dijkstra(start);
	    if(d[end]==INF)
	    printf("-1\n");
	    else
		printf("%d\n", d[end]);
      for(int i=0;i<m;i++) G[i].clear();//清空邻接表  
        memset(done, 0, sizeof(done));
//        cout<<"新的一组--------------"<<endl; 
	}
	return 0;
}

用Bellman-Ford算法解决

#include<iostream>
using namespace std;
int inf=0x3f3f3f3f;
int main(){
	int n,m,i,j,k,a,b,c,t1,t2;
	int check;
	while(cin>>n>>m){
	int dis[n];
	int u[m],v[m],w[m];
	//初始化 
	//构建图,一定要记住最刚开始不允许通过第三点,所以有多条路的话只留下一条最短的路。 
	for(i=0;i<m;i++)
	{
		cin>>u[i]>>v[i]>>w[i];
	}
	for(i=0;i<n;i++)dis[i]=inf;
	cin>>t1>>t2;
	dis[t1]=0;
	for(i=0;i<n-1;i++)//一共进行n-1轮即可 
	{
		check=0;
	for(j=0;j<m;j++)
	//对每一条边都进行松弛判断 (这里是双向边,所以要两次查看)
	{
		if(dis[v[j]]>dis[u[j]]+w[j])
		{
		dis[v[j]]=dis[u[j]]+w[j];
		check=1;		
		}
	
		if(dis[u[j]]>dis[v[j]]+w[j])
		{
		dis[u[j]]=dis[v[j]]+w[j];
		check=1;	
		}
	}	
		if(check==0)break;	
	}

	if(dis[t2]!=inf)//是否为inf可以看出是否有一条正确的路径 
	cout<<dis[t2]<<endl;
	else
	cout<<"-1"<<endl;
	} 
	
	
	return 0;
} 

用Bellman-Ford算法解决(队列优化)

#include <iostream>
#include <queue>
#include<cstdio>
#include<cstring>
using namespace std;
const int MAX = 210;
const int INF = 20000;
int map[MAX][MAX];
int dis[MAX];
bool used[MAX];//表示是否添加到了队列中,防止重复添加 
int SPFA(int start,int end,int n)
{
    int i;
    //存储初始化 
    for(i=0;i<n;i++)
    {
        dis[i] = INF;
    }
    dis[start] = 0;
    
    queue <int> q;
    q.push(start);//将起始点添加进去 
    used[start] = 1;
    while(!q.empty())
    {
        int mid;
        mid = q.front();
        q.pop();
        used[mid] = 0;
        for(i=0;i<n;i++)
        {
            if(map[mid][i] + dis[mid]< dis[i])
            {
                dis[i] = map[mid][i] + dis[mid];
                if(!used[i])
                {
                    q.push(i);//每次仅对最短路估计值发生变化了的顶点的所有出边执行松弛操作 
                    used[i] = 1;//防止下一次此时的i再次存进队列中
                }
            }
        }
    }
    return dis[end];
}
int main()
{
    int n,m;
    while(scanf("%d%d",&n,&m)!=EOF)
    {
        int i,j;
        for(i=0;i<MAX;i++)
        {
            for(j=0;j<MAX;j++)
            {
                map[i][j] = INF;
            }
        }//初始化map的值
        memset(used,0,sizeof(used));//used数组代表是否访问过
        for(i=0;i<m;i++)
        {
            int a,b,x;
            scanf("%d%d%d",&a,&b,&x);
            if(x < map[a][b])
            {
                map[a][b]=x;
                map[b][a]=x;
            }
        }
        int s,t;
        scanf("%d%d",&s,&t);
        int x = SPFA(s,t,n);//储存结果
        if(x<INF)
           printf("%d\n",x);
           else printf("-1\n");
    }
    return 0;
}

队列优化与原版本的区别:

  1. 存储结构不同。原先将每条边的两点以及权值存在三个数组中,因为每次需要全部遍历;改进后存在二维数组中,因为要遍历最短路估计值发生变化的顶点的所有出边。
  2. 处理对象不同。原先是所有边一视同仁,改进后避免了那些无用的计算,因为只对发生变化的顶点继续操作。
  3. 算法复杂度不同。后者减少了部分无用的计算,最差情况的时候,什么也减少不了,所以最差情况的算法复杂度和改进之前的一模一样。

总结(需要注意的几点)

  • 对于双向通道,一定要记得存储的时候考虑两个方向,即存入两条起点和终点相反的边。
  • Floyd算法不需要dis数组,直接在二维数组里操作,最终可以将任意两点的最短路径求出来,这个算法最大的优点就是方便实现,代码简单。其它算法都需要dis数组。
  • dis数组的初始化问题,如果图是用二维数组存储的话,直接用二维数组初始化就可以,如果用的容器,则对于起始点初始化为0,其它的均为INF即可。
  • 图的初始化问题,当使用二维数组的时候,每输入一条边,先判断一下是否小于已存在的那条边,因为两点之间可能有多条边,这样就能保存下来最短的那几条边。而对于用容器即邻接表存储的话,就直接将所有的边都存在vector即可。
  • 算法没有优劣之分,既然存在的算法则有其存在的意义,Dijsktra算法扩展性好,算法复杂度也不错,但是无法解决负边权问题,其它三个均能解决,所以要根据问题的具体分析,来确定使用哪个算法。
  • 所谓的四个算法本质上都是一样,都是利用松弛原理得到最终的答案。如下:

Floyd算法是按顺序逐渐允许每个顶点作为中间节点,按照这个中间节点的变化去进行松弛。
Dijsktra算法是根据当前离开始节点最近为标准来确定当前扩展节点,按照这个起始节点的变化去进行松弛。
Bellman-Ford算法是没有任何说法的对于每一条边都进行松弛,一共进行n-1轮松弛。
Bellman-Ford队列优化算法是将点存入队列里,如果当前点最短路径估计值发生了变化,则将该点添加进队列,继续松弛,如果没有发生变化则不需要进行无用的判断,减少了部分浪费的时间。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值