あなたには殺す価値がない…愚かな弟よ…私を殺そうとするなら…憎しみ!憎め!そして醜く生きろ!逃げよう、逃げよう、そして生きていこう!

浅谈最小生成树

浅谈最小生成树

            ———\(\rm BiuBiu\_Miku\)

1.一些概念

  · :在一个中,满足边数等于点数减一的条件。(如图1所示)

  · 生成树:在一个连通图中,截取一个子图,此子图满足树的性质,且通过每一个节点的树称为生成树。(如图2所示)

  · 最小生成树:在一个包含 \(n\) 个节点的加权连通图中,所有边的边权之和最小的树,且通过每一个节点的树,即为最小生成树。(如图3所示)



2.例题引入(题皆出自洛谷

【模板】最小生成树

题目描述

  给出一个无向图,求出最小生成树,如果该图不连通,则输出 \(orz\)

输入格式

  第一行包含两个整数 \(N,M\),表示该图共有 \(N\) 个结点和 \(M\) 条无向边。
  接下来 \(M\) 行每行包含三个整数 \(X_i,Y_i,Z_i\) 表示有一条长度为 \(Z_i\) 的无向边连接结点 \(X_i,Y_i\)

输出格式

   如果该图连通,则输出一个整数表示最小生成树的各边的长度之和。如果该图不连通则输出 \(orz\)

输入输出样例

输入

4 5
1 2 2
1 3 2
1 4 3
2 3 4
3 4 3

输出

7

3.算法实现

   关于算法的实现一般有两种算法,第一种称为 \(Kruskal\) ,第二种称为 \(Prim\)

   实现方法一.\(Kruskal\) 时间复杂度 \(O( MlogM )\)\(M\)为边数)

     \(Kruskal\) 是一种利用并查集来实现最小生成树的办法,其算法流程大致为。

     先将读进来的边按照边权排序,然后利用并查集建立关系,一但发现如果两个点不存在关系,那么就建这条边,不然的话就不建,最后把建了边的边权统计起来就好了。

     为什么这样就可以找到最小生成树呢?因为我们已经将边权排序过了,也就是说越前面的边他的边权就越小,因此我们就可以直接通过简单的步骤得到最小生成树了!

     以上面的题目的样例来做例子,来模拟一下该算法全过程:

      1.排序,排序后得到以下的数据

1 2 2
1 3 2
1 4 3
3 4 3
2 3 4

      2.发现 \(1\)\(2\) 不连通,于是建边,如下图所示:

      

      3.发现 \(1\)\(3\) 不连通,于是建边,如下图所示:

      

      4.发现 \(1\)\(4\) 不连通,于是建边,如下图所示:

      

      5.发现 \(3\)\(4\) 已经连通,于是不建边。

      6.发现 \(2\)\(3\) 已经联通,于是不建边。

      7.经统计,一共建的边的边权之和为 \(7\) 于是就输出 \(7\)

     \(Code:\)
