图的最小生成树(Kruskal算法,Prim算法)

      无向图中的最短路径问题?No,最短路径不是最小生成树!  


什么是最小生成树?

        在一个无向连通图中,有一个子图连接所有顶点,并且权重和最小,那么他就是最小生成树。如果权重和不是最小的只能叫做生成树。有n个顶点的连通图的生成树有n个顶点n-1条边

        如果各位学过最短路径(最短路径(Floyd-Warshall、Dijkstra、Bellman-Ford)-CSDN博客),那么对学两个算法就会很有一点帮助,但正如前边所说,最短路径并非最小生成树,二者是由区别的。这两个算法采用的是逐步求解的贪心策略。

        我们先来看一道例题:

镖局运镖(《啊哈!算法》)

        最近小哼迷上了《龙门镖局》,从恰可图到武夷山,从张家口到老河口,从迪化到佛山,从蒙自到奉天,迤逦数千里的商道上,或牛马,或舟楫,或驼驮(tuó),或肩挑,货物往来,钱财递送,皆离不开镖局押运。商号开在哪里,镖局便设在哪里。古代镖局的运镖,就是运货,也就是现代物流,镖局每到一个新地方开展业务,都需要对运镖途中的绿林好汉进行打点。好说话的打点费就比较低,不好说话的打点费就比较高。现在已知城镇地图如下,顶点是城镇编号,边上的值表示这条道路上打点绿林好汉需要的银子数

        镖局现在需要选择一些道路进行疏通,一遍镖局可以达到任意一个城镇,要求是花费的银子越少越好。

        输入格式:

        输入m+1行,第一行两个数n和m,n表示n个城市,m表示m条道路。接下来的m行,每行形如“a b c”,用来表示一条道路,意思是城市a到城市b需要花费的银子数是c。

         输出格式:

        输出一行,一个数表示花费的最少银子数。

        输入样例:

6 9

2 4 11

3 5 13

4 6 3

5 6 4

2 3 6

4 5 7

1 2 1

3 4 9

1 3 2

        输出样例:

19 

        这道题其实就是求这个无向连通图的最小生成树,首先要保证的是最短,这道题用贪心策略可以求得最优解,我们可以证明贪心是可行的,那么我们就是求任意两个顶点之间的最小权重路径,树不需要有回路,但并没有限制每个顶点连几条边。

Kruskal算法

       任意两个顶点之间的最小权重路径,Kruskal的做法是将路径从小到大排序,选择最小并不会产生回路的。因为我们要得到的是最小生成树,保证的是权重和最小,而不是单源最短路径最小。

        举个例子:我们求得的最小生成树为红色线所示,但1->3的最短路径不包含在其中

        我们将所有边排序:

1 2 1
1 3 2
4 6 3
5 6 4
2 3 6
4 5 7
3 4 9
2 4 11
3 5 13

        然后就是从小到大选择:

 

        当选择到“2 3 6”,这条边时,发现会形成回路,故跳过这一个:

 

        最终选用“3 4 9”这条边,形成最小生成树:

        那么我们如何判断是否会形成回路呢?可以通过book标记已经走过的数组嘛?答案是肯定不行的。这是无向图,我们没有向Prim一样确定从哪个顶点到哪个顶点,如果用book标记一条边的两个顶点的话,便不可能形成连通的子图了。

        所以我们使用并查集初识树(二叉树,堆,并查集)-CSDN博客)的方法。“u v w”中,如果u和v属于一个集合,那么连通这条路就会形成回路。

        所以Kruskal主要用到两个算法:一个是排序,一个是并查集,我们这里采用快排。当已经连通n-1条边的时候,就代表最小生成树已经形成。

        完整代码:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define inf 0x3f3f3f3f

struct edge
{
    int u;
    int v;
    int w;
};/*方便排序,用结构体存*/

int find(int v,int *f)/*查找*/
{
    if (f[v]==v)
    {
        return v;
    }
    else
    {
        f[v]=find(f[v],f);/*路径压缩*/
        return f[v];
    }
    
}

int Union(int x,int y,int *f)/*合并*/
{
    int t1=find(x,f);
    int t2=find(y,f);
    if (t1!=t2)
    {
        f[t2]=t1;
        return 1;
    }
    return 0;
}

