算法基础系列第三章——图论之最小生成树问题

咱们先看看蓝桥杯对最小生成树的考察范围如下

考察范围

圈定的范围是考Prim算法和Kruskal算法。那接下来咱们就开始逐步了解最小生成树以及可爱的Prim算法和Kruskal算法吧

💓最小生成树算法大纲

最小生成树算法大纲

🌟最小生成树的基本概念

🌻自由树和生成树

自由树(树):
1、自由树就是一个无回路的连通图(没有确定根)
2、n个顶点就一定有n-1条边

生成树:
1、包含全部顶点
2、n-1条边全部在图中

图的生成树不惟一。从不同的顶点出发进行遍历,可以得到不同的生成树。
生成树

🌻最小生成树
如果图G是一个连通图,G上的一棵各边权值之和最小的带权生成树,称为G的最小生成树。

💓普利姆算法(prim)——模板题

🌟通俗演示

Prim算法演示

🌟例题描述

Prim算法例题
🎇🎇🎇传送门🎇🎇🎇

因为这是用来做分析的模板题,题目要求里就很直接明了的指出,要咱们求最小生成树的树边权重之和。现在就可以直接去观察数据范围,可以看出是稠密图,那么我们可爱的Prim算法就可以掏出来啦~

🌟参考代码(C++版本)

#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>

using namespace std;

const int N = 510 , INF = 0x3f3f3f3f;
int n,m;
int g[N][N];
int dist[N];
bool st[N];

int prim()
{
    //初始化距离数组
    memset(dist,0x3f,sizeof dist);
    //res中存放最小生成树的树边权重之和
    int res = 0;        
     
    for(int i = 0; i < n;i++)
    {
        int t = -1;
        for(int j = 1; j <= n;j++)
            if(!st[j] && (t == -1 || dist[t] > dist[j]))
            t = j;
            
            //如果不是第一个点以及距离最小的点距离是正无穷,说明当前距离最近的点,到集合的距离都是正无穷,即当前图不连通
            if(i && dist[t] == INF) return INF;
            if(i) res += dist[t];
            
            //用t去更新其他点
            for(int j = 1; j <= n;j++)
                dist[j] = min(dist[j],g[t][j]);//注意这里是g[t][j],Dijkstra中是dist[t]+g[t][j]
                
                st[t] = true;
        
    }
    
    return res;
}


int main()
{
    //输入
    scanf("%d%d",&n,&m);
    //初始化邻接矩阵
    memset(g,0x3f,sizeof g);
    //建图
    while(m--)
    {
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        //因为是无向图,所以得建a 到 b 和 b 到 a的
        g[a][b] = g[b][a] = min(g[a][b],c);
    }
    
    int t = prim();
    
    if(t == INF ) puts("impossible");
    else printf("%d\n",t);
    
    return 0;
}

🌟算法模板

Prim算法实现的流程图如下:
prim算法实现流程

Prim算法实现的代码描述:

	int n;		 		// n表示点数
	int g[N][N];		// 邻接矩阵,存储所有边
	int dist[N];		// 存储其他点到当前最小生成树的距离
	bool st[N];			// 存储每个点是否已经在生成树中


	// 如果图不连通,则返回INF(值是0x3f3f3f3f), 否则返回最小生成树的树边权重之和
	int prim()
	{
		memset(dist, 0x3f, sizeof dist);
		
		int res = 0;
		for (int i = 0; i < n; i ++ )
		{
			int t = -1;
			for (int j = 1; j <= n; j ++ )
				if (!st[j] && (t == -1 || dist[t] > dist[j]))
					t = j;
			
			if (i && dist[t] == INF) return INF;
			
			if (i) res += dist[t];
			st[t] = true;
			
			for (int j = 1; j <= n; j ++ ) dist[j] = min(dist[j], g[t][j]);
		}
		
		return res;
	}

🌟疑难杂症剖析

一、建图

    g[a][b] = g[b][a] = min(g[a][b],c);
因为是无向图,所以需要建立从a 到 b的边 以及 从b 到 a的边

二、计算最小权值之和

        if(i && dist[t] == INF) return INF;
        if(i) res += dist[t];
计算最小的权值之和需要在保证当前这个点能和已经存在的最小生成树集合之间存在最小距离,倘若是距离是正无穷,说明无法与已经存在的最小生成树连通

三、更新

		  for(int j = 1; j <= n;j++)
		     dist[j] = min(dist[j],g[t][j]);
将算法模板的代码实现看完的小伙伴可能发现,Prim算法和朴素版Dijkstra算法好像呀

恍然大悟

