Day5课件
最短路
一个联通图中有n个点,m条边。边有权值(可正可负),可有向可无向。给定起点s和终点t,在所以连接s到t的路径中寻找边的权值之和最小的路径,这就是最短路径问题。
单源最短路
给定图,求一个点到所有其它点的最短路径。
Dijkstra算法:
该算法要求边权不为负,核心策略是每次选择当前距离起点最近的未确定点,用它来更新其他点的距离。
1.设置两个顶点的集合T和S:
S中存放已找到最短路径的顶点,初始状态时,集合S中只有一个顶点,即源点v0;
T中存放当前还未找到最短路径的顶点;
2.以后每求得一条最短路径(v0,…,vk),就将vk加入到顶点集合S中,直到所有的顶点都加入到集合S中,算法就结束了
算法的具体步骤
1.初始时,S中包含源点v0。
2.然后不断从T中选取到顶点v0路径长度最短的顶点u,找到后:
a)把顶点u加入到S;
b)修改顶点v0到T中剩余顶点的最短路径长度值。T中各顶点新的最短路径长度值为:min{原来的最短路径长度值, 顶点u的最短路径长度+u到该顶点的路径长度值};
3.重复2,直到T的顶点全部加入S为止。
初始化起点距离为0,其他点为无穷大,每次取出距离最小的点,用它更新邻居,重复直到剩下的顶点不可达。
vector<int> dijkstra(int n, int start, vector<vector<pair<int, int>>>& graph) {
vector<int> dist(n + 1, INT_MAX);
vector<bool> visited(n + 1, false);
dist[start] = 0;
for (int i = 1; i <= n; i++) {
// 寻找未访问的最小距离顶点
int u = -1;
for (int j = 1; j <= n; j++) {
if (!visited[j] && (u == -1 || dist[j] < dist[u])) {
u = j;
}
}
if (dist[u] == INT_MAX) break; // 剩下的顶点不可达
visited[u] = true;
// 用u更新相邻顶点
for (auto& edge : graph[u]) {
int v = edge.first;
int w = edge.second;
if (dist[v] > dist[u] + w) {
dist[v] = dist[u] + w;
}
}
}
return dist;
}
注意到上面在每次寻找未访问的最小距离节点时会遍历所有节点,这会浪费一些时间,可以使用优先队列进行优化。
vector<int> dijkstraHeap(int n, int start, vector<vector<pair<int, int>>>& graph) {
vector<int> dist(n + 1, INT_MAX);
dist[start] = 0;
// 使用优先队列(最小堆)
// pair<距离, 顶点>
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq;
pq.push({0, start});
while (!pq.empty()) {
auto [d, u] = pq.top();
pq.pop();
// 如果当前距离不是最短的,跳过
if (d > dist[u]) continue;
for (auto& edge : graph[u]) {
int v = edge.first;
int w = edge.second;
if (dist[v] > dist[u] + w) {
dist[v] = dist[u] + w;
pq.push({dist[v], v});
}
}
}
return dist;
}
朴素版本:O(V²) ,堆优化版本:O((V+E)logV)
优点:效率高,适合稠密图和稀疏图
缺点:不能处理负权边
Bellman-Ford算法:
该算法可处理负边权,但不能有负环
负环
先介绍 Bellman–Ford 算法要用到的松弛操作(Dijkstra 算法也会用到松弛操作).
对于边(u,v),松弛操作对应下面的式子:dis(v) = min(dis(v), dis(u) + w(u, v)).
这么做的含义是显然的:我们尝试用 𝑆 →𝑢 →𝑣,其中 𝑆 →𝑢的路径取最短路)这条路径去更新 𝑣点最短路的长度,如果这条路径更优,就进行更新.
Bellman–Ford 算法所做的,就是不断尝试对图上每一条边进行松弛.我们每进行一轮循环,就对图上所有的边都尝试进行一次松弛操作,当一次循环中没有成功的松弛操作时,算法停止.
每次循环是 𝑂(𝑚)
O(m) 的,那么最多会循环多少次呢?
在最短路存在的情况下,由于一次松弛操作会使最短路的边数至少+1,而最短路的边数最多为n-1,因此整个算法最多执行n-1 轮松弛操作.故总时间复杂度为𝑂(𝑛𝑚).
但还有一种情况,如果从 𝑆 点出发,抵达一个负环时,松弛操作会无休止地进行下去.注意到前面的论证中已经说明了,对于最短路存在的图,松弛操作最多只会执行n-1 轮,因此如果第n轮循环时仍然存在能松弛的边,说明从S 点出发,能够抵达一个负环.
struct Edge {
int u, v, w;
};
vector<Edge> edge;
int dis[MAXN], u, v, w;
constexpr int INF = 0x3f3f3f3f;
bool bellmanford(int n, int s) {
memset(dis, 0x3f, (n + 1) * sizeof(int));
dis[s] = 0;
bool flag = false; // 判断一轮循环过程中是否发生松弛操作
for (int i = 1; i <= n; i++) {
flag = false;
for (int j = 0; j < edge.size(); j++) {
u = edge[j].u, v = edge[j].v, w = edge[j].w;
if (dis[u] == INF) continue;
// 无穷大与常数加减仍然为无穷大
// 因此最短路长度为 INF 的点引出的边不可能发生松弛操作
if (dis[v] > dis[u] + w) {
dis[v] = dis[u] + w;
flag = true;
}
}
// 没有可以松弛的边时就停止算法
if (!flag) {
break;
}
}
// 第 n 轮循环仍然可以松弛时说明 s 点可以抵达一个负环
return flag;
}
时间复杂度:O(VE),V为顶点数,E为边数
优点:可以处理负权边,能检测负环
缺点:效率较低,不适合稠密图
记录路径
我们引入path数组用以记录当前点在最短路径中的前驱节点,每次更新某个点的最短路径值时同时更新其path值。
在Dijkstra中,我们修改更新临近节点的部分为
for (auto& edge : graph[u]) {
int v = edge.first;
int w = edge.second;
if (dist[v] > dist[u] + w) {
dist[v] = dist[u] + w;
path[v] = u;
}
}
同样,在Bellman-Ford中,可将松弛操作更改为
for (int j = 0; j < edge.size(); j++) {
u = edge[j].u, v = edge[j].v, w = edge[j].w;
if (dis[u] == INF) continue;
if (dis[v] > dis[u] + w) {
dis[v] = dis[u] + w;
path[v] = u;
flag = true;
}
}
在记录path数组后,可通过如下操作输出路径
void run(int s, int t) { //s为起点,t为终点
if(s == t) cout << s << " ";
else {
run(s, path[t]);
cout << t << " ";
}
}
简单计数
对于边权值为一的的图,最短路的值就是路径上边的个数。可以从起点开始进行bfs,由于每条边权相同,我们第一次访问某个点时就是最短路,我们从起点开始,向队列中加入距离起点为1的点,距离起点为2的点只能从距离为1的点过来,而我们在从队列中取出距离为2的点进行遍历之前我们已经处理完距离为1的点,此时我们对到距离为2的点的路径已经计数完成,依此类推即可。
vector<int> d(n + 1, 1e18), cn(n + 1, 0); //d数组记录路径长度,cn数组计数
queue<int> q;
q.push(1);
while (!q.empty()) {
auto u = q.front();
q.pop();
for (auto v : e[u]) {
if (d[v] == 1e18) { //第一次访问
q.push(v);
d[v] = d[u] + 1;
cn[v] = cn[u];
} else if (d[v] == d[u] + 1) { // 后续访问
cn[v] += cn[u];
}
}
}
多源最短路
Floyd 算法:
是用来求任意两个结点之间的最短路的.复杂度比较高,但是常数小,容易实现(只有三个 for).适用于任何图,不管有向无向,边权正负,但是最短路必须存在.(不能有个负环)。
我们定义一个数组 f[k][x][y],表示只允许经过结点 1到k(也就是说,在子图 𝑉′ =1,2,…,𝑘
中的路径,注意,𝑥
与 𝑦
不一定在这个子图中),结点 x到结点y的最短路长度.
很显然,f[n][x][y] 就是结点x到结点y的最短路长度(因为 𝑉′ =1,2,…,𝑛
即为 𝑉
本身,其表示的最短路径就是所求路径).
接下来考虑如何求出 f 数组的值.
f[0][x][y]:𝑥
与 𝑦
的边权,或者 0
,或者正无穷(f[0][x][y] 什么时候应该是 +∞
?当 𝑥
与 𝑦
间有直接相连的边的时候,为它们的边权;当x=y的时候为零,因为到本身的距离为零;当x与y没有直接相连的边的时候,为正无穷
).
f[k][x][y] = min(f[k-1][x][y], f[k-1][x][k]+f[k-1][k][y])(f[k-1][x][y],为不经过 𝑘
点的最短路径,而 f[k-1][x][k]+f[k-1][k][y],为经过了k点的最短路).
上面两行都显然是对的,所以说这个做法空间是 𝑂(𝑁3)
,我们需要依次增加问题规模(k从1到n),判断任意两点在当前问题规模下的最短路.
因为第一维对结果无影响,我们可以发现数组的第一维是可以省略的,于是可以直接改成 f[x][y] = min(f[x][y], f[x][k]+f[k][y])
vector<vector<int>> floydWarshall(int n, vector<vector<pair<int, int>>>& graph) {
// 初始化距离矩阵
vector<vector<int>> dist(n + 1, vector<int>(n + 1, INT_MAX));
// 初始化:自己到自己的距离为0,有边的直接赋值
for (int i = 1; i <= n; i++) {
dist[i][i] = 0;
for (auto& edge : graph[i]) {
int v = edge.first;
int w = edge.second;
dist[i][v] = min(dist[i][v], w); // 处理重边
}
}
// Floyd核心算法
for (int k = 1; k <= n; k++) {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (dist[i][k] != INT_MAX && dist[k][j] != INT_MAX) {
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);
}
}
}
}
return dist;
}
时间复杂度:O(V³),空间复杂度:O(V²)
优点:代码简单,可以处理负权边(但不能有负环)
缺点:时间复杂度高,只适合小规模图(V≤500)

浙公网安备 33010602011771号