void quicksort(int left,int right,struct edge *e)/*快排*/
{
    int i,j;
    struct edge t;
    if (left>right)
    {
        return ;
    }
    
    i=left;
    j=right;
    while (i!=j)
    {
        while (e[j].w>=e[left].w && i<j)
            j--;
        while (e[i].w<=e[left].w && i<j)
            i++;
        
        if (i<j)
        {
            t=e[i];
            e[i]=e[j];
            e[j]=t;
        }
    } 
    t=e[left];
    e[left]=e[i];
    e[i]=t;

    quicksort(left,i-1,e);
    quicksort(i+1,right,e);
    return ;
}

int main()
{
    struct edge e[20];
    int n,m;
    scanf("%d%d",&n,&m);
    for (int i = 1; i <= m; i++)
        scanf("%d%d%d",&e[i].u,&e[i].v,&e[i].w);
    // printf("test\n");
    quicksort(1,m,e);
    // for (int i = 1; i <= m; i++)/*打印排序过的路径*/
    //     printf("%d %d %d\n",e[i].u,e[i].v,e[i].w);
    
    int f[20];
    for (int i = 0; i <= n; i++)/*并查集初始化*/
        f[i]=i;

    int cont=0,sum=0;
    for (int i = 1; i <= m; i++)
    {
        if (Union(e[i].u,e[i].v,f))/*判断是否会形成回路*/
        {
            cont++;
            sum+=e[i].w;
        }
        if (cont == n-1)/*已经连通n-1条边*/
        {
            break;
        }
    }
    printf("sum=%d",sum);
    
    return 0;
}

Prim算法

        它与Dijkstra简直不能太相似了,区别就是在松弛的时候,Dijkstra是判断单源顶点到其它点的最短距离,而Prim是最小生成树(把最小生成树看成一个结点)到其它点的最短距离。

        还是用上边那个图作为例子:

        在连接3顶点的时候,是选择紫线还是绿线呢?如果求最短路径,到1顶点的距离是6和7,那当然是选紫线,但是现在是最小生成树,二者到3结点的距离是6和7,那么就是选绿线了。

        本代码结合了,邻接表,最小堆,代码比较长,但时间复杂度大大降低到了O(M\log N),如果用邻接矩阵和遍历的方法,时间复杂度为:O(N^2)。故我们这里只写时间复杂度小的代码:

        这里不再写程序设计部分,因为和Dijkstra差不多,会稍微讲一下堆优化。

        完整代码:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define inf 0x3f3f3f3f
int h[10]={0};/*h用来保存堆*/
int pos[10]={0};/*pos用来存储每个顶点在堆中的位置*/

void swap(int x,int y)/*交换函数,用来交换堆中的两个元素的值*/
{
    int t;
    t=h[x];
    h[x]=h[y];
    h[y]=t;

    /*同步更新pos*/
    t=pos[h[x]];
    pos[h[x]]=pos[h[y]];
    pos[h[y]]=t;
    return ;
}

void siftdown(int *num,int i,int n)/*向下调整函数,形成最小堆*/
{
    int t=0,flag=0;
    while (flag==0&&2*i<=n)
    {
        if (num[h[i]]>num[h[i*2]])/*左儿子小于于父亲*/
        {
            t=2*i;
        }
        else    
            t=i;
        if (2*i+1<n&&num[h[t]]>num[h[2*i+1]])/*右儿子小于t结点*/
        {
            t=2*i+1;
        }
        if (t!=i)
        {
            swap(t,i);
            i=t;
        }     
        else 
            flag=1;  
    }
    return ;
}

void siftup(int *num,int i)/*向上调整函数,形成最大堆*/
{
    int t=0,flag=0;
    if (i==1)
    {
        return ;
    }
    
    while (flag==0&&i!=1)
    {
        if (num[h[i]]<num[h[i/2]])/*父结点大于子结点,交换*/
        {
            t=i/2;
            swap(i,t);
        }
        else
            flag=1;
        i=i/2;
    }
}

