zoukankan      html  css  js  c++  java
  • 单源最短路进阶

    目录:

    从一道题目出发 —— Luogu 4779 - 【模板】单源最短路径(标准版)

    Bellman-Ford算法

    SPFA算法

    SLF+swap优化的SPFA(但它还是死了

    Dijkstra算法

    优先队列优化Dijkstra算法

    手写二叉堆优化Dijkstra算法

    线段树优化Dijkstra算法

    配对堆优化Dijkstra算法


    题目链接:https://www.luogu.org/problemnew/show/P4779

    题目背景

    2018 年 7 月 19 日,某位同学在 NOI Day 1 T1 归程 一题里非常熟练地使用了一个广为人知的算法求最短路。

    然后呢?

    100→60;

    Ag→Cu;

    最终,他因此没能与理想的大学达成契约。

    小 F 衷心祝愿大家不再重蹈覆辙。

    题目描述

    给定一个 N 个点,M 条有向边的带非负权图,请你计算从 S 出发,到每个点的距离。

    数据保证你能从 S 出发到任意点。

    输入格式:

    第一行为三个正整数 N,M,S。 第二行起 M 行,每行三个非负整数 ui​,vi​,wi​,表示从 ui​ 到 vi​ 有一条权值为 wi​ 的边。

    输出格式:

    输出一行 N 个空格分隔的非负整数,表示 S 到每个点的距离。

    输入样例#1:
    4 6 1
    1 2 2
    2 3 2
    2 4 1
    1 3 5
    3 4 3
    1 4 4
    输出样例#1:
    0 2 4 3

    题解:

    这是一道最短路的模板题,但是它卡SPFA,还卡某些优化不好的SPFA,所以本题我们要使用SLF+swap优化的SPFA。

    (顺便再用堆优化dijkstra)


    首先回顾一下Bellman-Ford算法:

    ①初始化,所有点的 dist[i] = INF,出发点 s 的 dist[s] = 0;

    ②对于每条边 edge(u,v),若 dist[u] != INF,且 dist[v] > dist[u] + edge(u,v).w,则松弛 dist[v] = dist[u] + edge(u,v).w

    ③循环步骤② $left| V ight| - 1$ 次,或者知道某一次步骤②中没有边可以松弛,则转步骤④

    ④若存在一条边 edge(u,v),满足 dist[u] != INF,且dist[v] > dist[u] + edge(u,v).w,则图中存在负环。

    我们知道,Bellman-Ford算法的时间复杂度是 $Oleft( {left| V ight|left| E ight|} ight)$,而我们可以使用队列对其进行优化,那就是大名鼎鼎的SPFA算法,

    所以说,SPFA就是队列优化的Bellman-Ford算法。


    不妨回顾一下SPFA算法:

    ①初始化,所有点的 dist[i] = INF,源点 s 的 dist[s] = 0;构建队列,源点 s 入队,并标记该点已在队列中。

    ②队头出队,标记该点已不在队列中(若图存在负权边,则可以对该点出队次数检查,若出队次数大于 n,则存在负环,算法结束),

     遍历该点出发的所有边,假设当前遍历到某条边为 edge(u,v),若 dist[v] > dist[u] + edge(u,v).w,则松弛dist[v] = dist[u] + edge(u,v).w,

     检查节点 v 是否在队列中,若不在则入队,标记节点 v 已在队列中。

    ④重复执行步骤②直到队列为空。

    普通SPFA的TLE代码:

    #include<bits/stdc++.h>
    using namespace std;
    
    const int maxn=1e5+10;
    const int INF=0x3f3f3f3f;
    
    int n,m,s;
    
    //邻接表存图
    struct Edge{
        int u,v,w;
        Edge(int u=0,int v=0,int w=0){this->u=u,this->v=v,this->w=w;}
    };
    vector<Edge> E;
    vector<int> G[maxn];
    void addedge(int u,int v,int w)
    {
        E.push_back(Edge(u,v,w));
        G[u].push_back(E.size()-1);
    }
    
    //SPFA单源最短路
    int dist[maxn];
    bool vis[maxn];
    void spfa()
    {
    
        for(int i=1;i<=n;i++) dist[i]=INF,vis[i]=0;
        dist[s]=0;
    
        queue<int> Q;
        Q.push(s); vis[s]=1;
        while(!Q.empty())
        {
            int u=Q.front();Q.pop(); vis[u]=0;
            for(int i=0;i<G[u].size();i++)
            {
                Edge &e=E[G[u][i]];
                int v=e.v;
                if(dist[v]>dist[u]+e.w)
                {
                    dist[v]=dist[u]+e.w;
                    if(!vis[v])
                    {
                        Q.push(v); vis[v]=1;
                    }
                }
            }
        }
    }
    
    int main()
    {
        scanf("%d%d%d",&n,&m,&s);
        for(int i=1;i<=m;i++)
        {
            int u,v,w;
            scanf("%d%d%d",&u,&v,&w);
            addedge(u,v,w);
        }
        spfa();
        for(int i=1;i<=n;i++) printf("%d%s",dist[i],((i==n)?"
    ":" "));
    }

      

    但是,对于这个“队列优化”,有必要清楚的一点是:

    SPFA的时间复杂度,其实和Bellman-Ford是一样的,都是$Oleft( {left| V ight|left| E ight|} ight)$,

    只是SPFA在部分图中跑的比较快,给人以 $Oleft( {kleft| E ight|} ight)$ 的感觉(其中 $k$ 为所有点入队次数的平均,部分图的 $k$ 值很小),

    但是,现在很多的题目,都是会卡掉SPFA的。所以,现在对于没有负权边的图,单源最短路请优先考虑堆优化Dij

    当然啦,SPFA被卡了我还是想用SPFA怎么办?根据知乎上@fstqwq对于“如何看待SPFA算法已死这种说法?”的回答表明,

    在不断的构造图卡SPFA和不断地优化SPFA过数据的斗争中,LLL优化、SLF优化、SLF带容错等一系列优化都被卡掉了,

    所以……

    而到目前(2018.9.4)为止,暂时有位神仙想出了一种SLF+swap优化的SPFA,暂时还很难卡掉,是不是SPFA还能苟住一波呢?心向往之情不自禁地就想了解一下:


    首先是单纯的 SLF优化:Small Label First策略,设要入队的节点是 j,而队首元素为 i,若dist[j] < dist[i] 则将 j 插入队首,否则插入队尾。

    再然后是 SLF+swap优化:每当队列改变时,如果队首节点 i 的 dist[i] 大于队尾节点 j 的 dist[j],则交换首尾节点。

    SLF+swap优化的AC代码:

    #include<bits/stdc++.h>
    using namespace std;

    const int maxn=1e5+10; const int INF=0x3f3f3f3f; int n,m,s; //邻接表存图 struct Edge{ int u,v,w; Edge(int u=0,int v=0,int w=0){this->u=u,this->v=v,this->w=w;} }; vector<Edge> E; vector<int> G[maxn]; void addedge(int u,int v,int w) { E.push_back(Edge(u,v,w)); G[u].push_back(E.size()-1); } //数组模拟队列 const int Qsize=2e5+10; int head,tail; int Q[Qsize]; //SPFA单源最短路 int dist[maxn]; bool vis[maxn]; void spfa() { for(int i=1;i<=n;i++) dist[i]=INF,vis[i]=0; dist[s]=0; head=tail=0; Q[tail++]=s; vis[s]=1; while(head<tail) { int u=Q[head++]; vis[u]=0; if(head<tail-1 && dist[Q[head]]>dist[Q[tail-1]]) swap(Q[head],Q[tail-1]); for(int i=0;i<G[u].size();i++) { Edge &e=E[G[u][i]]; int v=e.v; if(dist[v]>dist[u]+e.w) { dist[v]=dist[u]+e.w; if(!vis[v]) { Q[tail++]=v; vis[v]=1; if(head<tail-1 && dist[Q[head]]>dist[Q[tail-1]]) swap(Q[head],Q[tail-1]); } } } } } int main() { scanf("%d%d%d",&n,&m,&s); for(int i=1;i<=m;i++) { int u,v,w; scanf("%d%d%d",&u,&v,&w); addedge(u,v,w); } spfa(); for(int i=1;i<=n;i++) printf("%d%s",dist[i],((i==n)?" ":" ")); }

    第二天凌晨(2018.9.5 - 0:18)更新,上面的代码WA了,添加的第六组数据,直接把这种优化的SPFA给叉掉了,神奇!

    所以!再强调一遍!

    所以!还是Dijkstra大法好!


     接下来回到Dijkstra时间!之前说了,没有负权边的图,推荐使用堆优化的Dijkstra,时间复杂度有保证!

     

    首先依然是回顾一下Dijkstra算法:

    ①构建两个存储节点的集合:

     集合S:存储的是已经确定正确计算出dist[]的节点,刚开始为空;

     集合Q:$V - S$,刚开始时就等于集合V。

    构建标记数组vis[],标记为1代表该点在集合S中,标记为0就在集合Q中。

    ②初始化,所有点的 dist[i] = INF,源点 s 的 dist[s] = 0;所有点的标记全部置零。

    ②重复 $left| V ight|$ 次如下步骤:

     1.寻找集合Q里dist[]最小的那个节点 u,标记 vis[u] = 1(放入集合S中)

     2.遍历节点 u 出发的所有边,假设当前遍历到某条边为 edge(u,v),若节点 v 在集合Q中(vis[v] = 0),则尝试松弛dist[v] = min( dist[v] , dist[u] + edge(u,v).w )。

    普通的Dijkstra算法时间复杂度 $Oleft( {left| V ight|^2 } ight)$,跟Bellman-Ford算法和普通SPFA一样过不了本题,

    所以就要掏出堆优化的Dijkstra了,因为要寻找集合Q里dist[]最小的那个节点 u,不一定要遍历来寻找,可以通过堆来降低寻找的时间复杂度。


    第一种,实现起来最简单的,用STL库的优先队列实现,

    考虑最坏情况,所有的边都要松弛一遍,则往优先队列里push了 $Oleft( {left| E ight|} ight)$ 个元素,所以每次push和pop都要 $Oleft( {log left| E ight|} ight)$,

    同样,又因为最坏情况每条边都要松弛一次,则要进行 $Oleft( {left| E ight|} ight)$ 次push和pop。故时间复杂度 $Oleft( {left| E ight|log left| E ight|} ight)$,

    AC代码:

    #include<bits/stdc++.h>
    using namespace std;
    typedef pair<int,int> pii; //first是最短距离,second是节点编号
    #define mk(x,y) make_pair(x,y)
    const int maxn=1e5+10;
    const int INF=0x3f3f3f3f;
    
    int n,m,s;
    struct Edge{
        int u,v,w;
        Edge(int u=0,int v=0,int w=0){this->u=u,this->v=v,this->w=w;}
    };
    vector<Edge> E;
    vector<int> G[maxn];
    void addedge(int u,int v,int w)
    {
        E.push_back(Edge(u,v,w));
        G[u].push_back(E.size()-1);
    }
    
    int dist[maxn]; bool vis[maxn];
    priority_queue< pii, vector<pii>, greater<pii> > Q;
    void dijkstra()
    {
        for(int i=1;i<=n;i++) dist[i]=INF, vis[i]=0;
        dist[s]=0, Q.push(mk(0,s));
        while(!Q.empty())
        {
            int u=Q.top().second; Q.pop();
            if(vis[u]) continue;
            vis[u]=1;
            for(auto x:G[u])
            {
                Edge &e=E[x]; int v=e.v;
                if(vis[v]) continue;
                if(dist[v]>dist[u]+e.w) dist[v]=dist[u]+e.w, Q.push(mk(dist[v],v));
            }
        }
    }
    
    int main()
    {
        scanf("%d%d%d",&n,&m,&s);
        for(int i=1;i<=m;i++)
        {
            int u,v,w;
            scanf("%d%d%d",&u,&v,&w);
            addedge(u,v,w);
        }
        dijkstra();
        for(int i=1;i<=n;i++) printf("%d%s",dist[i],((i==n)?"
    ":" "));
    }


    接下来是手写二叉堆优化Dijkstra算法,由于控制堆内元素个数 $Oleft( {left| V ight|} ight)$,所以每次push和pop时间复杂度是 $Oleft( {log left| V ight|} ight)$,

    同时,每个点都出堆(或者说,出集合Q)一次,则进行了 $Oleft( {left| V ight|} ight)$ 次pop操作,

    又考虑最坏情况每条边都进行了松弛,则进行了 $Oleft( {left| E ight|} ight)$ 次入堆push或者堆内某个点上移up操作,

    因此总时间复杂度 $Oleft( {left( {left| V ight| + left| E ight|} ight)log left| V ight|} ight)$。

    AC代码:

    #include<bits/stdc++.h>
    using namespace std;
    
    const int maxn=1e5+10;
    const int INF=0x3f3f3f3f;
    
    int n,m,s;
    int dist[maxn];
    bool vis[maxn];
    
    struct Edge{
        int u,v,w;
        Edge(int u=0,int v=0,int w=0){this->u=u,this->v=v,this->w=w;}
    };
    vector<Edge> E;
    vector<int> G[maxn];
    void addedge(int u,int v,int w)
    {
        E.push_back(Edge(u,v,w));
        G[u].push_back(E.size()-1);
    }
    
    struct Heap
    {
        int sz;
        int heap[4*maxn],pos[maxn];
        void up(int now)
        {
            while(now>1)
            {
                int par=now>>1;
                if(dist[heap[now]]<dist[heap[par]]) //子节点小于父节点,不满足小顶堆性质
                {
                    swap(heap[par],heap[now]);
                    swap(pos[heap[par]],pos[heap[now]]);
                    now=par;
                }
                else break;
            }
        }
        void push(int x) //插入权值为x的节点
        {
            heap[++sz]=x;
            pos[x]=sz;
            up(sz);
        }
        inline int top(){return heap[1];}
        void down(int now)
        {
            while((now<<1)<=sz)
            {
                int nxt=now<<1;
                if(nxt+1<=sz && dist[heap[nxt+1]]<dist[heap[nxt]]) nxt++; //取左右子节点中较小的
                if(dist[heap[now]]>dist[heap[nxt]]) //子节点小于父节点,不满足小顶堆性质
                {
                    swap(heap[now],heap[nxt]);
                    swap(pos[heap[now]],pos[heap[nxt]]);
                    now=nxt;
                }
                else break;
            }
        }
        void pop() //移除堆顶
        {
            heap[1]=heap[sz--];
            pos[heap[1]]=1;
            down(1);
        }
        void del(int p) //删除存储在数组下标为p位置的节点
        {
            heap[p]=heap[sz--];
            pos[heap[p]]=p;
            up(p), down(p);
        }
        inline void clr()
        {
            sz=0;
            memset(pos,0,sizeof(pos));
        }
    }h;
    
    void dijkstra()
    {
        for(int i=1;i<=n;i++) dist[i]=INF, vis[i]=0;
        dist[s]=0;
    
        h.clr();
        h.push(s);
        while(h.sz)
        {
            int u=h.top(); h.pop();
            if(vis[u]) continue;
            vis[u]=1;
            for(int i=0;i<G[u].size();i++)
            {
                Edge &e=E[G[u][i]]; int v=e.v;
                if(!vis[v] && dist[v]>dist[u]+e.w)
                {
                    dist[v]=dist[u]+e.w;
                    if(h.pos[v]) h.up(h.pos[v]);
                    else h.push(v);
                }
            }
        }
    }
    
    int main()
    {
        scanf("%d%d%d",&n,&m,&s);
        for(int i=1;i<=m;i++)
        {
            int u,v,w;
            scanf("%d%d%d",&u,&v,&w);
            addedge(u,v,w);
        }
        dijkstra();
        for(int i=1;i<=n;i++) printf("%d%s",dist[i],((i==n)?"
    ":" "));
    }

    这个代码还没有AC,只拿了80分,但是今晚(2018.9.4 - 23:38)把上面AC过的代码交了一发,以及本题题解里面的代码交了一些,都只有80分,最后一个测试点没能通过,原因是因为Too long on line 1,比较奇怪,猜测可能是数据问题。

    第二天凌晨(2018.9.5 - 0:15)更新,上面的代码AC了,添加的第六组数据有点问题,已经被@fstqwq巨巨修好了。


    当然,最后还有一种比较神奇的优化Dijkstra方式,就是线段树优化(线段树天下第一!),

    其实它优化Dijkstra的原理和优先队列和二叉堆都是差不多的,优化的重点无非是在集合Q找dist[]最小的那个点,

    所以首先不妨把1~n个点全部扔进去建线段树,维护两个值:区间最小值minval 和 最小值在哪个位置minpos,现在这棵线段树就是我们的初始的集合Q了!

    要在集合Q里找dist[]最小的节点 u,简单啊 节点u 不就是 node[root].minpos 嘛!

    很好,那接下来怎么把这个节点从集合Q里踢出去呢,删除节点不现实,把它更新成INF不就好了,这样以后就不会再找到这个点了,

    如果还能再找到这个点……说明整棵线段树里所有元素的值都变成INF了,那不就代表集合Q是空的了嘛,所以循环结束~

    时间复杂度:建树 $Oleft( {left| V ight|} ight)$,线段树单点修改 $Oleft( {log left| V ight|} ight)$,

          每个点出集合Q一次即 $Oleft( {left| V ight|} ight)$ 次线段树单点修改,每条边全部松弛一次即 $Oleft( {left| E ight|} ight)$ 次线段树单点修改,

          因此总的时间复杂度 $Oleft( {left( {left| V ight| + left| E ight|} ight)log left| V ight|} ight)$。

    #include<bits/stdc++.h>
    using namespace std;
    
    const int maxn=1e5+10;
    const int INF=0x3f3f3f3f;
    
    int n,m,s;
    int dist[maxn];
    bool vis[maxn];
    
    struct Edge{
        int u,v,w;
        Edge(int u=0,int v=0,int w=0){this->u=u,this->v=v,this->w=w;}
    };
    vector<Edge> E;
    vector<int> G[maxn];
    void addedge(int u,int v,int w)
    {
        E.push_back(Edge(u,v,w));
        G[u].push_back(E.size()-1);
    }
    
    /********************************* Segment Tree - st *********************************/
    struct Node{
        int l,r;
        int minval,minpos;
    }node[4*maxn];
    int nodeidx[maxn];
    void pushup(int root)
    {
        if(node[root<<1].minval<=node[root<<1|1].minval)
        {
            node[root].minval=node[root<<1].minval;
            node[root].minpos=node[root<<1].minpos;
        }
        else
        {
            node[root].minval=node[root<<1|1].minval;
            node[root].minpos=node[root<<1|1].minpos;
        }
    }
    void build(int root,int l,int r)
    {
        if(l>r) return;
        node[root].l=l; node[root].r=r;
        if(l==r)
        {
            node[root].minval=((l==s)?0:INF);
            node[root].minpos=l;
            nodeidx[l]=root;
        }
        else
        {
            int mid=l+(r-l)/2;
            build(root*2,l,mid);
            build(root*2+1,mid+1,r);
            pushup(root);
        }
    }
    void update(int root,int pos,int val)
    {
        if(node[root].l==node[root].r)
        {
            node[root].minval=val;
            return;
        }
    
        int mid=node[root].l+(node[root].r-node[root].l)/2;
        if(pos<=mid) update(root*2,pos,val);
        if(pos>mid) update(root*2+1,pos,val);
        pushup(root);
    }
    /********************************* Segment Tree - ed *********************************/
    
    void dijkstra()
    {
        for(int i=1;i<=n;i++) dist[i]=((i==s)?0:INF),vis[i]=0;
    
        build(1,1,n);
        while(node[1].minval<INF)
        {
            int u=node[1].minpos;
            if(vis[u]) continue;
            vis[u]=1; update(1,u,INF);
            for(int i=0;i<G[u].size();i++)
            {
                Edge &e=E[G[u][i]]; int v=e.v;
                if(vis[v]) continue;
                if(dist[v]>dist[u]+e.w)
                {
                    dist[v]=dist[u]+e.w;
                    update(1,v,dist[u]+e.w);
                }
            }
        }
    }
    
    int main()
    {
        scanf("%d%d%d",&n,&m,&s);
        for(int i=1;i<=m;i++)
        {
            int u,v,w;
            scanf("%d%d%d",&u,&v,&w);
            addedge(u,v,w);
        }
        dijkstra();
        for(int i=1;i<=n;i++) printf("%d%s",dist[i],((i==n)?"
    ":" "));
    }
  • 相关阅读:
    PowerDesigner生成SQL的冒号设置
    Linux/Windows 一键获取当前目录及子目录下所有文件名脚本
    Target runtime jdk1.8.0_181 is not defined
    windows——任务计划程序
    12篇文章回顾总结
    《逆商》2月12日
    《终身成长》2月11日
    《心流》 什么才是真正的幸福
    《心流》 什么才是真正的幸福 2月7日
    《高效能人士的7个习惯》 2月3日
  • 原文地址:https://www.cnblogs.com/dilthey/p/9583728.html
Copyright © 2011-2022 走看看