文章目录
- 最短路径问题的定义:搜索,单源和多源
- 搜索问题
- 单源最短路径
-
- Dijsktra算法
-
- 例题
- [朴素实现: O ( n 2 ) O(n^2) O(n2)](#朴素实现: O ( n 2 ) O(n^2) O(n2))
- [堆优化版本: O ( m l o g n ) O(mlogn) O(mlogn)](#堆优化版本: O ( m l o g n ) O(mlogn) O(mlogn))
- Bellman_ford算法
-
- [用法:负权重图的最短路问题 和 负环路](#用法:负权重图的最短路问题 和 负环路)
- 例题
- [朴素实现: O ( n m ) O(nm) O(nm)](#朴素实现: O ( n m ) O(nm) O(nm))
- [SPFA: O ( m ) − O ( n m ) O(m)-O(nm) O(m)−O(nm)](#SPFA: O ( m ) − O ( n m ) O(m)-O(nm) O(m)−O(nm))
最短路径问题的定义:搜索,单源和多源
- 搜索中的最短路
- 单源最短路问题
- 多源最短路问题
搜索问题
参考:图论·搜索最短路径
- BFS:权值为1情况下的最短路径
- A*:权值不为1的情况下的最短路径,启发式算法
单源最短路径
Dijsktra算法
- 三部曲:贪心选点 ,加入集合 ,更新距离
例题
朴素实现: O ( n 2 ) O(n^2) O(n2)
cpp
int n, m, g[509][509], visited[509], dist[509];
int dj() {
dist[1] = 0;
for (int i = 1; i <= n; i++) {
int idx = -1, temp = MAX_VALUE;
for (int j = 1; j <= n; j++) {
if (!visited[j] && dist[j] < temp) {
temp = dist[j];
idx = j;
}
}
if (idx == -1)break;
visited[idx] = 1;
for (int j = 1; j <= n; j++) {
if (!visited[j] && g[idx][j] != MAX_VALUE) {
dist[j] = min(dist[j], dist[idx] + g[idx][j]);
}
}
}
return dist[n] == MAX_VALUE ? -1 : dist[n];
}
堆优化版本: O ( m l o g n ) O(mlogn) O(mlogn)
- 朴素实现的问题:每次都需要手动遍历寻找dist[i]最小的节点加入数组 ,每一次都要遍历邻接矩阵中所有终点的边(存在无效遍历)。
- 使用堆来获得dist[i]的数组 ,使用邻接表来优化邻接矩阵的存储(减少无效边的遍历)。
- 堆中存储当前加入节点和离起点的距离。
时间复杂度分析
时间复杂度分析:更新邻接矩阵所有边,时间复杂度为 O ( m ) O(m) O(m),然后不需要遍历所有节点,通过堆来加入节点,堆中元素至多为m个(将所有相邻边的节点加入),因此取出和插入操作复杂度为 O ( l o g m ) = O ( l o g n ) O(logm)=O(logn) O(logm)=O(logn),因此总共复杂度为 O ( m l o g n ) O(mlogn) O(mlogn)
链表前向星
- 额外定义w数组,w[i]表示存储与i位置的节点(存储于i != 节点i)的边权值。
cpp
int n, m;
int h[150009],node[150009],w[150009],nxt[150009], len = 0,dist[150009],visited[150009];
class cmp {
public:
bool operator()(const pair<int,int> &a ,const pair<int,int>&b) {
return a.second > b.second;
}
};
priority_queue<pair<int,int>, vector<pair<int,int>>, cmp>q;
void insert(int x, int y, int z) {
len++;
node[len] = y;
w[len] = z;
nxt[len] = h[x];
h[x] = len;
}
int dj() {
dist[1] = 0;
q.push(make_pair(1,0));
while (q.size()) {
pair<int,int> cur = q.top();
q.pop();
if (visited[cur.first])continue;
visited[cur.first] = 1;
for (int i = h[cur.first]; i != -1; i = nxt[i]) {
int v = node[i];// node's number
int weight = w[i];
if (!visited[v]&& dist[cur.first] + weight <dist[v]) {
dist[v] = dist[cur.first] + weight;
q.push(make_pair(v, dist[v]));
}
}
}
return dist[n] == MAX_VALUE ? -1 : dist[n];
}
STL链表实现
cpp
int n, m;
vector<list<pair<int,int>>>g(1500009);
vector<int>visited(1500009, 0);
vector<int>dist(1500009, MAX_VALUE);
class cmp {
public:
bool operator()(const pair<int,int>&a, const pair<int,int>&b) {
return a.second > b.second;//small top heap
}
};
priority_queue<pair<int, int>, vector<pair<int, int>>, cmp>q;
int dj() {
dist[1] = 0;
q.push({ 1,0 });// node, dist
while (q.size()) {
auto cur = q.top();
q.pop();
if (visited[cur.first]) {
continue;
}
visited[cur.first] = 1;
for (auto item : g[cur.first]) {
if (!visited[item.first]&&
dist[cur.first] + item.second < dist[item.first]
) {
dist[item.first] = dist[cur.first] + item.second;
q.push({ item.first,dist[item.first] });
}
}
}
return dist[n] == MAX_VALUE ? -1:dist[n];
}
失效情况:边权值为负数
简要证明思路和理解如下:
- 贪心假设:加入到当前集合中的节点都是离起点的距离最短。
- 情况分析:假设有一个边的权值为负数,但是它还没有被加入到当前集合中 (例如,离当前集合中所有点在图上的"距离"相对较远)。
- 失效案例:假设该边的权值为-无穷 ,加入到当前集合中可以使得当前集合中所有点到出发点的距离更新为负无穷 ,贪心假设不成立(这说明当前集合中所有点到出发点的距离不一定最短),矛盾!
Bellman_ford算法
- 松弛操作:
dist[j]=dist[i]+graph[i][j] - 相当于动态规划,dist[j]的距离减少 ,但是到dist[j]的路径长度增加1,引入中间节点减少距离。
用法:负权重图的最短路问题 和 负环路
- 可以用于负权重图的单源最短路
- 可以用于检测是否存在负环路 :如果第n+1次更新有效的话,那么图中一定存在负环路。
例题
- 853. 有边数限制的最短路:有边数限制的含负权重图==只能使用BF算法
朴素实现: O ( n m ) O(nm) O(nm)
- 遍历每一个边进行松弛即可
- 为了控制最短距离中路径的长度,需要定义一个二维数组,确保使用的是之前的数据。
- 注意
dist[i][j]表示起点1 到点i长度<=j的路径所需要的最短距离。
cpp
int n, m, k;
typedef struct node {
int x, y, z;
};
node edges[10009];
int dist[509][509];
void bf() {
for (int i = 0; i <= k; i++)dist[1][i] = 0;
for (int i = 1; i <= k; i++) {
for (int j = 1; j <= m; j++) {
int x = edges[j].x, y = edges[j].y, z = edges[j].z;
// 避免路径长度超过限制:例如 i=1时, x,y y,z 按顺序加入, 此时dist[z]的长度最短,但是不符合dp数组的定义(路径长度超过1).
dist[y][i] = min(dist[y][i], dist[x][i - 1] + z);
}
}
if (dist[n][k] > (MAX_VALUE / 2))cout << "impossible";
else cout << dist[n][k];
}
void solve() {
cin >> n >> m >> k;
memset(dist, 0x3F, sizeof dist);
for (int i = 1; i <= m; i++) {
int a, b, c;
cin >> a >> b >> c;
edges[i] = { a,b,c };
}
bf();
}
SPFA: O ( m ) − O ( n m ) O(m)-O(nm) O(m)−O(nm)
- 优化思路:动态规划的更新方法中存在一些无效操作:例如如果dist[x][i-1]在第i-1轮中没有更新,那么这一步更新操作可以省略。
- 我们可以只记录更新过后的节点 ,将其加入到队列中,确保所有的更新操作都是高效的。
- 注意这里的节点可以重复加入。
cpp
int n, m;
vector < list<pair<int, int>>>g(100009);// node, weight
queue<int>q;
int visited[100009],dist[100009];
void spfa() {
dist[1] = 0;
q.push(1);
while (q.size()) {
int cur = q.front();
q.pop();
// 允许重复更新
visited[cur] = 0;
for (auto item : g[cur]) {
if (dist[cur] + item.second < dist[item.first]) {
dist[item.first] = dist[cur] + item.second;
// 确保不存在多个重复的结点数
if (!visited[item.first]) {
q.push(item.first);
}
}
}
}
if (dist[n] > (MAX_VALUE / 2)) {
cout << "impossible";
}
else cout << dist[n];
}