最短路专题

最短路问题 \(\longrightarrow\) 差分约束

\(\rm{BFS}\)

算法详解

\(\rm{BFS}\) 全称是 Breadth First Search,中文名是宽度优先搜索,也叫广度优先搜索。

所谓宽度优先。就是每次都尝试访问同一层的节点。 如果同一层都访问完了,再访问下一层。

\(\rm{BFS}\)算法是以起点为中心,成放射性向外计算。具体地讲,就是先遍历起点的临界点,再重复遍历那些临界点的临界点,从而实现求解最短路。在 \(\rm{BFS}\) 结束时,每个节点都是通过从起点到该点的最短路径访问的。

\(\rm{BFS}\)算法又有多种衍生算法,如\(\rm{A}^*\)\(\rm{IDA}^*\),双向\(\rm{BFS}\),启发式搜索。但这些都不是重点。

算法流程

1、将起点入队,标记
2、将起点的邻接点入队,标记,计算出起点邻接点距起点的最短路
3、起点出队
4、将队列中点的邻接点入队,标记
5、出队
6、重复第4步和第5步

具体请看代码

算法分析

用于解决边权全部相等或者无边权的最短路

时间复杂度分析

  • 图论问题中
    • 邻接表存储中\(\rm{BFS}\)会遍历图中的每一个点和每一条边,所以时间复杂度为\(\Theta(v+e)\)
    • 邻接矩阵存储中\(\rm{BFS}\)会遍历图中的每一个点以及它的临界点,所以时间复杂度为\(\Theta(v^2)\)
  • 棋盘网格问题中
    • \(\rm{BFS}\)会遍历到网格中的每一个点,所以时间复杂度为\(\Theta(nm)\)

模板代码

#include <bits/stdc++.h>

using namespace std;

const int maxn = 1e5 + 10;

int n, m; // n个点, m条边
vector< int > edge[maxn]; // 邻接表存图

bool vis[maxn]; // vis标记数组, 用来标记走过的点
int dist[maxn]; // dist记录每个点距起点的最短路
queue< int > q; // 用于BFS进出点

void bfs(int s){ // s是起点
    dist[s] = 0; // 起点到自己的距离为0
    q.push(s); // 入队
    vis[s] = true; // 走过了, 标记为true
    
    while(!q.empty()){ // 只要还有点没有遍历完
        int u = q.front(); q.pop(); // 从队列中取出一个点, 以这个点为起点向外扩展, 找到这个点的所有邻接点
        for(auto i : edge[u]){ // 遍历这个点的邻接点
            int v = edge[u][i];
            dist[v] = dist[u] + 1; // 计算u的邻接点v距起点的最短路, v距起点的距离就是u距起点的距离+1或者加上相等的边权
            q.push(v); // 入队, 以后再计算v的邻接点的距离
            vis[v] = true; // 走过了, 标记
        }
    }
}
// 最后dist数组里存的就是每个点到起点s的距离

int main(){
    
    return 0;
}

\(\rm{dijkstra}\)

朴素\(\rm{dijkstra}\)

算法详解

\(\rm{dijkstra算法}\)算法是根据已经求出最短路径的点来更新没有算出最短路径的点。

说一下松弛操作,这点在\(\rm{Bellman-Ford}\)算法中也会提及。对于边\((u,v)\),松弛操作对应下面的式子:\(dis_u = min(dis_v, dis_u+w(u,v))\)

\(\rm{dijkstra}\)算法中,用两个点击集合\(S\)\(T\)分别表示已经求出最短路的还未求出最短路的\(dis\)数组记录到起点的最短路径。为了方便,将起点编号设为\(s\)

最开始,\(dis_s=0\),因为自己到自己的最短路径为\(0\);其他所有\(dis\)都赋值成\(+\infty\)

算法流程
  1. \(T\)集合中,选取一个最短路长度最小的结点,移到\(S\)集合中。
  2. 对那些刚刚被加入\(S\)集合的结点的所有出边执行松弛操作。

直到\(T\)集合为空,算法结束。

算法分析

