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

news2024/12/24 8:22:26

      无向图中的最短路径问题?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)_牛客博客

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2255599.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Flink-scala】DataStream编程模型之水位线

DataStream API编程模型 1.【Flink-Scala】DataStream编程模型之 数据源、数据转换、数据输出 2.【Flink-scala】DataStream编程模型之 窗口的划分-时间概念-窗口计算程序 3.【Flink-scala】DataStream编程模型之 窗口计算-触发器-驱逐器 文章目录 DataStream API编程模型前言…

PHP RabbitMQ连接超时问题

问题背景 Error: The connection timed out after 3 sec while awaiting incoming data 看到这个报错&#xff0c;我不以为意&#xff0c;认为是我设置的超时时间不够导致的&#xff0c;那就设置长一点 Error: The connection timed out after 300 sec while awaiting incom…

【LeetCode热题100】BFS解决FloodFill算法

这篇博客主要记录了使用BFS解决FloodFill算法的几道题目&#xff0c;包括图像渲染、岛屿数量、岛屿的最大面积、被包围的区域。 class Solution {using PII pair<int, int>; public:vector<vector<int>> floodFill(vector<vector<int>>& im…

L2G3000-LMDeploy 量化部署实践

文章目录 LMDeploy 量化部署实践闯关任务环境配置W4A16 量化 KV cacheKV cache 量化Function call LMDeploy 量化部署实践闯关任务 环境配置 conda create -n lmdeploy python3.10 -y conda activate lmdeploy conda install pytorch2.1.2 torchvision0.16.2 torchaudio2.1.…

大数据新视界 -- 大数据大厂之 Hive 临时表与视图:灵活数据处理的技巧(上)(29 / 30)

&#x1f496;&#x1f496;&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎你们来到 青云交的博客&#xff01;能与你们在此邂逅&#xff0c;我满心欢喜&#xff0c;深感无比荣幸。在这个瞬息万变的时代&#xff0c;我们每个人都在苦苦追寻一处能让心灵安然栖息的港湾。而 我的…

Ubuntu操作系统在Vmware中的安装、常用操作、最基础的知识、imx6ll基本开发环境配置

01-Ubuntu操作系统的安装 网盘搜索 “ubuntu18.04.zip”&#xff0c;下载下来之后用Vmware打开就行了。 我用的虚拟机是15.5.6&#xff0c;实测没有问题。 启动时用户名为book的密码为123456 提问&#xff1a;Ubuntu与Centos系统有何区别&#xff1f; 详情见 https://blog.cs…

windows2012服务器安装sqlserver2012出现NetFx3错误的解决方法。

出现以下错误: 启用 Windows 功能 NetFx3 时出错&#xff0c;错误代码: -2146498298。请尝试从 Windows 管理工具启用 Windows 功能 NetFx3&#xff0c;然后重新运行安装程序。有关如何启用 Windows 功能的详细信息&#xff0c;具体解决办法如下&#xff1a; 1、打开PowerShel…

FPGA实战篇(按键控制LDE实验)

1.按键简介 按键开关是一种电子开关&#xff0c;属于电子元器件类。我们的开发板上有两种按键开关&#xff1a;第一种是本实验所使用的轻触式按键开关&#xff0c;简称轻触开关。使用时以向开关的操作方向施加压力使内部电路闭合接通&#xff0c;当撤销压力时开关断开&#xff…

C++析构函数和构造函数

一、构造函数 1.构造函数的基本概念 1.对构造函数的理解&#xff1a; 构造函数是类的一种特殊成员函数&#xff0c;其主要功能是在创建对象时进行初始化操作。它的名字与类名相同&#xff0c;并且没有返回值类型&#xff08;不能是void&#xff09;。例如&#xff0c;对于一个…

【Axure高保真原型】数值条件分组

今天和大家分享数值条件分组的原型模板&#xff0c;效果包括&#xff1a; 点击添加分组按钮&#xff0c;可以显示添加弹窗&#xff0c;填写分组名称和数值区间后&#xff0c;可以新增该分组信息‘’ 修改分组区间&#xff0c;可以直接在输入框里修改已有的分组区间&#xff0c…

