洛谷P3366【模板】最小生成树-克鲁斯卡尔Kruskal算法详解附赠习题

链接

题目描述

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

输入输出格式

输入格式:

第一行包含两个整数N、M,表示该图共有N个结点和M条无向边。(N<=5000,M<=200000)

接下来M行每行包含三个整数Xi、Yi、Zi,表示有一条长度为Zi的无向边连接结点Xi、Yi

输出格式:

输出包含一个数,即最小生成树的各边的长度之和;如果该图不连通则输出orz

输入输出样例

输入样例#1:
4 5
1 2 2
1 3 2
1 4 3
2 3 4
3 4 3
输出样例#1:
7

说明

时空限制:1000ms,128M

数据规模:

对于20%的数据:N<=5,M<=20

对于40%的数据:N<=50,M<=2500

对于70%的数据:N<=500,M<=10000

对于100%的数据:N<=5000,M<=200000

分析

这是一道求最小生成树的模板题。

假设一个拥有n个节点的图,若它是个连通图,则可求其最小生成树。什么是最小生成树?先来解释一下生成树。

如果连通图 G的一个子图是一棵包含G 的所有顶点的树,则该子图称为G的生成树。

简而言之,就是通过连接一些边而使这个图的任意节点之间直接或间接地相连通。最小生成树,可以理解为连接的边的边权和最小时的生成树。一般而言,常用的求最小生成树的方法有克鲁斯卡尔Kruskal和普里姆Prim算法,在本篇中仅介绍克鲁斯卡尔算法。它的思想其实非常好理解,我们把每个点看做一个集合。

  1. 在一开始,设每个点各成一个集合,也就是运用并查集的思想。
  2. 读入边后,将边从小到大排序,运用的是贪心思想。这样可以保证最终的结果值最小。
  3. 我们从最小的边开始,如果这条边两端的点不在一个集合内,就把这条边连起来,再把两端的点并成一个集合。
  4. 当连接的边的数量是点数-1时,我们就得到我们要求的最小生成树了。

”当连接的边的数量是点数-1时“,为什么呢?若这个图中有n个点,则我们要做的是把n个集合连成一个集合,边数当然必须是n-1。多一条则会成环,少一条则无法联通。

以输入样例为例,这个图一开始就是这样的(样例似乎有些不合理,画图水平也实在不行,将就着看):

洛谷P3366【模板】最小生成树-克鲁斯卡尔Kruskal算法详解附赠习题

我们对边进行排序,得到最小的边是连接1,2和1,3的两条边。这里我们先处理1,2这条边,我们知道它们此刻当然不在一个集合中,于是连接。

洛谷P3366【模板】最小生成树-克鲁斯卡尔Kruskal算法详解附赠习题

如上图,sum表示当前所求出的边权之和。这时该操作的就是1,3之间的连线,它们目前并不属于一个集合,所以我们把它们连起来,点1,2,3就处于一个集合中啦。

洛谷P3366【模板】最小生成树-克鲁斯卡尔Kruskal算法详解附赠习题

再接下来剩下三条边,权值分别为3,3,4,那么我们先处理1,4之间的这条边,很明显点4现在并不与1处于同一个集合。我们把它连起来。

洛谷P3366【模板】最小生成树-克鲁斯卡尔Kruskal算法详解附赠习题

这时我们发现,连接的边数为3,已经等于点数-1了,退出操作,输出结果7.观察此时的图,四个点确实都在连线中,这个图的最小生成树值为7.

这就是克鲁斯卡尔Kruskal的总体思想,非常易于理解。

代码

首先考虑如何存边,由上面的思考过程我们可以想到,我们希望可以快捷简便的获得一条边的序号、起止点和权值。二维数组显然不是理想解法,我们考虑结构体

 struct bia{
int s,t;//s表示起点,t表示终点
long long c;//c表示权值,使用long long或int应依数据范围决定
};
bia b[];

