#2020寒假集训#最短路入门(Floyd弗洛伊德 和 Dijkstra迪杰斯特拉 算法)代码笔记

最短路问题

对于最短路问题,可用邻接表、邻接矩阵等形象表述,是图论算法的基础
比如这样的一个【图】(边长一般会有权值)
在这里插入图片描述在这里插入图片描述
它可以表示成像左图这样的【邻接表】或向右图这样的【邻接矩阵】
在这里插入图片描述在这里插入图片描述

算法方面主要分为四种
  1. Floyd弗洛伊德算法——处理多源最短路问题,但不可含负权环,时间复杂度O(n3)
  2. Dijkstra迪杰斯特拉算法——处理单源最短路问题,但不可含负权值,时间复杂度O(n2)
  3. Bellman-Ford算法——处理单源最短路问题,有负权回路时输出错误提示,时间复杂度为O(VE)
    给定图G(V, E)(其中V、E分别为图G的顶点集与边集)
    能够检测并输出负环
  4. SPFA 算法(Bellman-Ford的对队列优化算法)——处理单源最短路问题,时间复杂度为O(VE)
    给定图G(V, E)(其中V、E分别为图G的顶点集与边集)
    当存在一个点入队大于等于V次,则有负环
    但仅能检测负环,不能输出负环
区别负权和负环
  1. 负权:至少有一条边是负的
  2. 负环:相加和是负的
涉及小技巧
  1. ACM无穷大常量——0x3F3F3F3F,通常在代码开始就const int INF=0x3F3F3F3F(小写f也可)0x3f3f3f3f的十进制为1061109567,和INT_MAX一个数量级,即109数量级
    而一般场合下的数据都是小于109的。
    0x3f3f3f3f * 2 = 2122219134,无穷大相加依然不会溢出
  2. 拓扑排序(这部分内容也后续再做研究)可参考大佬博客讲解
    在有向图中选一个没有前驱的顶点并且输出
    从图中删除该顶点和所有以它为尾的弧(即删除所有和它有关的边)
    重复上述两步,直至所有顶点输出,或者当前图中不存在无前驱的顶点为止,后者代表我们的有向图是有环的,因此,也可以通过拓扑排序判断一个图是否有环
  3. 链式前向星储存邻接表
    边的结构体声明如下
struct EDGE
{
    int u,v,w,next;
    EDGE() {}
    EDGE(int _u,int _v,int _w,int _next)
	{
        u=_u,v=_v,w=_w,next=_next;
    }
}edge[MAXM];

初始化所有的head[i] = INF,当前边总数 edgeCount=0
每读入一条边,调用addEdge(u,v,w)函数如下:

void addEdge(int u,int v,int w)
{
    edge[edgeCount]=EDGE(u,v,w,head[u]);
    head[u]=edgeCount++;
}

暂时还比较习惯用vector储存邻接表,后续仔细学习链式前向星后再进行运用
可参考大佬博客讲解——前向星和链式前向星

  1. 术语小贴士:松弛——就是更新两点之间的最短路径/时间

今天我们主要运用一下Floyd弗洛伊德算法和Dijkstra迪杰斯特拉算法
另外两种算法后续仔细学习了再做运用叭φ(>ω<*)

先来康康最短路问题的例题。◕ᴗ◕。
畅通工程续 HZNU19training题源

Background
某省自从实行了很多年的畅通工程计划后,终于修建了很多路。不过路多了也不好,每次要从一个城镇到另一个城镇时,都有许多种道路方案可以选择,而某些方案要比另一些方案行走的距离要短很多。这让行人很困扰。
现在,已知起点和终点,请你计算出要从起点到终点,最短需要行走多少距离。

Input
本题目包含多组数据,请处理到文件结束。
每组数据第一行包含两个正整数N和M(0<N<200,0<M<1000),分别代表现有城镇的数目和已修建的道路的数目。城镇分别以0~N-1编号。
接下来是M行道路信息。每一行有三个整数A,B,X(0<=A,B<N,A!=B,0<X<10000),表示城镇A和城镇B之间有一条长度为X的双向道路。
再接下一行有两个整数S,T(0<=S,T<N),分别代表起点和终点。

