目录
2、SPFA算法(Shortest Path Faster Algorithm)
一、dijkstra 算法
dijkstra算法:在有权图(权值非负数)中求从起点到其他节点的最短路径算法
注意:
- dijkstra 算法可以同时求 起点到所有节点的最短路径
- 权值不能为负数
- 时空复杂度均为 O(N^2),其中 N 为节点个数
算法步骤(与prim算法类似):
- 选择最近的未访问节点
- 访问最近节点
- 更新其余未访问节点到源点的距离(即更新minDist数组)
原题卡码47 47. 参加科学大会(第六期模拟笔试)
1、dijkstra算法(朴素版)
int dijkstra()
{
int n, m, s, e, v;
cin >> n >> m;
vector<vector<int>> graph(n + 1, vector<int>(n + 1, INT_MAX));
for (int i = 0; i < m; i++)
{
cin >> s >> e >> v;
graph[s][e] = v;
}
// 起点和终点
int start = 1;
int end = n;
vector<int> minDist(n + 1, INT_MAX); // 记录源点到每个点的最短路径
vector<bool> visited(n + 1, false); // 记录是否访问过
minDist[start] = 0;// 初始化,源点到自身距离为 0
for (int i = 1; i <= n; i++)
{
int minVal = INT_MAX;
int cur = -1;
// 选距离源点最近且未访问过的节点
for (int j = 1; j <= n; j++)
{
if (!visited[j] && minDist[j] < minVal)
{
minVal = minDist[j];
cur = j;
}
}
// 没有一个节点可以抵达了
if (cur == -1)
{
break;
}
// 访问该节点
visited[cur] = true;
// 更新 minDist 数组
for (int j = 1; j <= n; j++)
{
if (!visited[j] && graph[cur][j] != INT_MAX && minDist[cur] + graph[cur][j] < minDist[j])
{
minDist[j] = minDist[cur] + graph[cur][j];
}
}
}
// 终点不可达返回 -1
if (minDist[end] == INT_MAX)
{
return -1;
}
return minDist[end];
}
2、dijkstra算法(堆优化)
优化思路是使用邻接表来存储图,使用优先队列(小顶堆)来存放距离源点距离最小的边
时间复杂度为 O(ElogE),其中 E 为边的数量。每条边遍历了一次,为 O(E),弹出并重新排序小顶堆,为 O(logE)
空间复杂度为 O(N+E),其中 N 为节点的数量
// 小顶堆,堆顶为距离源点距离最小的边
class cmp
{
public:
bool operator()(const pair<int, int>& a, const pair<int, int>& b)
{
return a.second > b.second; // 注意比较方向
}
};
struct Edge
{
int to; // 指向的节点
int val; // 边的权值
Edge(int to, int val) : to(to), val(val)
{}
};
int dijkstraHeap()
{
int n, m, s, e, v;
cin >> n >> m;
vector<list<Edge>> graph(n + 1);
for (int i = 0; i < m; i++)
{
cin >> s >> e >> v;
graph[s].push_back(Edge(e, v));
}
// 起点和终点
int start = 1;
int end = n;
vector<int> minDist(n + 1, INT_MAX); // 记录源点到每个点的最短路径
vector<bool> visited(n + 1, false); // 记录是否访问过
// 优先队列存放距源点距离最小的边 pair<节点编号,源点到该节点的权值>
priority_queue<pair<int, int>, vector<pair<int, int>>, cmp> pq;
// 初始化,源点到自身距离为 0
pq.push({start,0});
minDist[start] = 0;
while (!pq.empty())
{
// 选距离源点最近且未访问过的节点
pair<int, int> cur = pq.top();
pq.pop();
if (visited[cur.first])
{
continue;
}
// 访问该节点
visited[cur.first] = true;
// 更新 minDist 数组
for (Edge edge : graph[cur.first]) // 遍历当前节点连接的节点
{
if (!visited[edge.to] && edge.val + minDist[cur.first] < minDist[edge.to])
{
minDist[edge.to] = edge.val + minDist[cur.first];
pq.push({edge.to,minDist[edge.to]});
}
}
}
// 终点不可达返回 -1
if (minDist[end] == INT_MAX)
{
return -1;
}
return minDist[end];
}
二、Bellman_ford算法
主要用于求解带有负权值的单源最短路径问题
核心思路是对所有边进行n-1次松弛操作(n为节点数量),详见 代码随想录
1、原版 Bellman_ford 算法
原题卡码94 94. 城市间货物运输 I
时间复杂度:O(N*E) , N 为节点数量,E 为图中边的数量
空间复杂度: O(N) ,即 minDist 数组所开辟的空间
int Bellman_ford()
{
int n, m, s, e, v;
cin >> n >> m;
vector<vector<int>> graph;
for (int i = 0; i < m; i++)
{
cin >> s >> e >> v;
graph.push_back({s,e,v});
}
// 起点和终点
int start = 1;
int end = n;
vector<int> minDist(n + 1, INT_MAX); // 记录源点到每个点的最短路径
minDist[start] = 0; // 初始化,源点到自身距离为 0
for (int i = 1; i < n; i++) // 对所有边松弛 n-1 次
{
for (vector<int>& edge : graph)
{
int from = edge[0];
int to = edge[1];
int price = edge[2];
// 松弛操作
// minDist[from] != INT_MAX 防止从未计算过的节点出发
if (minDist[from] != INT_MAX && minDist[to] > minDist[from] + price)
{
minDist[to] = minDist[from] + price;
}
}
}
return minDist[end];
}
2、SPFA算法(Shortest Path Faster Algorithm)
即 Bellman_ford 队列优化算法(Queue improved Bellman-Ford)
核心:只需要对上一次松弛的时候更新过的节点作为出发节点所连接的边进行松弛
对此,使用队列记录上次更新过的节点
算法时间复杂度不稳定,取决于图的具体情况。图越稠密,越接近原版 Bellman_ford 算法;反之,效率越高。一般来说,SPFA 的时间复杂度为 O(K * N),其中 K 为不定值,因为节点需要进入几次队列取决于图的稠密度。如果图是一条线形图且单向的话,每个节点的入度为 1 ,那么只需要加入一次队列,这样时间复杂度就是 O(N)
struct Edge
{
int to; // 链接的节点
int val; // 边的权值
Edge(int to, int val) :to(to), val(val)
{}
};
int SPFA()
{
int n, m, s, e, v;
cin >> n >> m;
vector<list<Edge>> graph(n + 1);
for (int i = 0; i < m; i++)
{
cin >> s >> e >> v;
graph[s].push_back(Edge(e, v));
}
// 起点和终点
int start = 1;
int end = n;
vector<int> minDist(n + 1, INT_MAX); // 记录源点到每个点的最短路径
minDist[start] = 0; // 初始化,源点到自身距离为 0
queue<int> que; // 记录上次更新过的节点
que.push(start);
while (!que.empty())
{
int cur = que.front();
que.pop();
for (Edge edge : graph[cur])
{
int from = cur;
int to = edge.to;
int val = edge.val;
if (minDist[to] > minDist[from] + val)
{
minDist[to] = minDist[from] + val;
que.push(to);
}
}
}
return minDist[end];
}
3、存在负权回路情况下的 Bellman_ford 算法
原题卡码95 95. 城市间货物运输 II
核心:在原版 Bellman_ford 的基础上,再松弛一次,即总共松弛 n 次,如果结果发生了变化,就存在负权回路
时空复杂度与原版相同
拓展:若使用SPFA,可以记录节点入队次数,如果某节点入队次数大于n-1,存在负权回路
int Bellman_ford_haveCircle()
{
int n, m, s, e, v;
cin >> n >> m;
vector<vector<int>> graph;
for (int i = 0; i < m; i++)
{
cin >> s >> e >> v;
graph.push_back({s,e,v});
}
// 起点和终点
int start = 1;
int end = n;
vector<int> minDist(n + 1, INT_MAX); // 记录源点到每个点的最短路径
minDist[start] = 0; // 初始化,源点到自身距离为 0
bool flag = false; // 是否存在负权回路
for (int i = 1; i <= n; i++)
{ // 对所有边松弛 n 次
for (vector<int>& edge : graph)
{
int from = edge[0];
int to = edge[1];
int price = edge[2];
// 松弛操作
if (i < n)
{
// minDist[from] != INT_MAX 防止从未计算过的节点出发
if (minDist[from] != INT_MAX && minDist[to] > minDist[from] + price)
{
minDist[to] = minDist[from] + price;
}
}
else
{ // 再松弛一次,能找到更短的即为有负权回路
if (minDist[from] != INT_MAX && minDist[to] > minDist[from] + price)
{
flag = true;
}
}
}
}
if (flag) cout << "circle" << endl; //有负权回路
else if (minDist[end] == INT_MAX) cout << "unconnected" << endl; // 不能到达终点
else cout << minDist[end] << endl; // 到达终点最短路径
return 0;
}
4、单源有限最短路径下的 Bellman_ford 算法
原题卡码96 96. 城市间货物运输 III
核心:松弛 k + 1 次,同时使用复制数组记录上次的结果,并进行下一次的运算
时间复杂度: O(K * E) , K 为至多经过 K 个节点,E 为图中边的数量
空间复杂度: O(N) ,即 minDist 数组所开辟的空间
int Bellman_ford_withLimits()
{
int n, m, s, e, v, src, dst, k;
cin >> n >> m;
vector<vector<int>> graph;
for (int i = 0; i < m; i++)
{
cin >> s >> e >> v;
graph.push_back({s,e,v});
}
cin >> src >> dst >> k;
// 起点和终点
int start = src;
int end = dst;
vector<int> minDist(n + 1, INT_MAX); // 记录源点到每个点的最短路径
minDist[start] = 0; // 初始化,源点到自身距离为 0
vector<int> minDistCopy(n + 1, INT_MAX); // 记录上次遍历的结果
for (int i = 1; i <= k + 1; i++) // 对所有边松弛 k+1 次
{
minDistCopy = minDist; // 保存上一次的结果
for (vector<int>& edge : graph)
{
int from = edge[0];
int to = edge[1];
int price = edge[2];
// 松弛操作
// 注意使用 minDistCopy 来计算 minDist
if (minDistCopy[from] != INT_MAX && minDist[to] > minDistCopy[from] + price)
{
minDist[to] = minDistCopy[from] + price;
}
}
}
if (minDist[end] == INT_MAX) // 不能到达终点
{
cout << "unreachable" << endl;
}
else // 到达终点最短路径
{
cout << minDist[end] << endl;
}
return 0;
}
三、floyd算法(多源最短路径问题)
原题卡码97 97. 小明逛公园
思路:动态规划
定义 dp[i][j][k] = m,表示节点 i 到节点 j 以 [1...k] 集合为中间节点的最短距离为m
可以边权正负都可以解决;适合稠密图且源点较多的情况
时间复杂度:O(n^3)
空间复杂度:O(n^3)
int floyd()
{
int n, m;
cin >> n >> m;
vector<vector<vector<int>>> dp(n + 1, vector<vector<int>>(n + 1, vector<int>(n + 1, 100005)));
int u, v, w;
for (int i = 0; i < m; i++)
{
cin >> u >> v >> w;
dp[u][v][0] = w;
dp[v][u][0] = w;
}
for (int k = 1; k <= n; k++)
{
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= n; j++)
{
// i->j ,使用集合 [1...k] 的距离 =
// min(i->j ,使用集合 [1...k-1] 的距离,
// i->k ,使用集合 [1...k-1] 的距离 + k->j,使用集合 [1...k-1] 的距离)
dp[i][j][k] = min(dp[i][j][k - 1], dp[i][k][k - 1] + dp[k][j][k - 1]);
}
}
}
int q, start, end;
cin >> q;
for (int i = 0; i < q; i++)
{
cin >> start >> end;
if (dp[start][end][n] == 100005)
{
cout << -1 << endl;
}
else
{
cout << dp[start][end][n] << endl;
}
}
return 0;
}
空间优化:递推公式可以优化为
dp[i][j] = min(dp[i][j], dp[i][k] + dp[k][j]);
四、A*算法
原题卡码127 127. 骑士的攻击
详解 代码随想录
关键点:A* 算法是在 BFS 的基础上,增加了启发式函数,可以对队列中的备选节点进行优先级排序,确保每一次搜索都尽可能地往靠近目标的方向进行。缺点是难以解决目标不明确的最短路径问题,而且不一定能保证搜索出来的就是最短路,具体取决于启发式函数的实现
几种启发式函数:
- 曼哈顿距离,计算方式: d = abs(x1-x2)+abs(y1-y2)
- 欧氏距离(欧拉距离) ,计算方式:d = sqrt((x1-x2)^2 + (y1-y2)^2)
- 切比雪夫距离,计算方式:d = max(abs(x1 - x2), abs(y1 - y2))
时间复杂度:取决于启发式函数,最坏情况下,A* 退化成广搜,算法的时间复杂度是 O(n^2),n 为节点数量。最佳情况下,从起点直接到终点,时间复杂度为 O(dlogd),d 为起点到终点的深度;因为在搜索的过程中也需要堆排序,所以是 O(dlogd)。实际上 A* 的时间复杂度是介于 最优和最坏情况之间,可以非常粗略的认为 A* 算法的时间复杂度是 O(nlogn),n 为节点数量
空间复杂度:O(b^d) ,其中 d 为起点到终点的深度,b 是图中节点间的连接数量,本题因为是无权网格图,所以节点间连接数量为 4
int moves[1001][1001]; // 记录位置和对应的步数
int dirs[8][2] = {{-2,-1},{-2,1},{-1,2},{1,2},{2,1},{2,-1},{1,-2},{-1,-2}};
int b1, b2; // 终点
// F = G + H
// G = 从起点到该节点路径消耗
// H = 该节点到终点的预估消耗
struct Knight
{
int x, y; // 骑士当前坐标
int g, h, f; // 路径和,计算方式如上
bool operator < (const Knight& k) const
{ // 重载运算符, 从小到大排序
return k.f < f;
}
};
// 优先队列,保证每次取出最合适的节点进行下一步
priority_queue<Knight> que;
// A*算法的启发式函数,本题采用欧拉距离
int Heuristic(const Knight& k)
{
return (k.x - b1) * (k.x - b1) + (k.y - b2) * (k.y - b2); // 不开根号,提高精度
}
// A*算法本体
void astar(const Knight& k)
{
Knight cur, next;
que.push(k);
while (!que.empty())
{
cur = que.top();
que.pop();
if (cur.x == b1 && cur.y == b2)
{ // 到达终点
break;
}
for (const auto& dir : dirs)
{
next.x = cur.x + dir[0];
next.y = cur.y + dir[1];
if (next.x < 1 || next.x >= 1000 || next.y < 1 || next.y >= 1000)
{
continue;
}
if (!moves[next.x][next.y])
{
moves[next.x][next.y] = moves[cur.x][cur.y] + 1; // 记录步数
next.g = cur.g + dir[0] * dir[0] + dir[1] * dir[1];
next.h = Heuristic(next);
next.f = next.g + next.h;
que.push(next);
}
}
}
}
int main()
{
int n, a1, a2;
cin >> n;
while (n--)
{
cin >> a1 >> a2 >> b1 >> b2;
memset(moves, 0, sizeof(moves)); // 重置记录的位置和对应的步数
Knight start;
start.x = a1;
start.y = a2;
start.g = 0;
start.h = Heuristic(start);
start.f = start.g + start.h;
astar(start);
while (!que.empty())
{
que.pop(); // 队列清空
}
cout << moves[b1][b2] << endl;
}
return 0;
}