0x01 单源最短路径
单源最短路径:给定一个图 G = ( V , E ) G=(V,E) G=(V,E),我们希望找到从给定源节点 s ∈ V s\in V s∈V到每个节点 v ∈ V v\in V v∈V的最短路径。
注意:这篇文章的主要作用是代码模板记录,算法不做证明,证明过程参看算法导论!!!
0x0101 Bellman-Ford算法
Bellman-Ford算法的思路非常简单,我们观察下面的图:
我们此时知道0->1
的距离
d
01
d_{01}
d01、0->2
的距离
d
02
d_{02}
d02和1->2
的距离
d
12
d_{12}
d12。我们发现
d
01
+
d
12
<
d
02
d_{01}+d_{12}<d_{02}
d01+d12<d02,那么此时就可以缩短
d
02
d_{02}
d02。那么,对于从
u
u
u节点到
v
v
v节点都可以采用这种方法缩短距离,我们定义该操作为
r
e
l
a
x
(
u
,
v
)
relax(u,v)
relax(u,v)。
Bellman-Ford算法的思路就是对图的每条边进行 ∣ V ∣ − 1 |V|-1 ∣V∣−1次处理,每次处理对所有边都进行 r e l a x ( u , v ) relax(u,v) relax(u,v)操作。如果对图的所有边再进行一次 r e l a x ( u , x ) relax(u,x) relax(u,x)处理,如果存在可以缩短的距离,说明存在负权环。显然算法的时间复杂度就是 O ( V E ) O(VE) O(VE),至于算法的证明可以参看算法导论。
int n; // 点的数量
int dist[N]; // 存储所有点到1号点的距离
struct Edge { // 边,a表示出点,b表示入点,w表示边的权重
int a, b, w;
}edges[M];
// 判断是否存在负权环
bool 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;
}
}
for (int i = 0; i < m; i++)
{
int a = edges[i].a, b = edges[i].b, w = edges[i].w;
if (dist[b] > dist[a] + w) return false;
}
return true;
}
0x0102 DAG算法
首先通过拓扑排序确定节点之间的一个线性次序。如果有向无环图包含从节点 u u u到节点 v v v的一条路径,则 u u u在拓扑排序的次序中位于节点 v v v的前面。我们只需要按照拓扑排序的次序对节点进行一遍处理即可。每次对一个节点进行处理时,我们对该节点出发的所有的边进行 r e l a x ( u , v ) relax(u,v) relax(u,v)操作。
代码如下,我们通过数组模拟邻接表来表示图。
int n, m;
int h[N], e[M], w[M], ne[M], idx;
int d[N], seq[N], dist[N]; //d:当前点入度 seq:topsort结果 dist:存储所有点到1号点的距离
queue<int> q;
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
bool topsort()
{
int cnt = 0;
for (int i = 1; i <= n; i ++ )
if (!d[i]) q.push(i);
while (!q.empty())
{
int t = q.front(); q.pop();
seq[cnt++] = t;
for (int i = h[t]; ~i; i = ne[i])
{
int j = e[i];
if (--d[j] == 0)
q.push(j);
}
}
return cnt == n;
}
void DAG()
{
topsort();
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
for (int i = 0; i < n; i++)
{
int u = seq[i];
for (int j = h[u]; ~j; j = ne[j])
{
int v = e[j];
if (d[v] > d[u] + w[j])
{
d[v] = d[u] + w[j];
}
}
}
}
int main()
{
cin >> n >> m;
memset(h, -1, sizeof h);
memset(d, 0, sizeof d);
for (int i = 0; i < m; i++)
{
int a, b, c;
cin >> a >> b >> c;
add(a, b, c);
d[b]++;
}
}
0x0103 Dijkstra算法
Dijkstra算法解决的是带权重的有向图上单源最短路径问题,该问题要求所有边的权重都为非负值。该算法运行过程中维持的关键信息是一组节点集合 S S S(记录哪些点被访问过)。通过最小优先队列 Q Q Q维护 V − S V-S V−S,首先将其初始化(通过初始点),然后从 Q Q Q中抽取最短距离点 v v v,将其添加到 S S S集合中。算法重复从节点集 V − S V-S V−S中选择最短路径估计最小的节点 v v v,将 v v v加入到集合 S S S,然后对所有从 u u u发出的边进行松弛。
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())
{
int t = heap.top();
heap.pop();
int v = t.second, distance = t.first;
if (st[v]) continue;
st[v] = true;
for (int i = h[v]; ~i; i = ne[i]) //v发出的边进行松弛
{
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];
}
0x0104 SPFA算法
SPFA算法实际上就是队列优化的Bellman-Ford算法。Bellman-Ford算法中针对每个点,都要更新一遍所有边的最短距离。实际上,这里面包好了未更新的点去更新其他未更新的点(有点绕)的重复运算。所有我们可以通过记录哪些点遍历过,哪些点没有遍历过来加速Bellman-Ford算法。
SPFA算法最优情况下的时间复杂度是 O ( M ) O(M) O(M),最差和Bellman-Ford算法一样。
int n; // 总点数
int h[N], w[M], e[M], ne[M], idx; // 邻接表存储所有边
int dist[N], cnt[N]; // dist[x]存储1号点到x的最短距离,cnt[x]存储1到x的最短路中经过的点数
bool st[N]; // 存储每个点是否在队列中
// 如果存在负环,则返回true,否则返回false。
bool SPFA()
{
queue<int> q;
for (int i = 1; i <= n; i ++ )
{
q.push(i);
st[i] = true;
}
while (!q.empty())
{
int 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;
// 如果从1号点到x的最短路中包含至少n个点(不包括自己),则说明存在环
if (cnt[j] >= n) return true;
if (!st[j])
{
q.push(j);
st[j] = true;
}
}
}
}
return false;
}
0x0105 总结
处理环 | 处理负权边 | 时间复杂度 | |
---|---|---|---|
Bellman-Ford | 可以 | 可以 | O(N*M) |
DAG | 不可以 | 可以 | O(N+M) |
Dijkstra | 可以 | 不可以 | O(MlogN) |
SPFA | 可以 | 可以 | O(M)~O(N*M) |
0x02 所有节点对最短路径
所有节点对最短路径:对于每个节点 u u u和 v v v,找到从节点 u u u到节点 v v v的最短路径。
0x0201 Floyd算法
Floyd可以处理不包含负权环的节点对最短路径问题。思路如下:
设 d i j k d_{ij}^{k} dijk为从节点 i i i到节点 j j j的所有中间节点全部取自集合 { 1 , 2 , . . . k } \{1,2,...k\} {1,2,...k}的一条最短路径。当 k = 0 k=0 k=0的时候,从节点 i i i到节点 j j j的一条不包括编号大于 0 0 0的中间节点的路径将没有任何中间节点。这样的路径最多只有一条边,因此, d i j 0 = w i j d_{ij}^{0}=w_{ij} dij0=wij。对于包含中间节点的路径,可以使用 r e l a x relax relax进行放缩。所以
- d i j 0 = w i j d_{ij}^{0}=w_{ij} dij0=wij
- d i j k ! = 0 = m i n ( d i j k − 1 , d i k k − 1 + d k j k − 1 ) d_{ij}^{k!=0}=min(d_{ij}^{k-1},d_{ik}^{k-1}+d_{kj}^{k-1}) dijk!=0=min(dijk−1,dikk−1+dkjk−1)
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;
while (m--)
{
cin >> x >> y >> z;
d[x][y] = min(d[x][y], z); //可能有重复边,保存最小的边
}
// 算法结束后,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]);
}
reference:
https://www.acwing.com/blog/content/405/
https://www.geeksforgeeks.org/dijkstras-shortest-path-algorithm-greedy-algo-7/
https://baike.baidu.com/item/SPFA%E7%AE%97%E6%B3%95/8297411?fromtitle=SPFA&fromid=11018124&fr=aladdin