\(\rm{dijkstra}\)是一种求解非负权图单源最短路径的算法,不能处理存在负边权的图。

时间复杂度分析

朴素\(\rm{dijsktra}\)的时间复杂度为\(\Theta(n^2 + m)\)

模板代码
#include <bits/stdc++.h>

using namespace std;

const int maxn = 1e4 + 10;

int n, m;
int e[maxn][maxn];

int dis[maxn];
bool vis[maxn];

void dijkstra(){
    // 初始化
	memset(dis, 0x3f, sizeof dis);
	dis[1] = 0;

	for (int i = 1; i <= n; i++) { // 遍历每一个点
		int t = -1; // 记录邻接点中最短路长度最小的点
		for (int j = 1; j <= n; j++) {
			if (!vis[j] && (t == -1 || dis[t] > dis[j])) {
				t = j; //未标记的里面最小的点 
			}
		}
		for (int j = 1; j <= n; j++) {
			dis[j] = min(dis[j], dis[t] + e[t][j]); // 通过最短路径最小的点更新其他点的最短路径
		}
		vis[t] = true;
	}
} 

int main(){
    
    return 0;
}

堆优化\(\rm{dijkstra}\)

算法分析

我们在第二层第一个for循环中找的是未标记的里面最短路径最小的点,这个过程是可以用堆,也就是优先队列进行优化。

时间复杂度分析

堆优化的\(\rm{dijkstra}\)算法时间复杂度为\(\Theta(mlogn)\)

#include <bits/stdc++.h>

using namespace std;

const int maxn = 1e5 + 10;

int n, m;
vector< int, pair< int, int > > edge[maxn]; // {u, v, w}

int dis[maxn];
bool vis[maxn];

void dijkstra(){
	memset(dis, 0x3f, sizeof dis);
	dis[1] = 0;

	priority_queue< pair< int, int >, vector< pair< int, int > >, greater< pair< int, int > > > q;
	// first 存距离, second存点的编号 
	q.push(make_pair(0, 1));
	while(!q.empty()){
		pair< int, int > tmp = q.top();
		q.pop();
		int ver = tmp.second, dist = tmp.first; // 取出最短路径最小的点
		if(vis[ver]) continue;
		vis[ver] = true;
		for(auto u : edge[ver]){ // 遍历最短路径最小的点的邻接点
			int v = edge[ver][u].first, w = edge[ver][u].second;
			if(dis[v] > dis[ver] + w){
				dis[v] = dis[ver] + w;
				q.push(make_pair(dis[v], v));
			}
		}
	}	
}  
int main(){
    
    return 0;
} 

\(\rm{Bellman-Ford}\)

算法详解

\(\rm{Bellman-Ford}\)算法就是不断地对图上的所有边进行松弛操作。每进行一轮循环,就对图上的每一条边做一次松弛,直到找不到可以松弛的边时,此时所有边即为最短路。

算法分析

\(\rm{Bellman-Ford}\)是一个单源最短路算法,并且它还能判负环(负环就是一个边权和为负数的环)。

时间复杂度分析

在一个图中(存在最短路,即无负环),最短路的边数最多为\(n-1\)条,所以需要\(n-1\)轮松弛操作,每次松弛操作遍历\(m\)条边,故时间复杂度为\(\Theta(nm)\)

算法应用

刚刚说了,\(\rm{Bellman-Ford}\)算法可以判断负环,那它究竟是如何判断负环的呢?

我们知道,如果当前遍历到负环了,那么这个环中的最短路就会无限小,那么就会在这个负环中无线绕下去。上面说过,如果不存在负环的情况下只需跑\(n-1\)轮就可以求出最短路,那么如果存在负环的话,他会重复执行下去,那么在\(n\)轮时判断一下,如果还有能被松弛的边,那么就说明有负环。

模板代码

#include <bits/stdc++.h>

using namespace std;

const int maxn = 1e5 + 10;

int n, m;
struct Edge{
    int u, v, w;
}edge[maxn];

int dis[maxn], dis2[maxn]; // 利用dis2来松弛dis

