算法【A星、Floyd、Bellman-Ford、SPFA】

一、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算法的模版。代码如下。

#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有一个轮数,可以想到,用轮数对应中转次数。但是并不是完全对应,因为在一轮里面可以中转多次,所以在进行松弛操作的时候,代码需要调整一下。代码如下。

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,就可以判断是否存在负环。代码如下。

#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数组按照上面讲的优化设置;当退出循环后,对轮数进行判断,从而得出是否存在负环。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

还有糕手

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值