Output
对于每组数据,请在一行里输出最短需要行走的距离。如果不存在从S到T的路线,就输出-1.

Sample Input
3 3
0 1 1
0 2 3
1 2 1
0 2
3 1
0 1 1
1 2

Sample Output
-1

Floyd弗洛伊德算法

算法思想图解——构造邻接矩阵(INF表示无穷大)
在这里插入图片描述
该算法时间复杂度较大,但思维比较简单
核心代码中【 i 和 j 】表示两点之间的最短路径,外套【 k 】层循环表示其他经过点
也就是说,比如在26个字母中从A走到Z,要找最短路径就是A经过一点从那点到Z
那么具体是哪一点就靠循环k来遍历,如果k循环到起始位置,不用担心,同点的dis提早赋值为0啦

#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
const int N=1e3+10;
const int inf=0x3f3f3f3f;
int n,m,a,b,x,s,t;
int dis[N][N];
void Floyd()//弗洛伊德核心代码-不做优化时,时间复杂度为O(n3) 
{
	for(int k=0;k<n;k++)
	{
		for(int i=0;i<n;i++)
		{
			for(int j=0;j<n;j++)
			{
				dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);
			}
		}
	}
}
int main()
{
	while(~scanf("%d %d",&n,&m))
	{
		for(int i=0;i<n;i++)
		{
			for(int j=0;j<n;j++)
			{
				dis[i][j]=inf;
				if(i==j) dis[i][j]=0;
			}
		}
		for(int i=1,a,b,x;i<=m;i++)
		{
			scanf("%d %d %d",&a,&b,&x);
			x=min(x,dis[a][b]);
			dis[a][b]=dis[b][a]=x;
		}
		Floyd();
		scanf("%d %d",&s,&t);
		if(dis[s][t]==inf) printf("%d\n",-1);
		else printf("%d\n",dis[s][t]);
	}
	return 0;
}

上文已经提到Floyd算法的时间复杂度较大,一般的OJ系统1s能处理1e7-1e8的数据
那么对于Floyd这样n3的复杂度,只能接受不到1e3的数据,真的是非常非常小啦
所以很多时候我们需要更加快捷的算法,来康康下面这道例题叭。◕ᴗ◕。

一个人的旅行 HZNU19training题源

Background
虽然草儿是个路痴(就是在杭电待了一年多,居然还会在校园里迷路的人,汗~),但是草儿仍然很喜欢旅行,因为在旅途中 会遇见很多人(白马王子,0),很多事,还能丰富自己的阅历,还可以看美丽的风景……草儿想去很多地方,她想要去东京铁塔看夜景,去威尼斯看电影,去阳明山上看海芋,去纽约纯粹看雪景,去巴黎喝咖啡写信,去北京探望孟姜女……眼看寒假就快到了,这么一大段时间,可不能浪费啊,一定要给自己好好的放个假,可是也不能荒废了训练啊,所以草儿决定在要在最短的时间去一个自己想去的地方!因为草儿的家在一个小镇上,没有火车经过,所以她只能去邻近的城市坐火车(好可怜啊~)。

Input
输入数据有多组,每组的第一行是三个整数T,S和D,表示有T条路,和草儿家相邻的城市的有S个,草儿想去的地方有D个;
接着有T行,每行有三个整数a,b,time,表示a,b城市之间的车程是time小时;(1=<(a,b)<=1000;a,b 之间可能有多条路)
接着的第T+1行有S个数,表示和草儿家相连的城市;
接着的第T+2行有D个数,表示草儿想去地方。

Output
输出草儿能去某个喜欢的城市的最短时间。

Sample Input
6 2 3
1 3 5
1 4 7
2 8 12
3 8 4
4 9 12
9 10 2
1 2
8 9 10

Sample Output
9

这道题如果用第一道例题那样最普通的Floyd算法,大概率就TLE啦
但是有的时候碰碰运气,数据不是特别狠的话,我们对Floyd算法做个小优化还是可以AC哒!
来康康这个写法叭。◕ᴗ◕。

Floyd弗洛伊德算法

