zoukankan      html  css  js  c++  java
  • 【算法总结】图论算法(最小生成树和最短路)

    最小生成树和最短路算法

      是很久以前就学过的东西。图论最基础的算法。通常在各种题目中担任题解的基础部分(这道题先跑个生成树再balabala)

      这次也是复习了。

    最小生成树

      最小生成树是用来解决用最小的代价用N-1条边连接N个点的问题。常用的算法是Prim和Kruskal,两者时间复杂度并没有差很多(Prim堆优化的前提下),但是因为写Prim就要手撕堆所以我比较偏向Kruskal。

      没错我不会(can't)用sort以外的STL

    Prim

      Prim 算法使用和 Dijkstra 相似的蓝白点思想,用 dis 数组来表示 i 点与白点相连的最小权值,每一轮取出 dis 最小的蓝点,将其变为白点并修改与其相连的蓝点的 dis 值。

       n 次循环,每次循环 Prim 算法都能让一个新的点加入生成树,n 次循环就能把所有点囊括到其中;每次循环 Prim 算法都能让一条新的边加入生成树,n-1 次循环就能生成一棵含有 n 个点的树;每次循环 Prim 算法都取一条最小的边加入生成树,n-1 次循环结束后,我们得到的就是一棵最小的生成树。这就是 Prim 采取贪心法生成一棵最小生成树的原理。

      朴素Prim的时间复杂度是O(n²),显然的可以使用堆优化来获得更好的时间复杂度。

    Kruskal

      Kruskal 算法先将所有点认为是孤立的,然后将边按权值排序,每次选择一条边,如果这条边连接着两个不同的联通块,就合并这两个联通块,如果不是,就不选择这条边,直到选择了 n-1 条边为止。

      Kruskal 算法每次都选择一条最小的,且能合并两个不同集合的边,一张 n 个点的图总共选取 n-1 次边。因为每次选的都是最小的边,所以最后的生成树一定是最小生成树。每次选的边都能够合并两个集合,最后 n 个点一定会合并成一个集合。通过这样的贪心策略, Kruskal 算法就能得到一棵有 n-1 条边,连接着 n 个点的最小生成树。

      使用并查集来支持查询和合并的话,Kruskal的时间复杂度是O(Elog2E)的,E为边数。

    附上模板题Kruskal代码(Prim?没写,不想手撕堆。)

    #include <algorithm>
    #include <iostream>
    #include <cstring>
    #include <cstdlib>
    #include <cstdio>
    #include <cmath>
    using namespace std;
    struct node
    {
        int u;
        int v;
        int c;
    };
    node edge[90005];//没有M范围 请出题人自裁 
    int n,m,ans,head[305],pa[305];
    int Find(int x);
    bool uni(int x,int y);
    void kruskal();
    bool cmp(node a,node b);
    int main(void)
    {
        scanf("%d%d",&n,&m);
        int u,v,c;
        for(int i=1;i<=m;i++)
        {
            scanf("%d%d%d",&u,&v,&c);
            edge[i].u=u;
            edge[i].v=v;
            edge[i].c=c;
        }
        sort(edge+1,edge+m+1,cmp);
        for(int i=1;i<=n;i++)pa[i]=i;
        kruskal();
        printf("%d",ans);
        return 0;
    }
    
    bool cmp(node a,node b)
    {
        if(a.c<b.c)return 1;
        return 0;
    }
    
    int Find(int x)
    {
        if(pa[x]==x)return x;
        pa[x]=Find(pa[x]);
        return pa[x];
    }
    
    bool uni(int x,int y)
    {
        int px,py;
        px=Find(x),py=Find(y);
        if(px==py)return 0;
        pa[py]=px;
        return 1;
    }
    
    void kruskal()
    {
        int cnt=0,a=0,b=0;
        for(int i=1;i<=m;i++)
        {
            a=edge[i].u;
            b=edge[i].v;
            if(!uni(a,b))continue;
            cnt++;
            ans+=edge[i].c;
        }
        return;
    }
    View Code

    最短路

      最开始学图论的时候学习的算法,也是就算到现在为止也能够随时随地手撕的玩意。

      以及,就算有这样那样各种的理由,我还是要喊出: SPFA天下第一!!!!!!!!

    Floyd

      全源最短路算法,使用动态规划的思想,代码是极其优美的三层嵌套循环。时间复杂度也是极其优美的O(n³)

      唯一需要注意的一点是,枚举中间节点的k一定要放在最外层。

      伪代码如下

    for(int k=1;k<=n;k++)
        for(int i=1;i<=n;i++)
            for(int j=1;j<=n;j++)
                dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);
    View Code

      除了求全源最短路以外基本不用,偶尔用来找负环?

    Dijsktra

      单源正权最短路算法,意思是图上边权有负权的话dijsktra会WA。

      Dijkstra 的基本思想是蓝白点思想。蓝点是最短路径未确定的点,白点是最短路径确定了的点。最开始只有起点是白点,然后在蓝点中寻找一个离起点的距离最小的点,标记它为白点,它的 dis 值为它离更新它的点的距离加上更新它的白点的 dis 值,再用这个点去更新其余的点。

      与Prim算法相同的,可以使用堆优化。
      因为懒得手撕堆所以懒得写的算法+1

      昨天越写越觉得自己在写SPFA

      贴上模板题代码

      

    #include <algorithm>
    #include <iostream>
    #include <cstdlib>
    #include <cstring>
    #include <cstdio>
    using namespace std;
    struct node
    {
        int nw;
        int nxt;
        int value;
    };
    node edge[20005];
    int n,m,st,ed,cnt,dis[1005],head[1005];
    int Heap[4005],cnt_H;
    bool closed[1005]={0},IN[1005]={0};
    void build(int x,int y,int v);
    void add(int x);
    int get();
    void dijsktra();
    int main(void)
    {
        scanf("%d%d%d%d",&n,&m,&st,&ed);
        memset(dis,127/2,sizeof(dis));
        memset(head,-1,sizeof(head));
        int a,b,c;
        for(int i=1;i<=m;i++)
        {
            scanf("%d%d%d",&a,&b,&c);
            build(a,b,c);
            build(b,a,c);
        }
        a=dis[ed];
        dijsktra();
        if(a==dis[ed])printf("-1");
        else printf("%d",dis[ed]);
        return 0;
    }
    
    void build(int x,int y,int v)
    {
        edge[++cnt].nw=y;
        edge[cnt].nxt=head[x];
        edge[cnt].value=v;
        head[x]=cnt;
        return;
    }
    
    void add(int x)
    {
        Heap[++cnt_H]=x;
        int now=cnt_H,next;
        while(now/2)
        {
            next=now/2;
            if(dis[Heap[next]]<=dis[Heap[now]])break;
            swap(Heap[now],Heap[next]);
            now=next;
        }
        return;
    }
    
    int get()
    {
        int ans=Heap[1];
        Heap[1]=Heap[cnt_H];
        cnt_H--;
        int now=1,next;
        while(now*2<cnt_H)
        {
            next=now*2;
            if(dis[Heap[next+1]]<dis[Heap[next]])next++;
            if(dis[Heap[next]]>=dis[Heap[now]])break;
            swap(Heap[next],Heap[now]);
            now=next;
        }
        return ans;
    }
    
    void dijsktra()
    {
        dis[st]=0;
        add(st);
        int white=0,blue=0;
        while(cnt_H)
        {
            white=get();
            if(closed[white])continue;
            closed[white]=true;
            for(int j=head[white];j>0;j=edge[j].nxt)
            {    
                blue=edge[j].nw;
                if(dis[blue]>dis[white]+edge[j].value)
                {
                    dis[blue]=dis[white]+edge[j].value;
                    add(blue);
                }
            }
            closed[white]=false;
        }
        return;
    }
    View Code

    SPFA

      单源最短路算法,有负权也不会WA,我最常用的算法。

      我知道SPFA严格来说不被承认它就是个队列优化Bellman-Ford但是我就是要喊它SPFA

      SPFA先将起点放入队列, 每次使用队列首的点去试图更新所有与它相连的点的(距离起点的)最短距离,假如更新成功就把被更新的点放入队列去准备更新其它点。

      除此之外,也可以使用SPFA查出负环。

      因为各种原因,正权图最好还是使用DIjsktra算法说的就是那些闲着无聊卡SPFA的出题人

      附上模板题代码。

    #include <algorithm>
    #include <iostream>
    #include <cstdlib>
    #include <cstring>
    #include <cstdio>
    using namespace std;
    struct node
    {
        int nw;
        int nxt;
        int value;
    };
    node edge[20005];
    int n,m,st,ed,cnt;
    int dis[1005]={0},head[1005]={0};
    bool closed[1005]={0};
    void build(int x,int y,int v);
    void SPFA();
    int main(void)
    {
        scanf("%d%d%d%d",&n,&m,&st,&ed);
        memset(dis,0x7f/2,sizeof(dis));
        memset(head,-1,sizeof(head));
        int a,b,c;
        for(int i=1;i<=m;i++)
        {
            scanf("%d%d%d",&a,&b,&c);
            build(a,b,c);
            build(b,a,c);
        }
        a=dis[ed];
        dis[st]=0;
        SPFA();
        if(dis[ed]==a)printf("-1");
        else printf("%d",dis[ed]);
        return 0;
    }
    
    void build(int x,int y,int v)
    {
        edge[++cnt].nw=y;
        edge[cnt].nxt=head[x];
        edge[cnt].value=v;
        head[x]=cnt;
        return;
    }
    
    void SPFA()
    {
        int dl[2005]={0},Head=0,Tail=1,Now;
        dl[1]=st;
        closed[st]=true;
        do
        {
            Head++;
            if(Head>2000)Head=1;
            for(int i=head[dl[Head]];i>0;i=edge[i].nxt)
            {
                Now=edge[i].nw;
                if(dis[Now]>dis[dl[Head]]+edge[i].value)
                {
                    dis[Now]=dis[dl[Head]]+edge[i].value;
                    if(!closed[Now])
                    {
                        Tail++;
                        if(Tail>2000)Tail=1;
                        dl[Tail]=Now;
                        closed[Now]=true;
                    }
                }
            }
            closed[dl[Head]]=false;
        }while(Head!=Tail);
    }
    View Code

    本周习题……换教室严格来说是个DP附带了最短路,稍后我会单独贴题解。动态最小生成树我还不会搞……

    以上

  • 相关阅读:
    Laravel学习之旅(一)
    telnet模拟邮件发送
    学习CodeIgniter框架之旅(二)继承自定义类
    学习CodeIgniter框架之旅(一)自定义模板目录
    MySQL主从复制实现
    coreseek增量索引
    锁(MySQL篇)—之MyISAM表锁

    php文件锁
    进程与线程
  • 原文地址:https://www.cnblogs.com/CYWer/p/11732399.html
Copyright © 2011-2022 走看看