图论 最短路

前言

不要不求甚解,对新知识要刨根问底。背板子刷题不是目的,掌握了才是真理。

image


图论 最短路

简介

图论 (Graph theory) 是数学的一个分支,图是图论的主要研究对象。图 (Graph) 是由若干给定的顶点及连接两顶点的边所构成的图形,这种图形通常用来描述某些事物之间的某种特定关系。顶点用于代表事物,连接两顶点的边则用于表示两个事物间具有这种关系。

最短路是图论中最基础的一类,常考且有一定的思维量。我们的任务是掌握三种基本求最短路的方法并做到灵活运用。

建图

对于不同的数据范围,我们考虑用邻接矩阵和链式前向星两种方式建图。

邻接矩阵

实际上就是我们常用的二维数组,通常定义 \(d_{i,j}\)\(i\) 点到 \(j\) 点的最短距离。优点是方便且易懂,缺点是很吃内存,\(3000 \times 3000\) 的邻接矩阵就要占到约 \(34.4468MB\),再加上每道题要求的初始化,很容易爆 MLE。

使用方式

建图

很简单,以 \(n\times m\) 的图为例,两层循环搞定。

for(int i=1;i<=n;i++)
	for(int j=1;j<=m;j++)
		scanf("%d",&d[i][j]);
引用

更简单了,按照定义肆意使用即可。

用途

Floyd 刚需,其他可根据自身习惯和题目适用情况来选择是否使用。

链式前向星

一般由三个数组组成:\(head_i\)\(to_i\)\(next_i\)。若有边权则添加一个 \(w_i\) 数组存边权。链式前向星不仅在内存上优化很多,以 \(n\le 10000\)\(m\le 20000\)\(n\) 为点数,\(m\) 为边数)为例,链式前向星只占用了 \(0.267105MB\),并且在时间上也有一定幅度的优化。是用的最多的建图方式。

初学可能对这四个数组的含义不太理解其实也不用太理解,可以手搓一个小一点的图,你就会发现它的精妙之处。使用起来简单,只需要记住很短的板子就行了。

使用方式

建图

需要一个 cnt 存不同边的序号,之后不会再使用,注意不要与其他变量重名。

void add(int u,int v/*,int ww*/)
{
	to[++cnt]=v;
//	w[cnt]=ww;
	nxt[cnt]=head[u];
	head[u]=cnt;
	//这两句千万不能调换顺序!
}
引用

详见注释。

for(int i=head[u];i!=-1;i=nxt[i])
{
	int v=to[i];
	//循环中 i!=-1 是根据你对 head 数组初始化的数值而来的
	//全局变量中初始均为 0,所以写成 for(int i=head[i];i;i=nxt[i])即可
	//像我一样担心极小概率事件的可以先 memset(head,-1,sizeof head) 然后如上写
	//u 是起点 v 是终点 之后按要求操作即可
}

用途

很广,除了 Floyd 无脑用就行。

Vector

方便的 vector 容器大举入侵存图领域!

只能说写起来很方便,但时间会大↑幅↓增↗加→,且无法(至少我不知道)如何存边权,不推荐一般 OIer 使用该方式。如需使用,建议先钻研一下 vector 容器。

使用方式

定义 std::vector<int>ve[maxn]

存图
for(int i=1;i<=m;i++)
{
	int u,v;
	scanf("%d%d",&u,&v);
	ve[u].push_back(v);
}
引用
for(auto i:ve[u])
{
	int v=i;
	//操作
}

最短路

很多题目有形或无形要求找到某点到某点的最小代价,也诞生了许多跑最短路的算法。

一、Floyd

新手之神!多源最短路之神!最好写的一集!

适用于任何图,不管有向无向,边权正负,但是最短路必须存在,即无负环。

只需要邻接矩阵存图后三层循环即可完成,类似于 dp 的思想。时间复杂度为 \(O(n^3)\),只能极限跑 \(n\le 300\) 的图。

实现

for(int k=1;k<=n;k++)
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
			d[i][j]=min(d[i][j],d[i][k]+d[k][j]);
Tip 1

跑最短路前,我们通常会将距离都赋一个极大值。

大家应该已经了解到,如果两数相加超出 int 范围会炸。

那么观察上面的代码,存在一个加和 d[i][k]+d[k][j]

所以这个极大值我们应选取一个足够大但不超过 int 范围二分之一的数。

当然可以手搓,不过有更方便的办法。

0x3f0x7f 在 memset 中表示的就是 int 范围下一个极大值的一半和一个极大值,可以自己试一试。

所以,我们赋初值时应该这样写:memset(d,0x3f,sizeof d);

例题

模板题
注意一下重边取小以及自身为 0,其余套板子即可。

二、Dijkstra

是一种求解非负权图单源最短路径的算法。