#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
const int N=1e3+10;
const int inf=0x3f3f3f3f;
int n,t,s,d,a,b,time;
int other[N],pos[N],love[N],dis[N][N],present[N];
void init()
{
	for(int i=1;i<=N;i++)
	{
		for(int j=1;j<=N;j++)
		{
			if(i==j) dis[i][j]=0;
			else dis[i][j]=inf;
		}
	}
	memset(pos,-1,sizeof(pos));
	memset(love,-1,sizeof(love));
	memset(present,0,sizeof(present));
}
void Floyd()//弗洛伊德核心代码-不做优化时,时间复杂度为O(n3) 
{
	for(int k=1;k<=n;k++)
	{
		for(int i=1;i<=n;i++)
		{
			if(dis[other[i]][other[k]]==inf) continue;
			//注意这一步能节省时间、优化代码的时间复杂度 
			for(int j=1;j<=n;j++)
			{
				dis[other[i]][other[j]]=min(dis[other[i]][other[j]],dis[other[i]][other[k]]+dis[other[k]][other[j]]);
			}
		}
	}
}
int main()
{
	while(~scanf("%d %d %d",&t,&s,&d))
	{
		init();
		n=0;
		for(int i=1;i<=t;i++)
		{
			scanf("%d %d %d",&a,&b,&time);
			if(present[a]!=1)
			{
				n++;
				other[n]=a;
				present[a]=1;
			}
			if(present[b]!=1)
			{
				n++;
				other[n]=b;
				present[b]=1;
			}
			time=min(time,dis[a][b]);
			dis[a][b]=dis[b][a]=time;
		}
		for(int i=1;i<=s;i++)
		{
			scanf("%d",&pos[i]);
		}
		for(int i=1;i<=d;i++)
		{
			scanf("%d",&love[i]);
		}
		Floyd();
		int Min=inf;
		for(int i=1;i<=s;i++)
		{
			for(int j=1;j<=d;j++)
			{
				if(dis[pos[i]][love[j]]<Min) Min=dis[pos[i]][love[j]];
			}
		}
		printf("%d\n",Min);
	}
}

这段代码最最关键的就是下面这句话啦٩(๑❛ᴗ❛๑)۶

if(dis[other[i]][other[k]]==inf) continue;

因为当数据很大的时候,真的可能遇到很多很多的INF点,也就是路径不通的点,时间距离无穷大
那么三重循环里当我们发现两重循环得到的dis[i][k]已经是INF
又何必浪费时间去做下一层的比较呢,下一层的比较找的是min值呢!
况且无穷大代表路不通,走一条不通的路何必呢哈哈哈哈哈

不过呐,如果出题方特别狠,给的数据在1e3以上,偏巧每条路还都是通的
那么这个投机取巧的优化也就没有用武之地啦,新的算法还是要好好学下哒❥(ゝω・✿ฺ)

Dijkstra迪杰斯特拉算法

算法思想图解——构造邻接矩阵(INF表示无穷大)
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这个算法的核心代码就是要有一个两个集合
一个是【node型q优先队列pop出去】的是已经找到最短路径的点集
另一个是还在node型vector的G数组中记录着的待找的各点之间的距离
G数组二维
第一维是以某点出发
第二维是个node结构体,记录着到to点路径w
其中dis数组记录了已经找到的最短路径,同时也用于更新还没被pop的可能更短的路径
主要的思想就是从起点出发,找到到其他点的最短路径
比如起点是A,在其他所有点里面,它到G点的路径最短,那么这个最短路径就是A-G的最短路径
所以这个算法是单源的,必须确定起点然后去找到其他点的最短路径
接着,下一步从G点开始找最短路径的点,比如找到了C点
那么现在得到的最短路径值,就是A-C的最短路径啦~以此类推……

不过有的时候题目会要求输入几种起点的可能性,几种终点的可能性
那么怎么找所有方案的最短路径呢?(比如第二道例题,下附AC代码)
只要输入起点位置的时候,对G数组进行初始化,赋值让0点位和起始点位的距离为0
现在只要在循环初始传入Dijkstra函数的变量是个虚设的起点0
在Dijkstra函数中第一轮循环的时候找到的就是那几个真正的起点
因为它们对于0这个虚设起点来说,路径为0肯定最小
就相当于找的是从0(虚设起点)到真正起点再到其他各点的路径
最后任务完成,虚设的起点当然阔以忽略啦