如果要将结构体表示的边从小到大进行排序,直接使用sort会出问题。我们可以自己编写一个cmp函数,表示排序规则。

 bool cmp(bia a,bia b){
if(a.c<b.c)
return ;
return ;
}

我个人习惯一种更简洁的写法,一会会在完整程序中展现。

考虑到”把每个点看成一个集合“的并查集思想,我们当然也需要设一个数组fa[]表示每个点所属集合啦。但在这里我们只需要并查集比较常见的查询,合并可以直接用简洁的fa[u]=v来表示。

 int find(int x){
if(fa[x]==x)
return x;
else
{
fa[x]=find(fa[x]);
return fa[x];
}
}

这里同样有一种更简洁的写法。

处理完这些,我们就可以编写克鲁斯卡尔算法的主体程序了。

 void kru(){
for(int i=;i<=m;i++)//按权值从小到大的顺序对边进行操作
{
int u=find(b[i].s),v=find(b[i].t);//u和v分别表示这条边两端节点所属的集合
if(u==v)
continue;//若该边连接的两点已经处于一个集合中,则不必进行连接,直接进行下一条边
fa[u]=v;//将该边连接的两点进行合并
ans+=b[i].c;//ans即上图中的sum,表示所求的最小生成树之值
s++;//用s统计目前已连接的边数,若已连接n-1条边,则结束程序,输出结果
if(s==n-)
return;
}
}

这样我们就完成了克鲁斯卡尔算法,建议先通过洛谷P3366【模板】最小生成树这道题。AC代码如下。

 #include <iostream>
#include <algorithm>//sort需要的头文件
using namespace std;
struct bia{
int s,t;//s表示起点,t表示终点
long long c;//c表示权值,使用long long或int应依数据范围决定
};
bool cmp(bia a,bia b){
return a.c<b.c;//这是一种更简洁的写法
}
bia b[];//用b数组存储边
int n,m,ans,s,fa[];//n为点数,m为边数,ans为答案,s为已连接边数,fa[]表示点所在的集合
int find(int x){
return x==fa[x]?x:fa[x]=find(fa[x]);//也是一种简便写法
}
void kru(){
for(int i=;i<=m;i++)//从最小的边开始操作
{
int u=find(b[i].s),v=find(b[i].t);//u和v分别表示该条边两端节点所处的集合
if(u==v)
continue;//若两点属于同一集合,该边不需要连接,直接操作下一条
fa[u]=v;//将两点并入同一集合
ans+=b[i].c;//将该边边权加入答案
s++;//累计目前已连接边的边数,若为n-1则结束运算并输出答案
if(s==n-)
return;
}
}
int main()
{
cin>>n>>m;
for(int i=;i<=n;i++)
fa[i]=i;//初始化每个点各成一个集合
for(int i=;i<=m;i++)
cin>>b[i].s>>b[i].t>>b[i].c;
sort(b+,b+m+,cmp);
kru();
if(s==n-)
cout<<ans;
else
cout<<"orz";//实际上本题并不需要输出orz,数据中没有无解情况,但我们仍应考虑所有题目条件
return ;
}

评测结果(截图是无注释版的结果,仍可以进行快读快写之类的优化,但这个题显然并不需要)

洛谷P3366【模板】最小生成树-克鲁斯卡尔Kruskal算法详解附赠习题

值得一提的是,克鲁斯卡尔算法的时间复杂度为O(mlogn),普里姆算法的复杂度为O(n^2),优化后可达到O(mlogn)(实际上还可进行优化,但编程复杂度也会相应提高许多)。在面对稀疏图(边数较少的图)时不妨采用克鲁斯卡尔算法,在面对稠密图(边数较多)的图时则可考虑普里姆优化算法。

完成模板题后,不如再进行几道练习题练手——都是较为简单的题目,对模板稍加改动即可。附上AC代码,复制粘贴可能被kkk制裁哦。

洛谷P2820局域网

 #include <iostream>
