最短路专题
最短路问题 \(\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\)。
算法流程
- 从\(T\)集合中,选取一个最短路长度最小的结点,移到\(S\)集合中。
- 对那些刚刚被加入\(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\)的最短路径(当然这个状态可以后续滚动数组优化),那么可以轻松的求出状态转移方程:
时间复杂度分析
三层循环分别枚举\(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];