最小生成树和最短路算法

  是很久以前就学过的东西。图论最基础的算法。通常在各种题目中担任题解的基础部分(这道题先跑个生成树再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附带了最短路,稍后我会单独贴题解。动态最小生成树我还不会搞……

以上

 posted on 2019-10-24 15:19  SakuLeaF  阅读(405)  评论(0编辑  收藏  举报