#include<bits/stdc++.h>
using namespace std;
int f[200005],m;
struct edge{
	int a,b,lo;
}E[200005];	//边 
int n,ans;
bool cmp(edge x,edge y){return x.lo<y.lo;} //排序 
int getfather(int k){ //并查集 
	if(f[k]==k)return k;
	return f[k]=getfather(f[k]);
}
int main(){
    scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)f[i]=i;
	for(int i=1;i<=m;i++)scanf("%d%d%d",&E[i].a,&E[i].b,&E[i].lo);
	sort(E+1,E+1+m,cmp); //排序 
	for(int i=1;i<=m;i++){
		if(getfather(E[i].a)!=getfather(E[i].b)){	//判断两个点是否连通 
			ans+=E[i].lo;	//若不连通建边 
			f[getfather(E[i].a)]=getfather(E[i].b);
			n--;	//减掉一个未连通点 
		}
		if(n==1){	//若全部连通就输出答案 
        	printf("%d\n",ans);
			return 0;
		}
	}
	printf("orz\n");	//否则输出orz 
	return 0;
} 

   实现方法二.\(Prim\) (时间复杂度根据不同情况,不同计算)

     \(Prim\) 是一种利用不断寻找最小值来实现找到最小生成树的方法。

     具体来说就是以某一个点为起点(一般选择节点 \(1\) 为起点),寻找能走的边中边权(代价)最小的边,再将此点收入囊中,也就是看做是一段子图。

     然后更新能走的边所花费的代价,因为当我加入一个节点后,该节点也可以通到别的点,而且从此新节点出发走原来可以走的边,可能存在更小代价.

    例如:若此时 \(A\) 已经与 \(B\) 已经连接,\(A\)\(C\) 代价为 \(3\)\(B\) 存在一条边到 \(C\) 的代价为 \(1\) ,那么此时就可以更新当前可以走的边。

     以上面的题目的样例来做例子,来模拟一下\(Prim\) 算法全过程:

      1.选择节点 \(1\) 为起点,并发现当前 \(1\)\(2\)\(1\)\(3\) 都是当前能走的边中边权最小的,于是任意走一条即可(这里选择走 \(1\)\(2\) ),如下图所示。

      

      2.当前能走的边中发现 \(1\)\(3\) 是边权最小的边,于是走此边,如下图所示。

      

      3.当前能走的边中,发现当前 \(1\)\(4\)\(3\)\(4\) 都是当前能走的边中边权最小的,于是任意走一条即可(这里选择走 \(1\)\(4\) ),如下图所示。

      

      4.找到最小生成树,经统计,走过的边权总和为 \(7\) 于是输出 \(7\)

     \(Code:\) (这里用邻接矩阵存图,复杂度为 \(O(N^2)\) N表示点的数量 )
#include<bits/stdc++.h>
using namespace std;
int n,mmin=INT_MAX,dis[200005],ans,w[5005][5005],walk,m;	
bool vis[200005];	//vis表示该边是否被访问过 
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
			w[i][j]=1e9;		//矩阵初始化 
	for(int i=1;i<=m;i++){
		int a,b,c;
		scanf("%d%d%d",&a,&b,&c);
		w[a][b]=min(c,w[a][b]);
		w[b][a]=min(c,w[b][a]); 	//邻接矩阵存图 
	}
	for(int i=1;i<=n;i++)dis[i]=w[1][i];	//初始化从起点开始能到达的边,能到达就赋值走到该点要付出的代价,否则为1e9 
	dis[1]=0;
	vis[1]=true;
	for(int i=2;i<=n;i++){
		mmin=1e9;				
		for(int j=2;j<=n;j++)
			if(dis[j]<mmin&&!vis[j]){	//找到当前所有能走的边的最小值 
				mmin=dis[j];
				walk=j;
			}
		ans+=mmin;			//走这条边 
		vis[walk]=true;		//标记已经走过 
		for(int j=2;j<=n;j++)if(!vis[j])dis[j]=min(dis[j],w[walk][j]); //更新能走的边花费的价值 
	} 
	printf("%d\n",ans); 
	return 0; 
} 

4.题目推荐

  [USACO05MAR]Out of Hay S:相当于模板(双倍经验,岂不美哉)
  [USACO3.1]最短网络 Agri-Net:本题用 \(for\) 建边后跑流程即可。
  公路修建:本题要运用两点之间的距离公式 \(\sqrt{( x_1 - x_2 )^2+( y_1 - y_2 )^2}\) 来计算边权,然后跑流程就好了,本题推荐使用 \(Prim\) 来实现。

  \(PS\) :以上题目适合初学者食用。

5.结语

  以上便是此博客的全部内容,其主要适用于没学过最小生成树的人用,有什么写得不好也欢迎大佬在评论区指出,感谢大佬的阅读。

posted @ 2020-12-18 22:20  BiuBiu_Miku  阅读(657)  评论(1编辑  收藏  举报
骗分过样例,暴力出奇迹!