#include <algorithm>
using namespace std;
struct bia{
int s,t;
long long c;
};
bool cmp(bia a,bia b){
return a.c<b.c;
}
bia a[];
int n,k,sum,ans,s,fa[];
int find(int x){
return x==fa[x]?x:find(fa[x]);
}
void kru(){
for(int i=;i<=k;i++)
{
int u=find(a[i].s),v=find(a[i].t);
if(u==v)
continue;
fa[u]=v;
s++;
sum+=a[i].c;
if(s==n-)
return;
}
}
int main()
{
cin>>n>>k;
for(int i=;i<=n;i++)
fa[i]=i;
for(int i=;i<=k;i++)
cin>>a[i].s>>a[i].t>>a[i].c,ans+=a[i].c;
sort(a+,a+k+,cmp);
kru();
ans-=sum;
cout<<ans;
return ;
}

局域网

洛谷P2330[SCOI2005]繁忙的都市

 #include <iostream>
#include <algorithm>
using namespace std;
struct bia{
int s,t;
int c;
};
bool cmp(bia a,bia b){
return a.c<b.c;
}
bia a[];
int n,m,ans,sum,fa[];
int find(int x){
return x==fa[x]?x:find(fa[x]);
}
void kru(){
for(int i=;i<=m;i++)
{
int u=find(a[i].s),v=find(a[i].t);
if(u==v)
continue;
fa[u]=v;
sum++;
ans=max(ans,a[i].c);
if(sum==n-)
return;
}
}
int main()
{
cin>>n>>m;
for(int i=;i<=n;i++)
fa[i]=i;
for(int i=;i<=m;i++)
cin>>a[i].s>>a[i].t>>a[i].c;
sort(a+,a+m+,cmp);
kru();
cout<<sum<<" "<<ans;
return ;
}

繁忙的都市

一本通1393联络员(liaison)

 #include <iostream>
#include <algorithm>
using namespace std;
struct bia{
int s,t,c,y;
};
bool cmp(bia a,bia b){
if(a.y!=b.y)
return a.y<b.y;
else
return a.c<b.c;
}
bia a[];
int n,m,sum,ans,fa[],j;
int find(int x){
return x==fa[x]?x:find(fa[x]);
}
void kru(){
for(int i=;i<=m;i++)
{
int u=find(a[i].s),v=find(a[i].t);
if(a[i].y==)
{
if(u==v)
continue;
}
else
{
if(u==v)
sum--;
}
fa[u]=v;
sum++;
ans+=a[i].c;
if(sum==n-)
{
j=i;
return;
}
}
}
int main()
{
cin>>n>>m;
for(int i=;i<=n;i++)
fa[i]=i;
for(int i=;i<=m;i++)
cin>>a[i].y>>a[i].s>>a[i].t>>a[i].c;
sort(a+,a+m+,cmp);
kru();
for(int i=j;i<=n;i++)
if(a[i].y==)
ans+=a[i].c;
else
break;
cout<<ans;
return ;
}

联络员

一本通1394连接格点(grid)

 #include <iostream>
using namespace std;
int fa[],n,m,x1,y1,x2,y2,sum;
int find(int x){
return x==fa[x]?x:fa[x]=find(fa[x]);
}
bool unionn(int a,int b){
int u=find(a),v=find(b);
if(u!=v)
{
fa[v]=u;
return ;
}
else
return ;
}
int main()
{
cin>>n>>m;
for(int i=;i<=n*m;i++)
fa[i]=i;
while(cin>>x1>>y1>>x2>>y2)
unionn((x1-)*m+y1,(x2-)*m+y2);
for(int i=;i<=m;i++)
for(int j=;j<n;j++)
if(unionn((j-)*m+i,j*m+i))
sum++;
for(int i=;i<=n;i++)
for(int j=;j<m;j++)
if(unionn((i-)*m+j,(i-)*m+j+))
sum+=;
cout<<sum;
return ;
}

连接格点

上一篇:关于python中的字符串编码理解


下一篇:thinkPHP 模板中的语法知识 详细介绍(十二)