五种最短路算思路及其代码实现【全】

刚看完Acwing上面y总的最短路视频,过来写一篇博客总结一下,也希望能帮助到别人
首先安利一波这个网站,里面有很多视频题解,算法模板,还能进行匹配对战
AcWing
AcWing
AcWing

先上一张图,刚用这个画图软件,可能画的比较拙劣
五种最短路算思路及其代码实现【全】_第1张图片
根据图片我们可以知道,最短路问题分为单源最短路和多源最短路。
单源最短路:只有一个出发点。求出来的应该是一个一维数组,保存该点到各点最短距离
多源最短路:有多个出发点,那么求出来应该是一个二维数组,每行表示一个上面的一维数组。

Flyod

首先说一下多源最短路把。Floyd算是这几个算法里面最简单无脑的方法了,就是三重循环,暴力遍历所有的点。它的实现思想:从任意节点i到任意节点j的最短路径不外乎2种可能,1是直接从i到j,2是从i经过若干个节点k到j。所以,算法假设Dis(i,j)为节点u到节点v的最短路径的距离,对于每一个节点k,算法检查Dis(i,k) + Dis(k,j) < Dis(i,j)是否成立,如果成立,证明从i到k再到j的路径比i直接到j的路径短,便设置Dis(i,j) = Dis(i,k) + Dis(k,j),这样一来,当遍历完所有节点k,Dis(i,j)中记录的便是i到j的最短路径的距离。

