单源最短路学习笔记


什么是单源最短路?

首先我们得知道什么是图

图就是这玩意儿

每条边有方向,就是有向图,否则就是无向图

根据这条规则,上面的图就是有向图

我们来看这个问题:

给出一点 s ,求 s 到所有点的最短路径(单源最短路)

怎么求单源最短路?

一般来说,求单源最短路有3种方法:Floyd , Dijkstra , SPFA

Floyd

Floyd 算法基于 DP 思想,采用邻接矩阵存边

复杂度比较高,但是常数小,容易实现。(我会说只有三个 for 吗?)

适用于所有图,不管有向无向,边权正负,但是最短路必须存在。(但是不能有负环)

具体思路:枚举所有的两两点的组合以及每一个组合的“中转点”,再进行松弛操作

\(a_{x,y}\) 表示 \(x \to y\) 的最短路径长度

初始化:

\[a_{x,y}=\begin{cases} x=y \ 0 \\ x\neq y \ \infty \end{cases} \]

在求单源最短路径的时候就会浪费许多空间,但在求多源最短路时,复杂度仍是 $ O(n^{3}) $ ,使用范围很广

在用 Floyd 转递闭包时,我们可以考虑优化一点点:

在枚举两两的中转点时,我们枚举了很多不连通 的点

而这些点是没有贡献的

所以我们可以用 bitset 存边

每次用 _Find_next 寻找下一个联通的点

时间复杂度为 \(O(\dfrac{n^{3}}{k})\)(k是一个常数)

代码示例:

#include 
#include 
#define min(a,b) ((a)<(b)?(a):(b))
using namespace std;
const int inf=0x3f3f3f3f;
const int N=5e3+7;

int a[N][N];
int n,m,s;

inline void floyd() {
    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]); // 松弛操作,即更新每两个点之间的距离
}

signed main() {
  	scanf("%d%d%d",&n,&m,&s);
    memset(a,inf,sizeof(a));
    a[s][s]=0; //初始化
    for(int i=1,u,v,w;i<=m;++i) {
		scanf("%d%d%d",&u,&v,&w);
        a[u][v]=min(a[u][v],w); //建边,取min防止重边
    }
    floyd(); // 求最短路
    for(int i=1;i<=n;++i)
        printf("%d ",a[s][i]==inf ? 2147483647 : a[s][i]); // 输出
    return 0;
}

Floyd还可以做很多有意思的题目,只是复杂度 \(O(n^{3})\)


SPFA

SPFA基于广搜思想

我们用 dis 数组记录源点到图上任意一点距离,其中源点到自身距离为0,到其他点距离为INF。

将源点入队,并重复以下步骤:

  1. 队首 x 出队
  2. 遍历所有以队首为起点的有向边(x,i),若dis[x]+w(x,i)
  3. 如果点i不在队列中,则i入队
  4. 若队列为空,跳出循环;否则执行1

但是这么写在有源点可以到达的负环的情况下会T飞

如何解决?

我们考虑最短路存在的时候。

由于一次松弛操作会使最短路的边数至少 \(+1\) ,而最短路的边数最多为 \(n-1\)

所以最多执行 次松弛操作,\(n-1\) 即最多循环 \(n-1\) 次。

那么一个点入队次数 \(\geq n\) ,就会存在负环,我们就可以退出

如果图是随机的,时间复杂度为 \(O(KM)\) (K可以认为是个常数,m为边数,n为点数)

但是实际上 SPFA 的最坏复杂度是 \(O(NM)\) ,可以构造出卡 SPFA 的数据,让 SPFA 超时。

在 NOI 2018 的第一天第一题中,出题人卡了 SPFA 算法,导致 100 分变成 60 分,所以在没有负环、没有负边权、单纯求最短路径,不建议使用 SPFA 算法,而是用 Dijkstra 算法。

代码示例:

#include 
#include 
#include 
#include 
#define min(a,b) ((a)<(b)?(a):(b))
using namespace std;
const int inf=0x3f3f3f3f;
const int N=2e3+7;