int pop(int n,int *num)/*将堆顶出堆*/
{
    int t;
    t=h[1];/*临时变量存储堆顶对应的图中的结点*/
    pos[t]=0;/*无所谓*/
    h[1]=h[n];/*将堆中的最后一个结点赋给堆顶*/
    pos[h[1]]=1;/*更新pos*/
    n--;/*堆元素减1,表示又排序了一个*/
    siftdown(num,1,n);/*将堆顶调整,形成新的最小堆*/
    return t;
}

int main()
{
    int n,m;
    scanf("%d%d",&n,&m);
    int n_flag=n;
    /*u,v,w,next的数组大小要根据实际情况来确定,此图为无向图,要比2*m大1*/
    /*first要比n大1*/
    int u[20]={0},v[20]={0},w[20]={0},first[20],next[20]={0};
    memset(first,-1,sizeof(int)*(n+1));/*初始化first*/
    int dis[20]={0};
    memset(dis,inf,sizeof(int)*(n+1));/*初始化 dis*/
    int k=0;
    for (int i = 1; i <= m ; i++)
    {
        scanf("%d%d%d",&u[i],&v[i],&w[i]);
    }
    for (int i = m+1; i <= 2*m; i++)
    {
        u[i]=v[i-m];
        v[i]=u[i-m];
        w[i]=w[i-m];
    }
    for (int i = 1; i <= 2*m; i++)/*创建邻接表,无向图*/
    {
        next[i]=first[u[i]];
        first[u[i]]=i; 
    }

    for (int i = 0; i <= n; i++)
    {
        h[i]=i;
        pos[i]=i;
    }
    k=first[1];
    dis[1]=0;
    while(k!=-1)/*初始化dis数组,到生成树的最短距离*/
    {
        dis[v[k]]=w[k];
        k=next[k];
    }
    for (int i = n/2; i >= 1; i--)/*创建最小堆*/
    {
        siftdown(dis,i,n);
    }
    pop(n,dis);/*先弹出dis[1]=0这一个*/
    n--;/*堆顶元素减1,函数里面是形参,这里可以用全局变量改进*/
    /*核心代码*/
    int book[10]={0};/*保证不能有回路*/
    int cont=0;/*统计生成树有几个结点*/
    book[1]=1;
    cont++;
    int sum=0;
    while (cont<n_flag)
    {
        int j=pop(n,dis);
        // printf("j=%d h[1]=%d dis[h[i]]=%d\n",j,h[1],dis[h[1]]);/* 堆排序查看 */
        n--;
        cont++;
        book[j]=1;
        sum+=dis[j];
        k=first[j];
        while (k!=-1)
        {
            if (dis[v[k]]>w[k]&&book[v[k]]==0)
            {
                // printf("    %d %d %d pos[v[k]]=%d\n",u[k],v[k],w[k],pos[v[k]]);/*邻接表查看*/
                dis[v[k]]=w[k];
                siftup(dis,pos[v[k]]);
            }
            k=next[k];
        }
    }
    printf("sum=%d\n",sum);
    /******* */
    return 0;
}

        堆的优化讲解:

        这里我们需要三个数组来实现堆优化:dis、h、pos。

        我们来看一个刚开始生成的最小堆,即最小生成树只有1结点的时候,如图:

        堆里面并没有直接结点存储权重,因为树中结点的位置,与图中顶点不同,树中的1结点并不一定是无向连通图中的1结点。 故我们这里用到了p和pos两个数组来表示他们的对应关系:

        在p数组中,下标对应堆中的结点,值对应图中的顶点。

        在pos数组中,下标对应图中的顶点,值对应堆中的结点。

        dis数组中,下标表示图中的顶点,值对应,顶点到最小生成树的最短路径。

总结

        如果所有的边权不相等,那么最小生成树就是唯一的,Kruskal算法是一步步将树林中的树合并,而prim算法则是通过每次增加一条边来建立树。Kruskal算法适用于稀疏图,没有使用堆优化的prim适用于稠密图,使用了堆优化的Prim适用于稀疏图。

参考文献

《啊哈!算法》

图详解第三篇:最小生成树(Kruskal算法+Prim算法)-腾讯云开发者社区-腾讯云

最小生成树详细讲解(Prime算法+Kruskalsuanfa)_牛客博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值