来啦来啦~快康康下面用Dijkstra算法写的第二道例题叭٩(๑>◡<๑)۶

#include<stdio.h>
#include<string.h>
#include<vector>
#include<queue>
using namespace std;

const int N=1e3+10,INF=0x3f3f3f3f;
/*
	32-bit(位)int类型的最大值是0x7fffffff 
	0x3f3f3f3f为ACM的无穷大常量=1061109567
	好处:无穷大相加依然不会溢出
	可以用memset(array, 0x3f, sizeof(array))来为数组设初值为0x3f3f3f3f
	因为这个数的每个字节都是0x3f
*/
struct Edge//用于传变量,构造vector 
//构造一个结构体,里面存有to和w两个变量 
/*
	to存着这条边指向哪儿;w存着权值
	Edge(){}是个用来给变量初始化0的函数
	Edge(int to,int w):to(to),w(w){}为结构体赋初值
	定义的结构体数组大小开了N<<1即2*N 
*/
{
    int to,w;
    Edge(){}//后面别加分号 
    Edge(int to,int w):to(to),w(w){}//后面别加分号 
} edge[N<<1];//位移操作,往左n个单位,就是乘以2的n次方,往右是除以 

/*
Edge(int to, int w) : to(to), w(w) {}相当于像这样赋值 
void f()
{
	Edge e;
	e.to=1,e.w=2;
	Edge e2=Edge();
	Edge e3=Edge(1,2);
*/

struct qnode//用于传变量,构造优先队列 
{
    int u,w;
    qnode() {}//后面别加分号 
    qnode(int u,int w):u(u),w(w){}//后面别加分号,此处类似于上面的Edge赋初值 
    bool operator<(const qnode &other) const
	{//这是优先队列里面固定了只能重载小于号!!!return >小的出,return <大的出 
        return w>other.w;//优先队列默认大的在前,此处意义详见上一行 
    }
    /*
	或者定义成这样friend bool operator < (const node &a,const node &b)
	下面就要return a.w>b.w; 注意上面的第一个只有w
	同时定义的时候,没有friend,但末尾得有const 
	*/
};
/*
	结构体里构造这类函数就是用于
	把之后类似于"qnode(s,0)" 的值,按顺序对应赋给"qnode(int u,int w)"的u和w
	同时!!!切记,这类函数名必须和结构体类型名一样才能起到这个作用 
*/

int n,m;
int S,D,s[N],d[N];//s[N]存起点位置;d[N]存想去的地方
int dis[N];//存着最短时间,被pop了的是已经最短的,还没被pop的是还在另一集合内待更新的 
vector<Edge> G[N];//二维结构体可变长数组,是还没找到最短路径的点集 