相似,又不完全相似
Dijkstra算法中,dist数组维护的是1号点到当前点的距离
Prim算法中,dist数组维护的是当前点到已经存在的最小生成树集合的距离
因此
Dijkstra算法的更新是将t作为中介,将从1号点到j的距离dist[j] 和 1号点到t,再从t到j的距离dist[t]+g[t][j]作比较,找到最小的权值
Prim算法的更新则是,t是作为已经确实的最小生成树集合的代表

Prim算法的更新

💓克鲁斯卡尔算法(Kruskal)——模板题

🌟通俗演示

Kruskal算法通俗演示
Kruskal算法在理解上是比Prim更舒服的

🌟例题描述

Kruskal算法例题
🎇🎇🎇传送门🎇🎇🎇

🌟参考代码(C++版本)

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 200010;

int n,m;
int p[N];//并查集

int cnt,res;//cnt存当前加入多少条边 ; res存放的是最小生成树中所有树边的权重之和

//kruskal算法可以不用邻接表存,只要把点和点到点的边存下来就好
//就不用使用复杂的数据结构,直接结构体搞了,只是要注意重载小于符号,让sort的时候可以根据权重来比较

struct Edge
{
    int a,b,w;
    
    bool operator < (const Edge &W)const
    {
        return w < W.w;
    }
}edges[N];

//并查集的find函数
int find(int x)
{
    if(p[x] != x) p[x] = find(p[x]);
    return p[x];
}

void kruskal()
{
    
    //对存放的边按照权重排序
    sort(edges,edges+m);
    //初始化并查集
    for(int i = 1; i <= n;i++) p[i] = i;

    //从小到大枚举所以边
    for(int i = 0; i < m; i++) 
    {
        int a = edges[i].a, b = edges[i].b, w = edges[i].w;
        
        //找到a的祖宗结点
        a = find(a),b = find(b);
        //如果a 和 b 不在一个连通块中
        if(a !=  b)
        {
            //将a连通到b上
            p[a] = b;
            res += w;
            cnt ++;
        }
    }
}

int main()
{
    
    //输入
    scanf("%d%d",&n,&m);
    
    //建图
    for(int i = 0; i < m;i++)
    {
        int a,b,w;
        scanf("%d%d%d",&a,&b,&w);
        edges[i] = {a,b,w};
    }

    kruskal();
    
    //输出 :输出一个整数,表示最小生成树的树边权重之和
    //n个点,因为成最小生成树只能有n-1边
    if(cnt < n-1) puts("impossible");
    else printf("%d\n",res);
    return 0;
}

🌟算法模板

Kruskal算法的执行流程图如下;
执行流程
Kruskal算法代码实现:

int n, m;		// n是点数,m是边数
	int p[N];		// 并查集的父节点数组

	struct Edge
	{
	    int a,b,w;
	}edges[N];
	
	//自定义比较的方式,待会放置到sort函数中进行比较
	bool cmp(Edge a,Edge b)
	{
	    return a.w < b.w;
	}

	int find(int x)		// 并查集核心操作
	{
		if (p[x] != x) p[x] = find(p[x]);
		return p[x];
	}

	int kruskal()
	{
		sort(edges, edges + m);
		
		for (int i = 1; i <= n; i ++ ) p[i] = i;    // 初始化并查集
		
		int res = 0, cnt = 0;
		for (int i = 0; i < m; i ++ )
		{
			int a = edges[i].a, b = edges[i].b, w = edges[i].w;
			
			a = find(a), b = find(b);
			if (a != b)		// 如果两个连通块不连通,则将这两个连通块合并
			{
				p[a] = b;
				res += w;
				cnt ++ ;
			}
		}
		
		if (cnt < n - 1) return INF;
		return res;
	}

🌻疑难杂症剖析

一、存储边

Kruskal算法和Bellman-Ford算法挺相似的,都是随便大方的乖算法,只要能够获取到存储的信息就好。因此算法模板中可以使用最简单的结构体存储数据。

抱拳了

唯一需要注意的是,要重新制定比较的逻辑,让比较的逻辑是根据权重w来比较的。

二、并查集的使用
2.1、初始化并查集——让每个结点做自己的父结点
2.2、并查集的find函数的编写

嗯哼?对并查集不了解呀。嗦嘎
不是吧

贴心的笔者以前写过关于并查集的博客喔,两篇都是上了热榜的,值得信赖。对并查集不太熟悉的友友就可以看看这篇文章呀,笔芯。

算法基础系列第二章——出发,浅酌并查集

看到这里的小伙伴看到这里可能会觉得,呀,就这,就这呀?!学会了学会了
我又行啦

💓例题一

例题1
🎇🎇🎇传送门🎇🎇🎇
那咱们先来一道简单的活动活动筋骨

🌟解题报告