朴素的 Dijkstra 算法是 \(O(n^2)\) 的复杂度,并不算优秀,所以常用的是堆优化后复杂度降到 \(O(m \log m)\) 的写法。该算法较为稳定,但仅局限于非负权图上。

实现

int n,m,s,cnt;
int hh[N],to[N<<1],ne[N<<1],w[N<<1];//存图 
int dis[N];bool yz[N];//算法实现需要 
struct rmm
{
	int dis,u;
	bool operator<(const rmm &a)const{return a.dis<dis;}
};//手写小根堆(小的为根) 
namespace Wisadel
{
	void Wadd(int u,int v,int ww)
	{
		to[++cnt]=v;
		w[cnt]=ww;
		ne[cnt]=hh[u];
		hh[u]=cnt;
	}
	void Wdij(int x)
	{
		priority_queue<rmm>q;
		dis[x]=0;
		q.push({0,x});
		//先将起点置入堆中 
		while(!q.empty())
		{
			int u=q.top().u;
			q.pop();//取出 
			if(yz[u]) continue;
			yz[u]=1;//标记 
			for(int i=hh[u];i!=-1;i=ne[i])
			{
				int v=to[i];
				if(dis[v]>dis[u]+w[i])
				{//松弛操作 
					dis[v]=dis[u]+w[i];
					q.push({dis[v],v});
				}
			}
		 } 
	}
	short main()
	{
		n=qr,m=qr,s=qr;
		memset(dis,0x7f,sizeof dis); 
		memset(hh,-1,sizeof hh);
		//初始化 
		fo(i,1,m)
		{
			int a=qr,b=qr,c=qr;
			Wadd(a,b,c);
		}
		Wdij(s);
		return Ratio;
	}
}
int main(){return Wisadel::main();}

例题

模板题

完全没有坑点,放心打板子。

三、SPFA(优化Bellman–Ford)

Bellman–Ford 算法是一种基于松弛(relax)操作的最短路算法,可以求出有负权的图的最短路,并可以对最短路不存在的情况进行判断。Bellman–Ford 算法所做的,就是不断尝试对图上每一条边进行松弛。我们每进行一轮循环,就对图上所有的边都尝试进行一次松弛操作,当一次循环中没有成功的松弛操作时,算法停止。其时间复杂度为 \(O(nm)\)

大多数情况下,我们并不需要跑满所有的松弛操作。很显然,只有上一次被松弛的结点,所连接的边,才有可能引起下一次的松弛操作。那么我们用队列来维护哪些结点可能会引起松弛操作,就能只访问必要的边了。这就是 SPFA 算法。

SPFA 也可以用于判断 s 点是否能抵达一个负环,只需记录最短路经过了多少条边,当经过了至少 n 条边时,说明 s 点可以抵达一个负环。

虽然在大多数情况下 SPFA 跑得很快,但其最坏情况下的时间复杂度也是 O(nm),将其卡到这个复杂度也是不难的,所以要谨慎使用。在没有负权边时最好使用 Dijkstra 算法,在有负权边且题目中的图没有特殊性质时,若 SPFA 是标算的一部分,题目不应当给出 Bellman–Ford 算法无法通过的数据范围。

实现

#include<bits/stdc++.h>
using namespace std;
const int Ratio=0;
const int N=5e5+5; 
int hh[N],to[N<<1],w[N<<1],ne[N<<1]; 
int dis[N],cnt[N];bool yz[N];
queue<int>q;
bool spfa(int n,int s)
{
	memset(dis,0x3f,sizeof dis);
	dis[s]=0,yz[s]=1;
	q.push(s);
	while(!q.empty())
	{
		int u=q.front();
		q.pop();
		yz[u]=0;
    	for(int i=hh[u];i!=-1;i=ne[i])
    	{
    		int v=to[i];
    		if(dis[v]>dis[u]+w[i])
			{
    			dis[v]=dis[u]+w[i];
    			cnt[v]=cnt[u]+1;// 记录最短路经过的边数
        		if(cnt[v]>=n)
					return false;
		        // 在不经过负环的情况下,最短路至多经过 n - 1 条边
		        // 因此如果经过了多于 n 条边,一定说明经过了负环
        		if(!yz[v])
					q.push(v),yz[v]=1;
    		}
    	}
	}
	return true;
}

例题

模板题(不卡SPFA)

确实不卡,但在 Dij 的那道例题中会T4个点。

总结

image

三种方法各有优劣吧。Floyd 简单易行但时间复杂度高,Dijkstra 更稳定但只能应用于非负权图,SPFA 适用性更高且能判负环但容易被卡。因此我们不能只凭借某一种算法把题过了就不管了,而是要熟练运用这三种,根据不同的数据范围和图的类型来选择使用不同的算法。


完结撒花~

image

posted @ 2024-06-17 09:48  DrRatio  阅读(7)  评论(0编辑  收藏  举报