void Dijkstra(int s)//从main函数传入一个变量,后面用的时候叫做s 
{不做优化时,时间复杂度为O(n2) 
    priority_queue<qnode> q;
    memset(dis,INF,sizeof(dis));//dis数组赋初始值均为ACM无穷大常量
    dis[s]=0;
    q.push(qnode(s,0));//0,0入队列 
	/*
		等于有一个新的qnode类型结构体的变量
		它的u=s,w=0,同时被push到了优先队列里面 
	*/ 
    while(!q.empty())//这个部分类似于BFS广搜 
	{
        int u=q.top().u;
		/*
			定义一个新的变量u储存队首元素
			从起点走到某点目前最短的时间
			然后去找走到下一个点的最短时间
		*/ 
        q.pop();//储存完毕就移除,后续有push,直到队列无push的时候循环结束 
        /*
        	以下代码也可以写成(auto自动识别it迭代器的类型) 
			for(auto it:G[u])
			{
				int v=it.to,w=it.w;
				if(dis[v]>dis[u]+w)
				{
              		dis[v]=dis[u]+w;
               		q.push(qnode(v,dis[v]));
               	}
			}
		*/
        for (int i=0,len=G[u].size();i<len;i++)
		{
		/*
			遍历u到某点的时间,初始是点位0
			因为之前记录过邻接城市到0的时间是0min
			所以下一个先被找出来的最短时间位置就是起始位置啦 
		*/ 
		/*
			循环找从起点走到u之后的下一个点v的min time 
			找的时候就是遍历u记录的这个点往下走能走到哪个点
			最小的那个肯定也是从起点走到那个点的最小值
			因为对于非负的路径,已经是最小的
			通过其他路径再加路径长度,不会更小 
		*/
            int v=G[u][i].to,w=G[u][i].w;
			/*
				从u出发到i的时间,起始位置的w是0 
				i对应的to值是对于u来说要往下走的下一个点
				输入的时候已经用Edge捆绑录入过to和w 
				如果没连着就是w的值就是INF无穷大 
			*/
            if(dis[v]>dis[u]+w)
			{
                dis[v]=dis[u]+w;//dis[v]记录从起点暂时走到点位v的最短时间
                q.push(qnode(v,dis[v]));
				/*
					每个dis[v]都被放到队列里面
					那找被pop的那个,就找到最小的咯 
				*/
            }
        }
    }
}
/*
	传统写法-较慢
void Dijkstra(int s)
{
    memset(dis,INF,sizeof(dis));
	dis[1]=0;
	while(true)
	{
		int pos=-1,Min=INF;
		//找右边点集中最小值 
		for(int i=1;i<n;i++)
		{
			if(!used[i]&&dis[i]<Min)
			{
				Min=dis[i];
				pos=i;//已经走到i 
			}
		}
		if(pos==-1) break;
		int u=pos;
		//更新其他点 
		for(int i=0;len=G[u].size();i<len;i++)
		{
			int v=G[u][i].to,w=G[u][i].w;
			//遍历下一个点位,找下一个到哪儿会是最小 
			dis[v]=min(dis[v],dis[u]+w);//更新到每个v的dis-目前的最短时间 
		}
		used[u]=true;//要定义过used这个bool型数组 
	}
*/

int main()
{
    while(~scanf("%d %d %d",&n,&S,&D))
	{
        for(int i=0;i<N;i++)
		{
			G[i].clear();//vector二维数组的第二维循环清空 
		}
        for(int i=1,u,v,w;i<=n;i++)//u,v,w相当于a,b,time 
		{//u,v,w只是int在定义变量,因为循环内部要用 
            scanf("%d %d %d",&u,&v,&w);
            G[u].push_back(Edge(v,w));
			/*
				G[u]记录点u到点v的时间w,这个v和w用结构体捆绑
				且每个能从u走到的点,都会被接在G[u]可变长的第二维里面 
			*/
            G[v].push_back(Edge(u,w));
            /*
				G[v]记录点v到点u的时间w,这个u和w用结构体捆绑 
				且每个能从v走到的点,都会被接在G[v]可变长的第二维里面 
			*/
        }
        for (int i=1;i<=S;i++)
		{//输入可能的起点位置,让它们与0点位时间为0,方便函数里找起始位置 
            scanf("%d",&s[i]);//&s[i]也可用地址写成s+i
            G[0].push_back(Edge(s[i],0));//初始化点位0到邻接城市点s[i]是0min 
            G[s[i]].push_back(Edge(0,0));//初始化邻接城市点s[i]到点位0是0min 
        }
        Dijkstra(0);//传入s的初始值为0
        int res=INF;//输出的要是最短时间,初始值就设为无穷大 
        for (int i=1;i<=D;i++)//一共有D个想去的地方,就循环找最小时间 
		{
            scanf("%d",&d[i]);//&d[i]也可用地址写成d+i
            res=min(res,dis[d[i]]);
			/*
				这个写法就相当于if新的值比原来的min小
				那么min就变成了新的值 
				也就是遍历想去的城市,找到最短时间最短的那一个 
			*/
        }
        printf("%d\n",res);
    }
    return 0;
}
/*
	神奇的操作
	for(int i=1;i<=n;i++)
	{
		printf("%d%c",a[i]," \n"[i==n]);//看\n前面还有一个空格喔 
	}
	这个的意思就是当i==n的时候,对于%c输出的就是\n,否则输出的是空格
	[]内的条件成立的时候输出所列内容的后者!!! 
	这个小技巧在打印类似于到各个点最短路径矩阵的问题时可以用到
*/
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值