C++刷题周记(四)——最短路问题

目录

Dijkstra算法

朴素版dijkstra

堆优化版dijkstra

bellman-ford算法

SPFA算法

代码模板

与以上算法的分析比较

模板练习题:

Floyd算法


Dijkstra算法

适用范围:单源 无负权边

dijkstra算法是基于贪心的思想,每次选择至起点距离最近的点,去更新与其相邻的点,之后不再访问,不会回头。其因为不会回头这一性质,无法处理带负权边的图

朴素版dijkstra

针对稠密图使用时,时间性能还不算很差

模板题:Acwing 849

int g[N][N];// 该图为稠密图 用邻接矩阵来存储
// 用于存储每个点到起点的最短距离
int dist[N];
// 记录是否找到了起点到该节点的最短距离
bool state[N];

int dijkstra(){
    // 将距离初始化为最大值
    memset(dist,0x3f,sizeof dist);
    // 起点为1号点
    dist[1] = 0;
    for(int i = 0;i < n; i++){
        // 取-1 处理每次循环第一次找到的临界情况
        int t = -1;
        // *1 寻找未确定状态下离起点 距离最小 的结点
        // 存在优化空间-此处遍历了全部数来寻找最小值
        for(int j = 1;j <= n; j++){
            if(!state[j] && (t == -1 ||dist[t] > dist[j]) ){
                t = j;
            }
        }
        // *2 更新 这个结点的确定状态
        state[t] = true;
        // *3 更新 在该节点确定后的 各节点的最短路径
        // 其实是只需遍历更新 与节点t相邻各节点 的最短路径
        // 但是根据稠密图的性质 直接遍历所有节点更加方便
        for(int j = 1; j <= n; j++){
            dist[j] = min(dist[j],dist[t] + g[t][j]);
        }
    }
    if(dist[n] == 0x3f3f3f3f)
        return -1;
    return dist[n];
}

为什么使用 0x3f3f3f3f 表示无穷大?
①.对于memset赋值操作方便,memset(dist,0x3f,sizeof dist)   按字节操作赋值为0x3f3f3f3f

②. 0x3f3f3f3f数量级达到了10^9,一般图的边权达不到9次方,认为其为无穷大没有问题

③.因为松弛操作经常需要先将两边的权值相加后再使用min来进行比较,若使用严格无穷大0x7fffffff,两个无穷大的边权相加会发生溢出。0x3f3f3f3f很好地解决了该问题

算法的主要耗时的步骤是 *1

即从dist 数组中选出  未确定状态下离起点 距离最小 的结点

只是找个最小值而已,没有必要每次遍历一遍dist数组。

在一组数中每次能很快的找到最小值,很容易想到使用小根堆,我们考虑使用STL的优先队列对该算法步骤进行优化, 可将该步骤时间复杂度 从O(n^2) 降为了 O(1)

堆优化版dijkstra

**最常用的!只要无负权边的图,皆优先使用该方法模板!(SPFA存不了过大的图,且可能会被卡)

模板题:Acwing 850

int h[N],e[N],ne[N],w[N],idx;
// 稀疏图使用邻接表存储
int dist[N];
// 是否找到了起点到该节点的最短距离
bool state[N];

int dijkstra(){
    memset(dist,0x3f,sizeof dist);
    dist[1] = 0;
    // 定义小根堆 求 未确定状态下到起点距离最小的节点
    priority_queue<PII,vector<PII>,greater<PII> > heap;
    // PII{至起点的距离,点编号}
    // 因为优先队列默认比较first
    heap.push({0,1});// 放入起点
    
    while(heap.size()){
        auto t = heap.top();
        heap.pop();
        int num = t.second;
        // 因为有重边 所以找到最小边时要给点打标记
        if(state[num]) continue;
        state[num] = true;
        
        // 遍历所有与num号点有相邻边的点
        for(int i = h[num];i!=-1; i = ne[i]){
            int j = e[i];//相邻点的序号
            if(dist[j] > w[i] + t.first){
                dist[j] = w[i] + t.first;
                heap.push({dist[j],j});
            }
        }
    }
    return (dist[n] == 0x3f3f3f3f)?-1 :dist[n];
}

参考资料:AcWing 850. 朴素Dijkstra与堆优化Dijkstra总结 - AcWing

AcWing 849. Dijkstra求最短路 I:图解 详细代码(图解) - AcWing

bellman-ford算法

适用范围:单源 有负权边 判断负环    PS:效率低 

bellman_ford算法无需对自环与重边进行额外处理(参考资料:Bellman_ford算法 - AcWing)

同时也可以存在负权回路,因为它求得的最短路是有限制的,是限制了边数的,这样不会永久的走下去,会得到一个解;
SPFA算法各方面优于该算法,但是在碰到限制了最短路径上边的长度时就只能用bellman_ford了,此时直接把n重循环改成k次循环即可(即以下模板题)

模板题:Acwing 853. 有边数限制的最短路

// 该算法无非就是循环n次然后遍历所有的边,因此不需要做什么特别的存储
// 只要把所有的边的信息存下来能够遍历即可
struct Edge{
    int a,b,w;
}edges[M];