void Bellman_Ford(int s){
	// 初始化
	memset(dis, 0x3f, sizeof dis);
	dis[s] = 0;
	for (int i = 1; i < n; i++) { // n - 1轮
		memcpy(dis2, dis, sizeof dis); // 先将dis复制给dis2
		for(int j = 1; j <= m; j++){ // 遍历m条边
			int a = edge[j].u, b = edge[j].v, c = edge[j].w; // 取出m条边的数据
			dis[b] = min(dis[b], dis2[a] + c); // 进行松弛
		}
	} 
} 

int main(){
    
    return 0;
}

\(\rm{SPFA}\)

算法详解

\(\rm{Bellman-Ford}\)算法中,不难发现,每一轮的遍历有些是没必要的。通俗点讲,就是只有最短距离被更新的点,才有资格更新其他的点的最短距离。由此,出现了\(\rm{Bellman-Ford}\)队列优化算法:\(\rm{SPFA}\)(虽然它死了)

先来讲讲为什么只有最短距离被更新的点,才有资格更新其他的点的最短距离。反过来想,如果当前点的最短距离没有改变,那么通过它进行松弛操作明显是没有意义的,不会改变整个最短路;只有它的最短距离改变,才会影响到它邻接点的最短路。

\(\rm{SPFA}\)算法是在\(\rm{Bellman-Ford}\)算法的基础上,引入了队列来进行优化,每次只有最短路径被更新的点入队,每次只有队列里的点的邻接点才有可能被遍历到,大大优化了时间复杂度。

时间复杂度分析

均摊时间复杂度为\(\Theta(m)\) ,最坏情况下(即菊花图、网格图、完全图中)时间复杂度为\(\Theta(nm)\)

算法应用

\(\rm{Bellman-Ford}\),可以判负环。在此不多赘述。

模板代码

#include <bits/stdc++.h>

using namespace std;

const int maxn = 1e5 + 10;

int n, m;
int head[maxn], e[maxn], c[maxn], ne[maxn];

int dis[maxn];
bool vis[maxn];

void spfa(int s){
	memset(dis, 0x3f, sizeof dis);
	dis[s] = 0;
	queue< int > q;
	q.push(s);
	vis[s] = 1;
	while (!q.empty()) {
		int tmp = q.front();
		q.pop();
		vis[tmp] = false;
		for(int i = head[tmp]; ~i; i = ne[i]){
			int j = e[i];
			if(dis[j] > dis[tmp] + c[i]){
				dis[j] = dis[tmp] + c[i];
				if(!vis[j]){
					q.push(j);
					vis[j] = true;
				}
			}
		}
	}
} 

int main(){
    
    return 0;
}

\(\rm{floyd}\)

算法详解

\(\rm{floyd}\) ,是一个解决多源最短路的算法。运用了动态规划思想,定义\(f[k][i][j]\),为从\(i\)\(j\)经过点\(k\)​的最短路径(当然这个状态可以后续滚动数组优化),那么可以轻松的求出状态转移方程:

\[f[k][i][j]=min(f[k-1][i][j],f[k-1][i][k]+f[k-1][k][j]) \]

时间复杂度分析

三层循环分别枚举\(k,i,j\),所以时间复杂度是\(\Theta(n^3)\)

模板代码

void floyd(){
	for (int k = 1; k <= n; k++)
		for (int i = 1; i <= n; i++)
			for(int j = 1; j <= n; j++)
				f[i][j] = min(f[i][j], f[i][k] + f[k][j]);
	
}

算法应用

\(\rm{floyd}\)有个典型的运用叫做传递闭包,用来判断两点是否联通,用bitset优化时间复杂度可以达到\(\Theta(\frac{n^3}{64})\)

给个代码:

bitset< maxn > f[maxn];
for (k = 1; k <= n; k++)
  for (i = 1; i <= n; i++)
    if (f[i][k]) f[i] = f[i] | f[k];
posted @ 2025-05-17 10:10  a_noooooob  阅读(24)  评论(0)    收藏  举报
———————————————— 版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 原文链接:https://blog.csdn.net/LogicYarn/article/details/141198668