什么是单元最短路
在图论中,单元最短路(Single-Source Shortest Path)问题是指在带权有向图或无向图G=(V,E)中,从给定源顶点a出发,计算到图中所有其他顶点b∈V的最短路径及其权重 的问题。
关键定义:
- 带权图:图G中的每条边e∈E都被赋予一个实数值w(e),称为边的权重或长度。
- 路径长度:对于从顶点a到b的一条路径p=(a,v₁,v₂,...,vₖ,b),其路径长度L§定义为该路径上所有边权重的总和。
- 最短路径:在所有从a到b的可能路径中,具有最小路径长度的路径称为a到b的最短路径。
而实现单元最毒单路,常见的方法有dijkstra算法 、bellman_ford算法 和spfa算法。
常规版dijkstra算法:
dijkstra算法基于贪心思想的单元最短算法,求解的是"非负权图"上单元最短路径。
算法流程:
- 准备工作:创建一个长度为 n 的数组 dist,dist[i] 表示从起点到 i 的最短路,创建一个长度为 n 的 bool 数组 st,st[i] 表示 i 位置是否已经确定了最短路。
- 初始化:
dist[1] = 0
,其余节点的 dist 值为无穷大,表示还没找到最短路 - 重复:在所有没有确定的最短路中,找出最短路长度最小的点u。打上确定最短路的标记后,对 u 的出边进行松弛操作。
- 重复步骤3,直至所有点都确定了最短路
(松弛操作:假设起点到a的最短路长度为dist[a],a到b的边权为w,那么如果dist[a] + w < dist[b],则更新b点的dist[b]。)
代码实现:
cpp
vector<pair<int, int>>edge[N];
void dijkstra()
{
//初始化
for (int i = 0; i <= n; i++)dist[i] = INF;
dist[s] = 0;
for (int i = 1; i <= n; i++)
{
//1.找出没有确定最短路的点中,当前最短路最小的点
int a = 0;
for (int j = 1; j <= n; j++)
if (!st[j] && dist[j] < dist[a])
a = j;
//2.打上标记然后松弛
st[a] = true;
for (auto& t : edge[a])
{
int b = t.first, c = t.second;
if (dist[a] + c < dist[b])
dist[b] = dist[a] + c;
}
}
}
堆优化版dijkstra算法:
在常规版的基础上,用优先级队列维护待确定最短路的节点。
算法原理:
- 准备工作:创建一个长度为 n 的 dist 数组,其中 dist[i] 表示从起点到 i 结点的最短路,创建一个长度为 n 的bool数组 st,其中 st[i] 表示 i 点是否已经确定了最短路,创建一个小根堆,维护更新后的结点(也就是需要确定最短路的结点)
- 初始化:
dist[1] = 0
,然后将{ 0,s }
加到堆里;其余结点的dist值为无穷大,表示还没有找到最短路 - 重复:弹出堆顶元素,如果该元素已经标记过,就跳过;如果没有标记过,打上标记,进行松弛操作
- 重复上述操作,直到队列中没有元素为止
cpp
priority_queue<pair<int, int>, vector<pll>, greater<pll>>heap;
//这里greater按照第一个关键字进行比较,对堆创建有问题的读者可以移步笔者之前队列相关文章
void dijkstra()
{
//初始化
memset(dist, 0x3f, sizeof dist);
dist[s] = 0;
heap.push({ 0,s });//距离,结点
while (heap.size())
{
auto t = heap.top(); heap.pop();
int a = t.second;
if (st[a])continue;
st[a] = true;
for (auto& t : edge[a])
{
int b = t.first, c = t.second;
if (dist[a] + c < dist[b])
{
dist[b] = dist[a] + c;
heap.push({ dist[b],b });
}
}
}
}
注:dijkstra算法不能出现负权边,只要出现就会出错!!!
bellman_ford算法:
为了处理负权边的相关问题,笔者这里还有一计。
不断尝试对图上每一条边进行松弛,直到所有的点都无法松弛。bellman_ford算法是一种基于松弛操作的最短路算法,可以求出有负权的图的最短路,并可以对最短路不存在的情况进行判断。
算法流程:
- 准备工作:创建一个长度为n的dist数组,其中dist[i]表示从起点到i结点的最短路
- 初始化:
dist[1] = 0
,其余结点的dist值为无穷大,表示还没有找到最短路 - 重复:每次都对所有边进行一次松弛操作
- 重复上述操作,直到所有边都不需要松弛为止
最多重复多少次松弛操作?
在最短路存在的情况下,由于一次松弛操作会使最短路的边数至少加1,而最短路的边数至少增加1,而最短边的边数最短为 n - 1.因此整个算法最多执行松弛操作 n - 1 轮。
时间复杂度为O(nm)
cpp
vector<pair<int, int>>edge[N];
void bf()
{
//初始化:
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
bool flag = false;//判断循环中有没有进行松弛操作
for (int i = 1; i <= n; i++)
{
flag = false;
for (int u = 1; u <= n; u++)//遍历所有边
{
for (auto& t : edge[u])
{
int v = t.first, w = t.second;
if (dist[u] + w < dist[v])
{
dist[v] = dist[u] + w;
flag = true;
}
}
}
if (flag)break;
}
}
spfa算法:
(这是对BF算法进行优化、改良而来的)
在BF算法中,很多时候我们并不需要那么多无用的松弛操作:
**只有上一次被松弛的节点的出边,才有可能引起下一次的松弛操作。**因此,如果用队列来维护"哪些节点可能会引起松弛操作",就能只访问必要的边,时间复杂度就可以降低。
算法流程:
- 准备工作:创建一个长度为 n 的 dist 数组,其中 dist[i] 表示从起点到 i 结点的最短路;创建一个长度为 n 的bool数组 st,其中 st[i] 表示 i 点是否已经在队列中。
- 初始化:
dist[1] = 0
,其余结点的dist值为无穷大,表示还没有找到最短路。 - 重复:每次拿出队头元素u,去掉在队中的标记,同时对u所有相连的点v进行松弛操作。如果结点v被松弛,那就放到队列中。
- 重复上述操作,直到队列中没有节点为止
代码实现:
cpp
vector<pair<int, int>>edge[N];
void spfa()
{
//初始化:
memset(dist, 0x3f, sizeof dist);
dist[s] = 0;
queue<int>q;
q.push(s);
st[s] = true;
while (q.size())
{
auto a = q.front(); q.pop();
st[a] = false;
for (auto& t : edge[a])
{
int b = t.first, c = t.second;
if (dist[a] + c < dist[b])
{
dist[b] = dist[a] + c;
if (!st[b])//如果这个点能松弛且不在队列中
{
q.push(b);
st[b] = true;
}
}
}
}
}
注意:虽然大部分情况下spfa算法跑的很快,但其极限情况下时间复杂度为O(nm),且将他卡到整个复杂度也表示很难,所以,没有负边权的情况下最好使用Dijkstra算法。
判断负环:
BF算法判断负环:
- 执行 n 轮松弛操作
- n 轮结束之后在判断一下第 n 轮是否存在松弛操作,如果有,则存在负环
代码实现:
cpp
vector<pair<int, int>>edge[N];
bool bf()
{
memset(dist, 0x3f, sizeof dist);
dist[s] = 0;
bool flag = false;
for (int i = 1; i <= n; i++)
{
flag = true;
for (int u = 1; u <= m; u++)
{
for (auto& t : edge[u])
{
int a = t.first, b = t.second;
if (dist[a] > dist[u] + b)
{
dist[a] = dist[u] + b;
flag = true;
}
}
if (!flag)break;
}
}
if (flag)return false;//说明有负环
else return true;
}
spfa算法判断负环:
- 维护一个 cnt 数组,表示从起点走到前点,经过了多少条边
- cnt[i] >= n,说明有环(cnt[i] 表示从起点走到 i 节点时经过了多少条边)
代码实现:
cpp
vector<pair<int, int>>edge[N];
bool spfa()
{
//初始化:
memset(dist, 0x3f, sizeof dist);
memset(st, false, sizeof st);
memset(cnt, 0, sizeof cnt);
dist[s] = 0;
queue<int>q;
q.push(s);
st[s] = true;
cnt[s] = 0;
while (q.size())
{
int a = q.front(); q.pop();
for (auto& t : edge[a])
{
int b = t.first, c = t.second;
if (dist[b] > dist[a] + c)
{
dist[b] = dist[a] + c;
cnt[b] = cnt[a] + 1;
if (cnt[b] >= n)return true;//有负环
if (!st[b])
{
q.push(b);
st[b] = true;
}
}
}
}
return false;
}