算法适用范围
约定:n是点的数量,m是边的数量
n
2
n^2
n2 与 m 是相同级别的是稠密图
稠密图使用朴素Dijkstra算法O(
n
2
n^2
n2),若使用堆优化的就是
n
2
l
o
g
n
n^2logn
n2logn
n, m <
10
e
5
10e5
10e5 时使用堆优化即可
有向图与无向图使用算法是没有区别的
- 单源最短路指从一个点到其他点的最短路
- 多源汇最短路指可能有多个询问问任意两点的最短路
- 自环的处理方法是将它初始化为零
- 重边就是保留最小的边
提示:例题中的注释是精华
朴素的Dijkstra算法
实现步骤
s:当前已确定最短距离的点
- dist[i] = 0, dist[i] = ∞ \infty ∞
- for (int i = 0; i < n; i ++)
- 找到不在s中的距离最近的点
- t加到s中
- 用t更新其他点的距离
模板
稠密图用邻接矩阵来存
int g[N][N]; // 存储每条边
int dist[N]; // 存储1号点到每个点的最短距离
bool st[N]; // 存储每个点的最短路是否已经确定
// 求1号点到n号点的最短路,如果不存在则返回-1
int dijkstra()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
for (int i = 0; i < n - 1; i ++ )
{
int t = -1; // 在还未确定最短路的点中,寻找距离最小的点
for (int j = 1; j <= n; j ++ )
if (!st[j] && (t == -1 || dist[t] > dist[j]))
t = j;
// 用t更新其他点的距离
for (int j = 1; j <= n; j ++ )
dist[j] = min(dist[j], dist[t] + g[t][j]);
st[t] = true;
}
if (dist[n] == 0x3f3f3f3f) return -1;
return dist[n];
}
时间复杂是 O( n 2 n^2 n2 + m)
/*
Dijkstra算法 是将初始点初始为零,其他所有点初始为正无穷
首先是在所有点中寻找到源点距离最短的点,由于距离源点较远的点未被遍历所以距离时正无穷,符合从根源开始找的人类思维
而找到这个最小的点就说明从源点到这个点是最优的,那么更新从源点到这个点相邻的点(使其到源点距离最短)g[t][j] != 0x3f3f3f说明连通,更新从t点走的距离如果更短就更新
如果在经历了n - 1次(最后一次集合只剩1个点无需遍历直接拿出)相当于每个结点都有找到的机会,如果这样还为找到说明无最短路
*/
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 510;
int g[N][N], dist[N];// g[i][j]表示结点i和结点j的边的权重, dist[i] 表示第i个节点从源点到它的最短距离
bool st[N]; // st[N] 表示已经确定的最短距离了的结点
int n, m;
int dijkstra()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
for (int i = 0; i < n - 1; i ++)
{
int t = -1; // 初始化开始是的确定最短距离了的结点
for (int j = 1; j <= n; j ++)
if (!st[j] && (t == -1 || dist[j] < dist[t]))
t = j;
st[t] = true;// 找到下一个要走的结点,它到源点的距离最短
for (int j = 1; j <= n; j ++)
dist[j] = min(dist[j], dist[t] + g[t][j]);// 如果从t走时到j的距离更小,就走t,否则有其他的路, 当未连通时 0x3f3f3f 一定小于 0x3f3f3f 加一个数仍未连通
}
if (dist[n] > 0x3f3f3f >> 1) return -1; // 最后dist[n]的距离可能会改变,但不会变太多
else return dist[n];
}
int main ()
{
memset(g, 0x3f, sizeof g);
cin >> n >> m;
for (int i = 0; i < m; i ++)
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
g[a][b] = min(g[a][b], c); // 重边的处理办法,只保留最短的边
}
printf("%d", dijkstra());
return 0;
}
堆优化的Dijkstra算法
内容
稀疏图使用邻接表存储
由于朴素的Dijkstra算法中最费时间的是在图中遍历找到距离起点最小的结点的步骤上,所以可以使用堆来存储结点,这样每次就可以以O(1)的时间复杂度找到距离最小的结点
但是,这样的化每次修改邻接的点的距离时就会引起堆结构的变化O(mlogn)的时间复杂度
模板
时间复杂度 n l o g n nlogn nlogn
最小堆写法:priority_queue<PII, vector, greater > heap;
typedef pair<int, int> PII;
int n; // 点的数量
int h[N], w[N], e[N], ne[N], idx; // 邻接表存储所有边
int dist[N]; // 存储所有点到1号点的距离
bool st[N]; // 存储每个点的最短距离是否已确定
// 求1号点到n号点的最短距离,如果不存在,则返回-1
int dijkstra()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
priority_queue<PII, vector<PII>, greater<PII>> heap;
heap.push({0, 1}); // first存储距离,second存储节点编号
while (heap.size())
{
auto t = heap.top();
heap.pop();
int ver = t.second, distance = t.first;
if (st[ver]) continue;
st[ver] = true;
for (int i = h[ver]; i != -1; i = ne[i])
{
int j = e[i];
if (dist[j] > distance + w[i])
{
dist[j] = distance + w[i];
heap.push({dist[j], j});
}
}
}
if (dist[n] == 0x3f3f3f3f) return -1;
return dist[n];
}
总结:与写BFS模板类似
例题
AcWing 850. Dijkstra求最短路 II
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;
typedef pair<int, int> PII;
const int N = 1e6 + 10;
int h[N], e[N], w[N], ne[N], idx = 0;
int dist[N];
bool st[N];
int n, m;
void add (int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++;// 邻接表初始化时记得加入权重参数
}
int dijkstra()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
priority_queue<PII, vector<PII>, greater<PII>> heap;
heap.push({0, 1});// first表示距离, second表示节点编号,这是因为在优先队列中是优先按元祖第一个元素进行排序
while (heap.size())
{
auto t = heap.top();
heap.pop();// 记得踢掉,不然循环听不了
int ver = t.second, distance = t.first;// ver表示节点编号
if (st[ver])continue;
st[ver] = true;
for (int i = h[ver]; ~i; i = ne[i])
{
int j = e[i];
if (dist[j] > distance + w[i])// 因为要遍历Ver相连的所有边i所以提前将源点到ver的最短距离记作distance, 而w[i]记录的是第i个节点到j的距离(权重)i是与ver相连的边
// 将与ver相连的边更新为最短路径值,j是i的下一条边是一个指针关系
{
dist[j] = distance + w[i];
heap.push({dist[j], j});
}
}
}
if (dist[n] == 0x3f3f3f3f) return -1;
else return dist[n];
}
int main ()
{
cin >> n >> m;
memset(h, -1, sizeof h);
for (int i = 0; i < m; i ++)// 注意是输入边的次数,呜呜
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add (a, b, c);
}
printf("%d", dijkstra());
return 0;
}
Bellman-Ford算法
处理有负权边的图
但是不能存在负环,最短路径不一定存在
如果第n次迭代还存在更新说明存在负环
有边数限制的题目只能用Bellman-Ford算法
实现步骤
- 迭代k次
迭代k次指不超过k条边的最短距离
- for 循环所有的边 a -> b(w) (a 到 b 权重为w)
- 更新最短路径 dist[b] = min(dist[b], dist[a + w]) // 松弛操作
更新完之后满足 dist[b] <= dist[a] + w
模板
时间复杂度:O(nm)
当存在次数限制时就需要进行备份,一次迭代的过程中会遍历所有边,这会导致最小值的变化,而对下次更新造成影响
//注意在模板题中需要对下面的模板稍作修改,加上备份数组,详情见模板题。
int n, m; // n表示点数,m表示边数
int dist[N]; // dist[x]存储1到x的最短路距离
struct Edge // 边,a表示出点,b表示入点,w表示边的权重
{
int a, b, w;
}edges[M];
// 求1到n的最短路距离,如果无法从1走到n,则返回-1。
int bellman_ford()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
// 如果第n次迭代仍然会松弛三角不等式,就说明存在一条长度是n+1的最短路径,由抽屉原理,路径中至少存在两个相同的点,说明图中存在负权回路。
for (int i = 0; i < n; i ++ )
{
for (int j = 0; j < m; j ++ )
{
int a = edges[j].a, b = edges[j].b, w = edges[j].w;
if (dist[b] > dist[a] + w)
dist[b] = dist[a] + w;
}
}
if (dist[n] > 0x3f3f3f3f / 2) return -1; // 因为无穷可能被负权边更新所以用大于进行判断不能用等于
return dist[n];
}
/*
bellman_ford算法从每条边出发来对结点进行优化,利用三角不等式进行松弛操作
每轮都要对所有边进行松弛操作,每轮至少确定一个点的最优值,所以最坏情况要进行n - 1轮操作
当所有边都满足松弛操作时说明找到了最短路径
bellman_ford算法适用于限制查询次数的情况下,其他情况都逊于spaf算法
*/
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 510, M = 10010;
struct Edge
{
int a, b, c;
}edges[M];// bellman_ford算法是将边作为基本点,去找结点所以存储的是结点的信息
int dist[N]; // 同其他几个算法一样表示到源点的最短路径值
int last[N];// 由于这道题中限制了次数所以要进行备份`超出次数的可行方案`(优化的结点路径,但是这个次数超了,要回复到这个操作之前的值)
int n, m, k;// k是限制的次数
int bellman_ford()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
for (int i = 0; i < k; i ++ )// 呜呜,限制了次数,要循环k次
{
memcpy(last, dist, sizeof dist);
for (int j = 0; j < m; j ++)
{
int a = edges[j].a, b = edges[j].b, c = edges[j].c;// 更新经过这条边的后面的结点用前面结点后权重
if (dist[b] > last[a] + c)
dist[b] = last[a] + c;
}
}
return dist[n];
}
int main()
{
cin >> n >> m >> k;
for (int i = 0; i < m; i ++)
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
edges[i] = {a, b, c};
}
bellman_ford();
if(dist[n] > 0x3f3f3f3f / 2) puts("impossible"); // 可能将dist[n] 中的值改变但是改变的幅度不会太大
else printf("%d", dist[n]);
return 0;
}
SPFA算法
每一次不一定都会对dist[b]进行更新只有当dist[a]变小时才会进行更新,因此用bfs 进行优化,将发生变下的结点加入队列
实现步骤
- 现将起点加入队列
- while (queue不空)
- f <- q.front()
- q.pop()
- 更新t的所有出边,如果更新成功的话加入队列
模板
时间复杂度平均情况是O(m),最坏情况是O(nm)
int n; // 总点数
int h[N], w[N], e[N], ne[N], idx; // 邻接表存储所有边
int dist[N]; // 存储每个点到1号点的最短距离
bool st[N]; // 存储每个点是否在队列中,防止存重复的点
// 求1号点到n号点的最短路距离,如果从1号点无法走到n号点则返回-1
int spfa()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
queue<int> q;
q.push(1);
st[1] = true;
while (q.size())
{
auto t = q.front();
q.pop();
st[t] = false;
for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
if (dist[j] > dist[t] + w[i])
{
dist[j] = dist[t] + w[i];
if (!st[j]) // 如果队列中已存在j,则不需要将j重复插入
{
q.push(j);
st[j] = true;
}
}
}
}
if (dist[n] == 0x3f3f3f3f) return -1;
return dist[n];
}
负环的判断
设置dist[x]最短距离
设置cnt[x] 到最短路的边数
每次更新最短距离时:
- dist[x] = dist[t] + w[i]
- cnt[x] = cnt[t] + 1
例题
求最短路
/*
spfa算法相当于是将bellman_ford算法的改良用队列来进行维护每条边
因此邻接表中存储的是边的信息
算法与Dijkstra算法类似
*/
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;
const int N = 100010;
int h[N], w[N], e[N], ne[N], idx = 0;// 邻接表用来存边的信息
int dist[N];
bool st[N]; // 区别是st[]数组判断的是这条边是否用过
int n, m;
void add (int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++;
}
void spfa()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
queue<int> q;
q.push(1);
st[1] = true;
while (q.size())
{
auto t = q.front();
q.pop();
st[t] = false;// 已经弹出队列中没你了
for (int i = h[t]; ~i; i = ne[i])
{
int j = e[i];
if (dist[j] > dist[t] + w[i])
{
dist[j] = dist[t] + w[i];
if(!st[j])
{
q.push(j);
st[j] = true;
}
}
}
}
}
int main ()
{
memset (h, -1, sizeof h);
cin >> n >> m;
for (int i = 0; i < m; i ++)
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, c);
}
spfa();
if(dist[n] > 0x3f3f3f3f / 2) puts("impossible");
else printf("%d", dist[n]);
return 0;
}
判断负环
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;
const int N = 2010, M = 10010;
int h[N], e[M], w[M], ne[M], idx = 0;
int dist[N], cnt[N];
bool st[N];
int n, m;
void add (int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++;
}
bool spfa()
{
queue<int> q;
// 由于本体是求是否存在负环,那么就不能这从1开始遍历,因为1可能在负环的后面
for (int i = 1; i <= n; i ++)
{
st[i] = true;// 因此将所有节点都加入到队列中进行遍历
q.push(i);
}
while(q.size())
{
auto t = q.front();
q.pop();
st[t] = false;
for (int i = h[t]; ~i; i = ne[i])
{
int j = e[i];
if (dist[j] > dist[t] + w[i])
{
dist[j] = dist[t] + w[i];
cnt[j] = cnt[t] + 1;// 每走一次就加一次,最多也只会走n次因为有n个节点
if (cnt[j] >= n) return true;// 当大于n是说明存在自环
if (!st[j])
{
q.push(j);
st[j] = true;
}
}
}
}
return false;
}
int main()
{
memset(h, -1, sizeof h);
cin >> n >> m;
for (int i = 0; i < m; i ++)
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, c);
}
puts(spfa() ? "Yes" : "No");
return 0;
}
Floyd算法
实现多源汇最短路
算法也可以处理负权边
实现步骤
用邻接矩阵存储所有的边
- 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))
模板
时间复杂度是O( n 3 n^3 n3)
初始化:
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= n; j ++ )
if (i == j) d[i][j] = 0;
else d[i][j] = INF;
// 算法结束后,d[a][b]表示a到b的最短距离
void floyd()
{
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]);
}
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 210, INF = 0x3f3f3f3f;
int d[N][N]; // d[i][j][k] 表示从i到j必须经过k的最短路径的值
int n, m, q;
void floyd()
{
for (int k = 1; k <= n; k ++)//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]);// 简记:k是进过的点所以在中间
}
int main()
{
cin >> n >> m >> q;
// 初始化 => memset
for (int i = 1; i <= n; i ++)
for (int j = 1; j <= n; j ++)
{
if (i == j)d[i][j] = 0;// 干掉自环
else d[i][j] = INF;
}
for (int i = 0; i < m; i ++)
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
d[a][b] = min(d[a][b], c);// 干掉重边
}
floyd();
while (q --)
{
int start, end;
scanf("%d%d", &start, &end);
if(d[start][end] > INF / 2) puts("impossible");
else printf("%d\n", d[start][end]);
}
return 0;
}
参考文献
学习自AcWing