下面上这个算法实现的一个基本模板,具体代码要根据具体问题进行更改

 for(int k=1;k<=n;k++){//代表中转顶点从1到n
        for(int i=1;i<=n;i++){
            for(int j=1;j<=n;j++){
                if(dis[i][j]>dis[i][k]+dis[k][j]){
                    dis[i][j]=dis[i][k]+dis[k][j];

Dijkstra

这个算法应该是大部分人刚接触最短路时最先学到的算法。Dijkstra其实是基于贪心的思想,采用了广度优先搜索的策略,以起始点为中心向外层层扩展,直到扩展到终点为止。。

原理:
设G=(V,E)是一个带权有向图,把图中顶点集合V分为两组,第一组为已求出最短路径的顶点集合(用S表示,初始时S中只有一个源点,以后每求得一条最短路径 , 就将加入到集合S中,直到全部顶点都加入到S中,算法就结束了),
第二组为其余未确定最短路径的顶点集合(用U表示),按最短路径的的递增次序依次把第二组中的顶点加入S中。在加入的过程中,总保持从源点v到S中各个顶点的最短路径长度不大于从源点v到U中任何路径的长度。
此外,每个顶点对应一个距离,S中的顶点的距离就是从v到此顶点的最短路径长度,U中的顶点的距离,是从v到此顶点只包括S中的顶点为中间顶点的当前路径的最短长度。
然后下面提供一个比较好用的模板:

#include
using namespace std;
const int N=510;
int dist[N],g[N][N],n,m;
bool st[N];
int dijkstra()
{
	memset(dist,0x3f,sizeof dist);
	dist[1]=0;
	for(int i=0;i<n-1;i++)
	{
		int t=-1;
		for(int j=1;j<=n;j++)
			if(!st[j]&&(t==-1||dist[j]<dist[t]))
				t=j;
		st[t]=true;
		for(int j=1;j<=n;j++)
			dist[j]=min(dist[j],dist[t]+g[t][j]);
	}
	if(dist[n]==0x3f3f3f3f)	return -1;
	return dist[n];
}
int main()
{
	memset(g,0x3f,sizeof g);
	cin>>n>>m;
	while(m--)
	{
		int a,b,c;
		cin>>a>>b>>c;
		g[a][b]=min(g[a][b],c);
	}
	cout<<dijkstra();
}

优化版的Dijkstra

优化原理:
优化Dijkstra有两个版本,一个是用优先队列优化,一个是用堆来进行优化。首先我们要分析哪里需要优化,为什么这样优化?
先回答第一个问题:从上面Dijkstra的原理和代码我们可以看出来。我们每次在找最近的点的时候(就是代码中每次给t赋值的这个过程)上面这个过程的代码是O(n^2)的,那么我们可以考虑用一种存储形式,让他每次直接返回一个最小的,就达成了优化。
然后第二个问题:小根堆和优先队列(设置出队顺序)正好符合这个要求。
优化代码:

#include
using namespace std;
typedef pair<int,int>PII;
const int N=1e5+10;
int n,m,h[N],w[N],e[N],ne[N],idx,dist[N];
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);
	dist[1]=0;
	priority_queue<PII,vector<PII>,greater<PII>>heap;
	heap.push({0,1});
	while(heap.size())
	{
		auto t=heap.top();
		heap.pop();
		int ver=t.second,distance=t.first;
		if(st[ver])	continue;
		st[ver]=true;
		for(int i=h[ver];i!=-1;i=ne[i])
		{
			int j=e[i];
			if(dist[j]>distance+w[i])
			{
				dist[j]=distance+w[i];
				heap.push({dist[j],j});
			}
		}
	}
	if(dist[n]==0x3f3f3f)	return -1;
	return dist[n];
}
int main()
{
	cin>>n>>m;
	memset(h,-1,sizeof h);
	while(m--)
	{
		int a,b,c;
		cin>>a>>b>>c;
		add(a,b,c);
	}
	cout<<dijkstra()<<endl;
}

Bellman-Ford

这个算法应该算是除了Flyod之外最简单且最容易理解的算法了。
Bellman-Ford算法的优点是可以发现负圈,缺点是时间复杂度比Dijkstra算法高。

算法流程:
(1)初始化:将除起点s外所有顶点的距离数组置无穷大 d[v] = INF, d[s] = 0
(2)迭代:遍历图中的每条边,对边的两个顶点分别进行一次松弛操作,直到没有点能被再次松弛
(3)判断负圈:如果迭代超过V-1次,则存在负圈

根据上面这个流程可以知道,我们只需要把输入数据按边存储,然后依次遍历完就ok了,算是一个比较傻的算法,这里就不多做解释了。
然后这个算法因为是按边进行处理的,所以有一个优势,就是当题目限制了最短路能有几条边的时候,只有这个算法可以使用。下面我们上一道例题 题目代码来源AcWing 853. 有边数限制的最短路
五种最短路算思路及其代码实现【全】_第2张图片
代码如下:

#include 
#include 
#include 

using namespace std;

const int N = 510, M = 10010;

struct Edge
{
    int a, b, c;
}edges[M];

int n, m, k;
int dist[N];
int last[N];

void bellman_ford()
{
    memset(dist, 0x3f, sizeof dist);

    dist[1] = 0;
    for (int i = 0; i < k; i ++ )
    {
        memcpy(last, dist, sizeof dist);
        for (int j = 0; j < m; j ++ )
        {
            auto e = edges[j];
            dist[e.b] = min(dist[e.b], last[e.a] + e.c);
        }
    }
}

int main()
{
    scanf("%d%d%d", &n, &m, &k);

    for (int i = 0; i < m; i ++ )
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        edges[i] = {a, b, c};
    }

    bellman_ford();

    if (dist[n] > 0x3f3f3f3f / 2) puts("impossible");
    else printf("%d\n", dist[n]);

    return 0;
}

作者:yxc
链接:https://www.acwing.com/activity/content/code/content/48523/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

SPFA

SPFA算法就是用优先队列优化后的Bellman—Ford算法,所以原理我们就不在赘述了。
这个算法算是使用最为普遍的,很多问题不管是正权图还是带负权图,都可以用SPFA算法。
优化原理:
Bellman—Ford算法的时间复杂度比较高,原因在于Bellman—Ford算法要递推n次,每次递推,扫描所有的边,在递推n次的过程中很多判断是多余的,SPFA算法是Bellman—Ford算法的一种队列实现,减少了不必要的冗余判断。
大致流程:
用一个队列来进行维护。初始时将源点加入队列。每次从队列中取出一个顶点,并与所有与它相邻的顶点进行松弛,若某个相邻的顶点松弛成功,则将其入队。重复这样的过程直到队列为空时算法结束。
代码模板:

#include 
#include 
#include 
#include 

using namespace std;

const int N = 100010;

int n, m;
int h[N], w[N], e[N], ne[N], idx;
int dist[N];
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 spfa()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;

    queue<int> q;
    q.push(1);
    st[1] = true;

    while (q.size())
    {
        int t = q.front();
        q.pop();

        st[t] = false;

        for (int i = h[t]; i != -1; i = ne[i])
        {
            int j = e[i];
            if (dist[j] > dist[t] + w[i])
            {
                dist[j] = dist[t] + w[i];
                if (!st[j])
                {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }

    return dist[n];
}

int main()
{
    scanf("%d%d", &n, &m);

    memset(h, -1, sizeof h);

    while (m -- )
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c);
    }

    int t = spfa();

    if (t == 0x3f3f3f3f) puts("impossible");
    else printf("%d\n", t);

    return 0;
}

作者:yxc
链接:https://www.acwing.com/activity/content/code/content/48498/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

你可能感兴趣的:(个人总结,算法,队列,数据结构)