🌻解题思路
把题目阅览完之后,小伙伴们心中大抵知道这是最小生成树的题,因为题目信息给的很直接呀,hh,题目中让咱们求最小生成树的各边的长度之和。然后看给的数据范围,可以看出是稀疏图,那么Kruskal算法就可以拿出来了。 感觉起来,和咱们上面演示的例题是不是感觉换汤不换药呀。那就开始操作啦~

开始操作

🌻参考代码(C++版本)

🎇🎇🎇点击这里查看参考代码喔~🎇🎇🎇

💓例题二

例题二是蓝桥练习系统上的习题,但是我感觉测试数据好像有问题吧~,跑出来只有50分,我把好几位博主的Ac代码复制过来跑也是50分

这不合理
例题二
🎇🎇🎇传送门🎇🎇🎇

🌟解题报告

🌻解题思路

题目要的是一棵方差最小的生成树
一、数学知识的回忆——方差
方差
二、解决需求——计算方差
这道题要咱们求的方差最小,那么就是要求每条边的(权重 - 平均值)2的和最小。

因为随着每次输入是动态的,所以直接求平均值是比较困难的。这个时候,我们就可以拿出我们可爱的枚举
暴力
我们可以枚举全部边权和的可能,对于每一个可能,都去执行Kruskal算法,执行的时候,把(权重 - 平均值)2作为真正要用的权重w。
同时,当我们先前枚举的边权和等于执行完Kruskal算法之后的生成树权值的和,此时就可以更新答案了。

🌻参考代码(C++版本)

🎇🎇🎇点击这里查看参考代码喔~🎇🎇🎇

🌻疑难杂症剖析

一、执行思路

执行的大框架仍然是可以套用kruskal算法的模板。

kruskal算法执行流程

二、圈定枚举的区间

		//minv就是可能出现的权值和的最小值,maxv则是可能出现的权值之和的最大值
        for(int i = 0; i < n - 1; i++)
            minv += tmp[i];
        for(int i = m - 1; i > m - n; i--)
            maxv += tmp[i];
        for(int i = minv; i <= maxv; i++)
            Kruskal(i);

因为最小生成树有n-1条边。

下界minv可以从存放在tmp数组(注:tmp数组已从小到大排序)中的权值依次获取n-1个最小值。
上界maxv则可以从存放在tmp数组的权值倒着获取n-1个最大值。

三、谨记需求

方差才是我们最后要获取的,所以在kruskal算法中进行排序前,把(权重 - 平均值)^2^作为真正要用的权重w。
    for(int i = 0; i < m; i++)
        e[i].w = (e[i].val - ave) * (e[i].val - ave);
        
	//重新排序
    sort(e, e + m, cmp);

💓例题三

例题描述:
prim算法
🎇🎇🎇传送门🎇🎇🎇

🌟解题报告

🌻解题思路

一、第一感受

在阅览完例题之后,从"为了使花费最少,他希望用于连接所有的农场的光纤总长度尽可能短"这句话可以get到,这道题其实想让我们求这个图的最小生成树。

二、看数据范围定算法

重新看看最小生成树问题中,在什么图中应该使用什么类型的算法模板

最小生成树算法大纲

对于这道题而言,从数据范围可以得知是稠密图,那么我们就可以回忆一下prim算法的实现流程然后去逐步落实。

🌻参考代码(C++版本)

🎇🎇🎇点击这里查看参考代码喔~🎇🎇🎇

🌻疑难杂症剖析
留意这道例题的输入输出,和我们上面用来演示的模板题又不太相似的喔 本题是一个输入,根据输入建立一个邻接矩阵,因此在代码落实到时候就更清爽了,初始化邻接矩阵和建图环节可以放在一起啦~
    //输入
    scanf("%d",&n);
    for(int i = 1;i <= n;i++)
        for(int j = 1;j<=n;j++) cin >>g[i][j];

💓总结

一、脑海中对稠密图和稀疏图应该使用的prim算法和kruskal算法的模板实现流程有个大致的印象,倘若忘了,可以回来看看实现流程和模板题喔
二、留意输入输出,从而对初始化和建图更清晰
三、最小生成树假如只是用来解决考试的话,只要明白通俗演示,然后可以画出各自的生成树就好。对于竞赛的同学就建议先熟悉模板,然后写题巩固,去亲自感受将图论问题中的图抽象出来的过程
四、图论问题难点主要是将问题中这个图抽象出来,然后才可以定,要用dfs解决?还是拓扑排序了?算法模板是可以很快的套上去的

谢谢友友们耐心观看啦~,若有偏颇,欢迎及时私信指出喔💖💖💖

基础算法持续更新中ing~

在这里插入图片描述

  • 29
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 50
    评论
评论 50
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

杨枝

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值