您真的了解图中的最短路径问题吗?(c++)

目录

前言

单源路最短路径

       朴素版的Dijkstra算法:

       堆优化版的的Dijkstra算法:

       bellman-ford算法:

       SPAF算法:

 多元最短路:

        Floyd算法:

 总结

      

         


前言:

   通过本章节的学习,能使你对图论中的最短路径有一定的认识。再通过一定的刷题联系,能够掌握基本的图论最短路径问题。

     最短路问题分为:

          1.单源最短路

              1.1:所有边权都为正的最短路问题。

                      解决方案:

                             1.朴素版的Dijkstra算法。

                             2.堆优化版的Dijkstra算法。

             1.2:存在负边权的最短路问题。

                     解决方案:

                              1.bellman-ford算法。

                              2.SPFA算法。

          2.多元最短路

                     解决方案:

                               Floyd算法。

单源路最短路径:

   朴素版的Dijkstra算法:

                迪杰斯特拉算法(Dijkstra)是由荷兰计算机科学家狄克斯特拉于1959年提出的,因此又叫狄克斯特拉算法。是从一个顶点到其余各顶点的最短路径算法,解决的是有权图中最短路径问题。迪杰斯特拉算法主要特点是从起始点开始,采用贪心算法策略,每次遍历到始点距离最近且未访问过的顶点的邻接节点,直到扩展到终点为止。

                 扩展的过程:找到始点距离最近且未访问过的顶点,用这个点去“松弛”其他的点。接下来我们用一道实例来看。

例题:

         拿样例来模拟松弛的过程。

       dis数组是1号点到其他点走k步的最短距离

               

 题解:

#include <iostream>
#include <algorithm>
#include <cstring> 
using namespace std;
const int N=510;
int map[N][N],dis[N],m,n;
bool book[N];//用于标记,将松弛过的点标记一下。以后不再松弛这个点
int dijkstra()
{
	memset(dis,0x3f,sizeof dis);
	dis[1]=0;//自己到自己 
	for(int i=1;i<=n;i++)
	{
		int k=-1;
		for(int j=1;j<=n;j++)
		if(!book[j]&&(k==-1||dis[k]>dis[j]))
		k=j;
		book[k]=true;
		for(int j=1;j<=n;j++)
		dis[j]=min(dis[j],map[k][j]+dis[k]);
	}
	if(dis[n]==0x3f3f3f3f) return -1;
	else return dis[n];
}
int main()
{
	//先对邻接矩阵进行初始化
	memset(map,0x3f,sizeof map); 
	cin>>n>>m;
	while(m--)
	{
		int a,b,c;
		cin>>a>>b>>c;
		map[a][b]=min(c,map[a][b]);
	}
	cout<<dijkstra()<<endl;
	return 0;
}

 堆优化版的的Dijkstra算法:

          

    其过程和朴素版的差不多,就是使用了一个堆,进行了优化,在实际写题中用到的不多。大多数都能用SPAF算法代替

  代码:

#include<iostream>
#include<algorithm>
#include<cstring>
#include<queue>

using namespace std;

typedef pair<int,int> PII;
const int N=1000010;
int h[N],ne[N],e[N],w[N],dist[N],idx;//w表示权重
int n,m;
bool st[N];

void add(int a,int b,int c)
{
    e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}

int dijkstra()
{
    memset(dist,0x3f,sizeof(dist));
    priority_queue<PII,vector<PII>,greater<PII>> heap;
    //小根堆的定义方式,PII的第一个变量存储的是距离,第二个变量存储的是该点的编号,内部按照第一个变量排序,即按距离排序
    dist[1]=0;
    heap.push({0,1});
    while(heap.size())
    {
        auto t=heap.top();//选择最小的距离的点
        heap.pop();//利用完该点之后要弹出
        int ver=t.second;//表示该点的编号
        if(st[ver]) continue;
        /*
        堆优化版的是将距离直接加入到堆中.
        例如:dist[5]=9(在堆中{9,5},第一次更新时加入),dist[5]=7(在堆中{7,5},第二次更新时加入)
        使用时用的是dist[5]=7,将该点({7,5})弹出后,在下一次循环中,如果{9,5}在堆顶的话,使用时
        两者间肯定要选距离要小的那个,不能使用{9,5}重复更新,所以要用st数组进行标记
        */
        st[ver]=true;//标记该点已经使用过了
        for(int i=h[ver];i!=-1;i=ne[i])
        {
            int j=e[i];
            if(dist[j]>dist[ver]+w[i])//如果j到起点的距离大于ver到1的距离加上ver到j的距离,就更新值的大小;
            {
                dist[j]=dist[ver]+w[i];
                heap.push({dist[j],j});
                /*
                更新距离之后将该点的距离加入到堆中,这也是上述为何要进行标记的原因,
                因为一个点的距离加入堆的次数可能有两次甚至更多,这样会影响到其他的点
                例如:
                {9,5},{7,5},{10,6},如果{7,5}被弹出后,堆中剩余的是{9,5},{10,6},堆顶
                的元素是{9,5}而5这个点的距离已经被使用过了,所以要将{9,5}这个点忽视掉
                */
            }
        }
    }
    if(dist[n]==0x3f3f3f3f) return -1;
    return dist[n];
}