vector > edge[N];

int cnt[N]; // 记录入队次数
int dis[N]; // 记录最短路径长度
bool inque[N]; // 判断点是否在队列中

int n,m,s;

inline bool SPFA(int s) {
	memset(dis,inf,sizeof(dis));
	queue q;
	q.push(s);
	dis[s]=0,inque[s]=true,cnt[s]=1; // 初始化
	while(!q.empty()) {
		int u=q.front(); // 取出队首点
		q.pop();
		inque[u]=false;
		for(int i=0,v,w;idis[u]+w) {
				dis[v]=dis[u]+w; // 进行松弛操作
				if(!inque[v]) {
					q.push(v); // 如果不在队列中,入队
					inque[v]=true;
					++cnt[v]; // 入队次数+1
					if(cnt[v]>=n) // 如果入队次数>n-1
						return false; // 有负环,退出
				}
			}
		}
	}
	return true;
}

int main() {
	scanf("%d%d%d",&n,&m,&s);
	for(int i=1,u,v,w;i<=m;++i) {
		scanf("%d%d%d",&u,&v,&w);
		edge[u].push_back({v,w});
	}
	if(!SPFA(s))
		return puts("NO"),0;
	for(int i=1;i<=n;++i)
		printf("%d ",dis[i]==inf?2147483647:dis[i]);
	return 0;
}

Dijkstra

首先,在这个算法中,有两种点,一种是已经确定最短路径的点(又称为蓝点),一种是尚未确定最短路径的点(又称为白点)。蓝点的最短路径不会再改变。

我们可以写一个代码框架:

memset(dis,inf,sizeof(dis));
dis[s]=0;
for(int i=1;i<=n;++i) {
    int u=当前白点里dis值最小的;
    将u设为蓝点;
    for(每一条从u出去的边u->v)
        dis[v]=min(dis[v],dis[u]+(u->v)这条边的权值); //更新其它点的最短路径,也叫松弛
}

这样为什么就能得出从起点出发所有点的最短路径呢?

首先,所有点都是蓝点。起点到起点的最短路径为0。

然后要找出一个当前没有到达过的点(白点)中路径最短的点,记为 u。将 u 设为蓝点确定它的最短路径)。

有人又要问了:为什么当前最短路径最短的白点,可以转化为蓝点呢?

反正法:反正就是这样

反证法:

如果 u 的 dis 值还能更新,那么考虑从 s 到它的最短路径,其倒数第二个点,一定是现在的白点(如果它的 dis 值还能更新,又不是由蓝点更新的,自然就是由白点更新的喽)。设这个白点为 v。

那么既然 u 是当前 dis 值最小的,\(dis_{v} > dis_{u}\),那 \(dis_v + value_{v -> u}\) 怎么会 \(> dis_{u}\) 呢?

证毕

有人又要问了:万一 $ value_{v -> u} < 0 $ 呢?

负权边这种毒瘤东西,也不是不存在的。

怎么解决呢?答案就是( )

无法解决!

我们的巨佬们,都没有解决这个问题(也许屏幕面前的巨佬您就能解决)

事实上 Dijkstra 算法只能解决正权图上的最短路问题问题!

复杂度分析:外层一个循环,去除最小值一个循环,所以时间复杂度就是 $ O(n^{2}) $

我们还可以优化!!!

事实上,Dijkstra算法可以将时间复杂度降至 $ O(n \log n + m) $。

想一想,是哪个地方使算法运行得不够快?

(评测机(确信))

对于取出当前白点里dis值最小的这一步,我们可以用 堆优化 !!!

这样就可以 AC P4779 【模板】单源最短路径(标准版)

示例代码:

#include 
#include 
#include 
#include 
using namespace std;
const int inf=0x3f3f3f3f;
const int N=1e5+7;

vector > edge[N];

int dis[N];

int n,m,s;

