一、A*算法
A*算法,指定源点,指定目标点,求源点到达目标点的最短距离。增加了当前点到终点的预估函数。在堆中根据从源点出发到达当前点的距离+当前点到终点的预估距离来进行排序。剩下的所有细节和Dijskra算法完全一致。和分支限界法差不多。
预估函数要求:当前点到终点的预估距离 <= 当前点到终点的真实最短距离。
预估函数是一种吸引力
1.合适的吸引力可以提升算法的速度,吸引力过强会出现错误。
2.保证预估距离 <= 真实最短距离的情况下,尽量接近真实最短距离,可以做到功能正确且最快。
预估终点距离经常选择:曼哈顿距离、欧式距离、对角线距离。
二、Floyd算法
Floyd算法,得到图中任意两点之间的最短距离。时间复杂度O(n^3),空间复杂度O(n^2),常数时间小,容易实现。适用于任何图,不管有向无向、不管边权正负、但是不能有负环(保证最短路存在)。
过程简述:
distance[i][j]表示i和j之间的最短距离。
distance[i][j] = min ( distance[i][j] , distance[i][k] + distance[k][j])。
枚举所有的k即可,实现时一定要最先枚举跳板。
三、Bellman-Ford算法
Bellman-Ford算法,解决可以有负权边但是不能有负环(保证最短路存在)的图,单源最短路算法。
松弛操作
假设源点为A,从A到任意点F的最短距离为distance[F]。假设从点P出发某条边,去往点S,边权为W。如果发现,distance[P] + W < distance[S],也就是通过该边可以让distance[S]变小,那么就说,P出发的这条边对点S进行了松弛操作。
Bellman-Ford过程
1.每一轮考察每条边,每条边都尝试进行松弛操作,那么若干点的distance会变小。
2.当某一轮发现不再有松弛操作出现时,算法停止。
Bellman-Ford算法时间复杂度
假设点的数量为N,边的数量为M,每一轮时间复杂度O(M)。最短路存在的情况下,因为1次松弛操作会使1个点的最短路的边数+1,而从源点出发到任何点的最短路最多走过全部的n个点,所以松弛的轮数必然 <= n - 1,所以Bellman-Ford算法时间复杂度O(M*N)。
重要推广:判断从某个点出发能不能到达负环。上面已经说了,如果从A出发存在最短路(没有负环),那么松弛的轮数必然 <= n - 1。而如果从A点出发到达一个负环,那么松弛操作显然会无休止地进行下去,所以,如果发现从A点出发,在第n轮时松弛操作依然存在,说明从A点出发能够到达一个负环。
四、Bellman-Ford + SPFA优化(Shortest Path Faster Algorithm)
很轻易就能发现,每一轮考察所有的边看看能否做松弛操作是不必要的。因为只有上一次被某条边松弛过的节点,所连接的边,才有可能引起下一次的松弛操作。所以用队列来维护 "这一轮哪些节点的distance变小了"。下一轮只需要对这些点的所有边,考察有没有松弛操作即可。
SPFA只优化了常数时间,在大多数情况下跑得很快,但时间复杂度为O(n*m)。看复杂度就知道只适用于小图,根据数据量谨慎使用,在没有负权边时要使用Dijkstra算法。
Bellman-Ford + SPFA优化的用途
1.适用于小图。
2.解决有负边(没有负环)的图的单源最短路径问题。
3.可以判断从某个点出发是否能遇到负环,如果想判断整张有向图有没有负环,需要设置虚拟源点。
4.并行计算时会有很大优势,因为每一轮多点判断松弛操作是相互独立的,可以交给多线程处理。
五、练习题
下面通过一些题目加深理解。
题目一
测试链接:https://www.luogu.com.cn/problem/P2910
分析:这个就是一个Floyd算法的模版。代码如下。
cpp
#include <iostream>
using namespace std;
int N, M;
int pass[10002];
int Distance[101][101];
int main(void){
int ans = 0;
scanf("%d%d", &N, &M);
for(int i = 0;i < M;++i){
scanf("%d", &pass[i]);
}
for(int i = 1;i <= N;++i){
for(int j = 1;j <= N;++j){
scanf("%d", &Distance[i][j]);
}
}
for(int bridge = 1;bridge <= N;++bridge){
for(int i = 1;i <= N;++i){
for(int j = 1;j <= N;++j){
if(Distance[i][bridge] + Distance[bridge][j] < Distance[i][j]){
Distance[i][j] = Distance[i][bridge] + Distance[bridge][j];
}
}
}
}
for(int i = 0;i < M-1;++i){
ans += (Distance[pass[i]][pass[i+1]]);
}
printf("%d", ans);
}
其中,中间的三重for循环就是Floyd算法。
题目二
测试链接:https://leetcode.cn/problems/cheapest-flights-within-k-stops/
分析:看到这个k站中转,可以想到使用Bellman-Ford算法,因为Bellman-Ford有一个轮数,可以想到,用轮数对应中转次数。但是并不是完全对应,因为在一轮里面可以中转多次,所以在进行松弛操作的时候,代码需要调整一下。代码如下。
cpp
class Solution {
public:
vector<int> cur_distance;
int findCheapestPrice(int n, vector<vector<int>>& flights, int src, int dst, int k) {
int turn = k + 1;
int length = flights.size();
cur_distance.assign(n, -((1 << 31) + 1));
cur_distance[src] = 0;
for(int i = 0;i < turn;++i){
vector<int> next_distance = cur_distance;
for(int j = 0;j < length;++j){
if(cur_distance[flights[j][0]] != -((1 << 31) + 1)){
next_distance[flights[j][1]] = next_distance[flights[j][1]] < cur_distance[flights[j][0]] + flights[j][2] ?
next_distance[flights[j][1]] : cur_distance[flights[j][0]] + flights[j][2];
}
}
cur_distance = next_distance;
}
return cur_distance[dst] == -((1 << 31) + 1) ? -1 : cur_distance[dst];
}
};
其中,在进行松弛操作时,使用了一个next_distance数组,表示所有的条件判断采用上一轮的状态,而不是实时更新的状态,这样就可以保证轮数对应中转次数。
题目三
测试链接:https://www.luogu.com.cn/problem/P3385
分析:对于是否存在复环的判断,我们可以想到,使用Bellman-Ford算法,同时,加上SPFA优化。根据松弛轮数必然小于等于点的数量减1,就可以判断是否存在负环。代码如下。
cpp
#include <iostream>
#include <queue>
using namespace std;
int T;
bool ans[10];
int Head[2001];
int Next[6001];
int To[6001];
int Weight[6001];
int cnt;
vector<int> Distance;
vector<bool> enter;
void build(int n){
Distance.clear();
Distance.assign(n+1, -((1 << 31) + 1));
enter.clear();
enter.assign(n+1, false);
for(int i = 0;i < 2001;++i){
Head[i] = 0;
}
for(int i = 0;i < 6001;++i){
Next[i] = 0;
To[i] = 0;
Weight[i] = 0;
}
cnt = 1;
}
int main(void){
int n, m;
int u, v, w;
scanf("%d", &T);
for(int i = 0;i < T;++i){
scanf("%d%d", &n, &m);
build(n);
queue<int> q;
for(int j = 0;j < m;++j){
scanf("%d%d%d", &u, &v, &w);
Next[cnt] = Head[u];
Head[u] = cnt;
To[cnt] = v;
Weight[cnt] = w;
++cnt;
if(w >= 0){
Next[cnt] = Head[v];
Head[v] = cnt;
To[cnt] = u;
Weight[cnt] = w;
++cnt;
}
}
int turn = 0;
int num, cur;
q.push(1);
enter[1] = true;
Distance[1] = 0;
while (turn <= n-1 && !q.empty())
{
num = q.size();
for(int k = 0;k < num;++k){
cur = q.front();
q.pop();
enter[cur] = false;
for(int next = Head[cur];next != 0;next = Next[next]){
if(Distance[cur] + Weight[next] < Distance[To[next]]){
Distance[To[next]] = Distance[cur] + Weight[next];
if(enter[To[next]] == false){
q.push(To[next]);
enter[To[next]] = true;
}
}
}
}
if(q.size() > 0){
++turn;
}
}
if(turn >= n){
ans[i] = true;
}else{
ans[i] = false;
}
}
if(ans[0]){
printf("YES");
}else{
printf("NO");
}
for(int i = 1;i < T;++i){
if(ans[i]){
printf("\nYES");
}else{
printf("\nNO");
}
}
}
其中,采用链式前向星建图;distance和enter数组按照上面讲的优化设置;当退出循环后,对轮数进行判断,从而得出是否存在负环。