int main()
{
    memset(h,-1,sizeof(h));
    cin>>n>>m;
    while(m--)
    {
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        add(a,b,c);
    }
    cout<<dijkstra()<<endl;
    return 0;
}

 bellman-ford算法:

               贝尔曼-福特算法(英语:Bellman–Ford algorithm),求解单源最短路径问题的一种算法,由理查德·贝尔曼(Richard Bellman) 和莱斯特·福特创立的。有时候这种算法也被称为 Moore-Bellman-Ford 算法,因为Edward F. Moore也为这个算法的发展做出了贡献。它的原理是对图进行V-1次松弛操作,得到所有可能的最短路径。

       这个算法可以求出走n步到达某个点的最短路径。(算法有两个循环构成)其外部循环有一个含义,就是循环几次,相当于走了几步。内部循环相当于是对走第n步的松弛。因为在执行这个算法的时候,我们只需要考虑边之间的关系即可,所以直接用一个结构体储存其边之间的关系即可。

               

这一题看起来和上面一题差不多,但是由于边权有负值,所以不能用Dijkstra算法来解决。

为什么不能用dijkstra算法?

dijkstra是基于贪心策略,每次都找一个距源点最近的点,然后将该距离定为这个点到源点的最短路径;但如果存在负权边,那就有可能先通过并不是距源点最近的一个次优点,再通过这个负权边,使得路径之和更小,这样就出现了错误。

题解:

#include <iostream>
#include <cstring>
using namespace std;//bellman - ford
const int N=1e4+10,M=510;
int st[M],back[M];
int m,n,k,number;
struct ff{
	int x,y,z;
}map[N];
int bellman_ford()
{
	memset(st,0x3f,sizeof st);
	st[1]=0;
	for(int i=1;i<=k;i++)//k是几,就相当于走了多少步
	{
		memcpy(back,st,sizeof st);
		for(int j=0;j<number;j++)
		{
			int a=map[j].x,b=map[j].y,c=map[j].z;
			st[b]=min(st[b],back[a]+c);
		}
	}
   return st[m];
}
int main()
{
	cin>>m>>n>>k;
	number=n;
	while(n--)
	{
	   // cout<<n<<endl;
		int a,b,c;
		cin>>a>>b>>c;
		map[n]={a,b,c};
	} 
	if(bellman_ford()<0x3f3f3f3f/2)
		cout<<bellman_ford()<<endl;
	else
		puts("impossible");
	return 0;
}

 SPAF算法:

        其实SPAF算法就是优化版的bellman-ford算法。我们在学习benllman-ford算法的时候,会发现,在第二个循环中,会对每一个边都进行处理。而实际上,这里面有些边就没有发生过变化所以造成了时间上的浪费。我们这里可以用一个队列对bellman-ford算法进行一个优化,就是每次仅让最短路程发生变化的点(进队列)的相邻边进行“松弛操作”。这样就避免了很多比必要的过程。

                          

题解: 