inline void Dijkstra(int s) {
    priority_queue > q; // 堆优化
    memset(dis,inf,sizeof(dis));
    dis[s]=0;
    q.push({0,s}); // 初始化
    while(!q.empty()) {
        pair c=q.top();
        q.pop(); // 取出堆中dis最小的点
        if((-c.first)!=dis[c.second])  // 如果它被修改了,就不可以被设为蓝点,先continue
            continue;
        int u=c.second;
        for(int i=0,v,w;idis[u]+w) { // 更新
                dis[v]=dis[u]+w;
                q.push(make_pair(-dis[v],v)); // 用负数弄小根堆
            }
        }
    }
}
signed main() {
    scanf("%d%d%d",&n,&m,&s);
    for(int i=1,u,v,w;i<=m;++i) {
        scanf("%d%d%d",&u,&v,&w);
        edge[u].push_back({v,w});
    }
    Dijkstra(s);
    for(int i=1;i<=n;++i)
        printf("%d ",dis[i]);
    return 0;
}

例题:

P3371 【模板】单源最短路径(弱化版)

P4779 【模板】单源最短路径(标准版)

P3385 【模板】负环

P1576 最小花费

P1629 邮递员送信

先用正图做一遍1为起点 Dijkstra ,再建反图跑n遍以1为终点的 Dijkstra

反图:把所有 \(u \to v\) 的边改成 \(v \to u\)

P6175 无向图的最小环问题

本题可以用用 Floyd 算法

首先这一定是一个简单环。

想一想这个环是怎么构成的。

在枚举 \(k_{i}\) 的时候,我们已经得到了前 \(k-1\) 个点的最短路径

f[x][y]\((k,x), (k,y)\)共同构成了环。

所以连接 $ x \to y \to k \to x $ ,我们就得到了一个经过 \(x , y , k\) 的最小环

在 Floyd 的过程中枚举 k,计算这个和的最小值即可。

#include
#define min(a,b) ((a)<(b)?(a):(b))
using namespace std;
typedef long long ll;
const int inf=1e13;
const int N=1e2+7;

ll dis[N][N],mp[N][N];

ll ans=inf;
int n,m;

signed main() {
	scanf("%d%d",&n,&m);
	for(register int i=1;i<=n;i++)
		for(register int j=1;j<=n;j++)
			if(i!=j)
				dis[i][j]=mp[i][j]=inf; // 初始化
	for(int i=1,u,v,w;i<=m;++i) {
		scanf("%d%d%d",&u,&v,&w);
		dis[u][v]=dis[v][u]=min(dis[u][v],w);
		mp[u][v]=mp[v][u]=min(mp[u][v],w); // 判断重边
	}
	for(int k=1;k<=n;++k) { // 枚举中转点
		for(int i=1;i

P1144 最短路计数

将每条边的边权设为1,加一个计数装置进去,具体细节看代码

#include 
#include 
#include 
#include 
using namespace std;
const int inf=0x3f3f3f3f;
const int mod=100003;
const int N=1e6+7;

vector edge[N];

int dis[N],ans[N];
bool inque[N];

int n,m;

inline void SPFA() {
	queue q;
	memset(dis,inf,sizeof(dis));
	dis[1]=0;
	inque[1]=true;
	ans[1]=1;
	q.push(1);
	while(!q.empty()) {
		int u=q.front();
		q.pop();
		inque[u]=false; // 取出队首的点
		for(int i=0,v;idis[u]+1) {
				dis[v]=dis[u]+1;
				ans[v]=ans[u]; // 更新答案
				if(!inque[v])
					q.push(v),inque[v]=true; // 入队
			}
			else if(dis[v]==dis[u]+1)
					ans[v]=(ans[u]+ans[v])%mod; // 更新答案
		}
	}
}

signed main() {
	scanf("%d%d",&n,&m);
	for(int i=1,u,v;i<=m;++i) {
		scanf("%d%d",&u,&v);
		edge[u].push_back(v);
		edge[v].push_back(u); // 建边
	}
	SPFA();
	for(int i=1;i<=n;++i)
		printf("%d\n",ans[i]);
	return 0;	
}

P1744 采购特价商品

简单的最短路,边权设置为它们的欧几里得距离