阳光电脑公司的维修服务微信小程序ssm+论文源码调试讲解

第2章 开发环境与技术 阳光电脑公司的维修服务微信小程序的编码实现需要搭建一定的环境和使用相应的技术&#xff0c;接下来的内容就是对阳光电脑公司的维修服务微信小程序用到的技术和工具进行介绍。 2.1 MYSQL数据库 本课题所开发的应用程序在数据操作方面是不可预知的&…

重生之我在异世界学编程之C语言:深入结构体篇(下)

大家好&#xff0c;这里是小编的博客频道 小编的博客&#xff1a;就爱学编程 很高兴在CSDN这个大家庭与大家相识&#xff0c;希望能在这里与大家共同进步&#xff0c;共同收获更好的自己&#xff01;&#xff01;&#xff01; 本文目录 引言结构体的自引用实现链表一、链表的基…

使用Redis Stream偶发空指针问题

问题描述&#xff1a;使用redission客户端封装的stream消息队列&#xff0c;在进行消息轮询时&#xff0c;偶发出现空指针问题。 [2024-11-13 09:59:20] [] [] [redis-stream-consumer-thread-1 ] [lambda$streamMessageListenerContainer$1] [ERROR] [c.r.c.r.s.config.Redi…

35页PDF | 元数据与数据血缘落地实施(限免下载)

一、前言 这份报告详细介绍了元数据与数据血缘的概念、重要性以及在企业数据中台中的应用。报告阐述了数据中台的核心价值在于整合和管理体系内的数据&#xff0c;以提升数据资产化能力并支持业务决策。报告还涵盖了元数据的分类&#xff08;技术元数据和业务元数据&#xff0…

方案精读:50页智慧园区数字化平台总体规划与建设方案PPT

本文介绍了智慧园区数字化平台总体规划与建设方案&#xff0c;包括智慧园区工业云平台、智慧办公平台、智能工厂、智慧能源管理、智慧政务管理等方面的建设内容。方案旨在通过技术手段加强园区内部沟通和管理能力&#xff0c;实现个性化营销、柔性化制造、高效智能协同的供应链…

深入浅出:Go语言标准库探索

深入浅出&#xff1a;Go语言标准库探索 引言 Go语言自发布以来&#xff0c;以其简洁的语法、高效的性能和强大的并发支持赢得了开发者的青睐。除了这些特性外&#xff0c;Go还拥有一个功能丰富且设计精良的标准库&#xff0c;几乎涵盖了现代应用程序开发所需的所有基本功能。…

python selenium(4+)+chromedriver最新版 定位爬取嵌套shadow-root(open)中内容

废话不多说&#xff0c;直接开始 本文以无界作为本文测试案例&#xff0c;抓取shadow-root&#xff08;open&#xff09;下的内容 shadow Dom in selenium&#xff1a; 首先先讲一下shadow Dom in selenium 版本的区别&#xff0c;链接指向这里 在Selenium 4版本 以及 chrom…

1207论文速读

1、SCN_GNN: A GNN-based fraud detection algorithm combining strong node and graph topology information 全文总结&#xff1a;本文介绍了一种基于图神经网络的欺诈检测算法——SCN_GNN。在欺诈检测中&#xff0c;由于欺诈者经常使用多种关系类型来掩盖其活动&#xff0c…

5.3_小程序渗透

小程序 行为工具小程序抓包全局代理&#xff0c;Proxifer小程序渗透小程序反编译Wxapkg 参数&#xff1a;scan 联网自动扫描&#xff0c;-r 指定 wx文件路径小程序逆向WeChatOpenDevTools 小程序抓包 工具步骤 全局代理 Burp中Proxy setings导出证书选择 Select file &a…

怎么实现邮件营销自动化?

邮件营销能够出色地帮助我们与客户建立良好关系。无论是新客户还是老客户&#xff0c;都可以通过邮件来达成较为良好的客户关系。然而&#xff0c;从消费者的角度来看&#xff0c;每个人都有自己独特的习惯和特点&#xff0c;没有人希望收到千篇一律、营销意味过重的邮件。因此…