#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
//用邻接矩阵进行储存
const int N=1e5+10;
int head[N],ne[N],e[N],w[N],indx=0;//用邻接表进行储存
int m,n,st[N],book[N];
queue<int> q;
void add(int a,int b,int c)
{
	w[indx]=c;e[indx]=b;ne[indx]=head[a];head[a]=indx++;
}
int spfa(int k)
{
	memset(st,0x3f,sizeof st);
	st[k]=0;
	q.push(k);
	book[k]=1;
	while(!q.empty())
	{
		int s=q.front();
		q.pop();
		book[s]=0;
		for(int i=head[s];i!=-1;i=ne[i])
		{
			int j=e[i];
			if(st[j]>st[s]+w[i])
			{
				st[j]=st[s]+w[i];
				if(book[j]==0)
				{
					q.push(j);
					book[j]=1;
				}
			}
		}
	}
	return st[n];
}
int main()
{
	cin>>n>>m;
	memset(ne,-1,sizeof ne);
	memset(head,-1,sizeof head);
	while(m--)
	{
		int x,y,z;
		cin>>x>>y>>z;
		add(x,y,z);
	}
	if(spfa(1)<0x3f3f3f3f)
	cout<<spfa(1)<<endl;
	else
	puts("impossible") ;
	return 0;
}

 多元最短路:

        Floyd算法:

     又被称作是只有五行的算法。

基本思想就是:最开始只允许经过 1号顶点进行中转,接下来只允许经过1和2号顶点进行中转……允许经过 1~n 号所有顶点进行中转,求任意两点之间的最短路程。用一句话概括就是:从 i号顶点到j号顶点只经过前 k号点的最短路程。其实这是一种“动态规划”的思想。
例题:

                

 题解:

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N=210;
int a[N][N];
int n,m,k;
int main()
{
	cin>>n>>m>>k;
	memset(a,0x3f,sizeof a);
	for(int i=1;i<=n;i++)
	a[i][i]=0;
	while(m--)
	{
		int x,y,z;
		cin>>x>>y>>z;
		a[x][y]=min(a[x][y],z);//因为可能存在自环 
	}
    for(int k=1;k<=n;k++)
    {
        for(int i=1;i<=n;i++)
        {
            for(int j=1;j<=n;j++)
            a[i][j]=min(a[i][j],a[i][k]+a[k][j]);
        }
    }//floyd算法的时间复杂度为O(n^2)
	/*	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=n;j++)
		cout<<a[i][j]<<" ";
		cout<<endl;
	}*/
	 while(k--)
	 {
	 	int x,y;
	 	cin>>x>>y;
	 	if(a[x][y]>=0x3f3f3f3f/2)
	 	cout<<"impossible"<<endl;
	 	else
	 	cout<<a[x][y]<<endl;
	 }
	 return 0;
} 

 总结:

        最短路径问题能够解决很多生活中的实际问题,所以掌握这几种常见的算法很重要!

近一周学习的总结!

         

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
贪心算法是一种常见的求解最短路径问题的方法,其中迪杰斯特拉算法是其中的一种实现方式。下面是迪杰斯特拉算法的具体步骤: 1. 创建一个大小为n的数组dist,用来记录源点到每个顶点的最短距离。初始化为无穷大,除了源点为0。 2. 创建一个大小为n的数组visited,记录每个顶点是否已经被访问过,初始值为false。 3. 在dist数组中找到未被访问过的距离最小的顶点v,并将visited[v]设置为true。 4. 对于顶点v的每个邻接点u,如果visited[u]为false,并且源点到u的距离比源点到v加上v到u的距离小,则更新dist[u]为这个更小的值。 5. 重复步骤3和步骤4,直到所有顶点都被访问过。 下面是迪杰斯特拉算法的C++代码实现: ```c++ #include<iostream> #include<cstring> using namespace std; const int N = 1010; const int INF = 0x3f3f3f3f; int n,m,s,t; int g[N][N],dist[N]; bool visited[N]; void Dijkstra(int s){ memset(visited,false,sizeof(visited)); memset(dist,INF,sizeof(dist)); dist[s] = 0; for(int i=0;i<n;i++){ int v = -1; for(int j=0;j<n;j++){ if(!visited[j] && (v==-1 || dist[j]<dist[v])) v = j; } visited[v] = true; for(int u=0;u<n;u++){ if(!visited[u] && g[v][u]!=INF && dist[v]+g[v][u]<dist[u]){ dist[u] = dist[v]+g[v][u]; } } } } int main(){ cin>>n>>m>>s>>t; memset(g,INF,sizeof(g)); for(int i=0;i<m;i++){ int a,b,c; cin>>a>>b>>c; g[a][b] = g[b][a] = c; } Dijkstra(s); cout<<dist[t]<<endl; return 0; } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小魏苦练算法

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值