int bellman_ford(){
    memset(dist,0x3f,sizeof dist);
    dist[1] = 0;
    // 根据限制 遍历k次
    for(int i = 0;i < k; i++){
        // 使用backup:避免给a更新后立马更新b, 这样b一次性最短路径就多了两条边出来
        memcpy(backup,dist,sizeof dist);
        // 遍历所有边,而朴素dijkstra是遍历所有顶点n*n
        // spfa主要优化此步骤 是没有必要遍历所有边的
        for(int j = 1;j <= m; j++){
            int a = edges[j].a, b = edges[j].b, w = edges[j].w;
            dist[b] = min(dist[b],backup[a] + w);
        }
    }
    return dist[n];
}

练习题:lc 787. K 站中转内最便宜的航班

SPFA算法

shortest path faster algorithm  适用范围:单源 有负权边 判断负环

代码模板

模板题1:Acwing 851 (设起点为1,终点为n,求最短路代码)

注意:state数组的含义与dijkstra的不一样 需要辨析

int h[N],e[N],ne[N],w[N],idx;
bool state[N];
// 表示当前是否放入队列里的状态
int spfa(){
    memset(dist,0x3f,sizeof dist);
    // 定义起点为1
    dist[1] = 0;
    queue<int> q;q.push(1);
    state[1] = true;
    
    while(q.size()){
        // 对队头进行松弛操作
        int t = q.front();
        q.pop();
        // 队头出队后更新状态
        state[t] = false;
        // 遍历所有与当前节点相邻的点
        for(int i = h[t];i != -1;i = ne[i]){
            int j = e[i];
            // 更新最短距离
            // 因为该点至起点的距离缩短了
            // 所以应该入队 对该点相邻继续进行更新
            if(dist[j] > dist[t] + w[i]){
                dist[j] = dist[t] + w[i];
                // state的作用:
                // 如果此时该点已在队列中则无需重复入队 该点出队更新一次即可
                if(!state[j]){//更新状态
                    q.push(j);
                    state[j] = true;
                }
            }
        }
    }
    return dist[n];
}

模板题2:Acwing 852 spfa判断负环

与以上模板的差别在于:需要维护cnt数组 记录每个点至起点的路径边数,同时初始将图中所有点全部加入队列中(若不全部加入,只能判断从起点至终点的所有路径是否存在负环,存在遗漏)

int h[N],e[N],ne[N],w[N],idx;
int dist[N],cnt[N];
bool state[N];
bool spfa(){
    queue<int> q;
    for(int i = 1;i <= n; i++){
        q.push(i);
        state[i] = true;
    }
    while(q.size()){
        int t = q.front();
        q.pop();
        state[t] = false;
        for(int i = h[t];i != -1;i = ne[i]){
            int j = e[i];
            if(dist[j] > dist[t] + w[i]){
                dist[j] = dist[t] + w[i];
                cnt[j] = cnt[t] + 1;
                if(cnt[j] >= n)
                    return false;
                if(!state[j]){
                    q.push(j);
                    state[j] = true;
                }
            }
        }
    }
    return true;
}

参考资料:AcWing 851. spfa和bellman-ford的区别,以及和djikstra的区别 - AcWing

AcWing 851. spfa求最短路---图解--$\color{red}{海绵宝宝来喽}$ - AcWing

与以上算法的分析比较

与Dijsktra比较

dijkstra算法是基于贪心的思想,每次选择至起点距离最近的点,去更新与其相邻的点,之后不再访问,不会回头。其因为不会回头这一性质,无法处理带负权边的图

所以优化版的dijkstra基于的是优先队列,小根堆保证了能以O(1)的代价得到距离最近的点,满足了贪心思想。

而spfa算法,只要有某个点的距离被更新了,就把它加到队列中,去更新其它点,每个点都有被重复加入队列的可能(即可以回头),所以可以解决负权边的问题。

与bellman-ford比较

Bellman-ford算法不管三七二十一,遍历n次/k次,每次遍历图中的所有边(m条边),所以时间复杂度为O(n*m)

遍历每条边的时候,进行松弛操作,把入度的点的距离更新成最小。
然而,这样就循环遍历了很多用不到的边。比如第一次遍历,只有第一个点的临边是有效的。

spfa算法主要就是对该过程进行优化得来的

模板练习题:

Acwing 1127 (堆优化迪杰与SPFA均可解决)

lc 743. 网络延迟时间   (以上两种方法均可)

Floyd算法 

基本思想:递推产生一个dist[k][i][j],表示 i -> j的路径长度,k表示绕行第k个顶点的运算步骤。若i与j之间存在边,则以此边上的权值作为它们之间的最短路径长度;若不存在边,先用正无穷表示。

之后逐步尝试在原路径中加入顶点k作为中间顶点。若增加中间顶点k后路径长度更短,则更新dist

适用范围:多源最短路  有负权边  (ps 时间复杂度高 O(n^3)  )

原理:基于动态规划思想  参考资料:AcWing 854. Floyd闫式dp分析法 - AcWing

 因为k为最外层的遍历,所以可以将状态转移方程进行压缩,忽略k这一维度

dist[i][j] = min(dist[i][j],dist[i][k] + dist[k][j])

 模板题:Acwing 854 

// 稠密图使用邻接矩阵来求
// d[i][j]-点i至点j的最短路
int d[N][N];
// n为图的点数
void floyd(){
    for(int k = 1; k <= n; k++){
        for(int i = 1; i <= n; i++){
            for(int j = 1; j <= n; j++){
                d[i][j] = min(d[i][j],d[i][k]+d[k][j]);
            }
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值