文章目录
多图生动理解并查集&最小生成树(附核心代码模板)
相关知识
并查集是什么?
并查集指的对于集合及其元素的两类“操作”,即“并”和“查”。简单来说,“并”指的是不同集合的合并操作,“查”指的是查看元素具体属于哪一个集合。
如何进行集合初始化?
并查集涉及集合,那么集合如何初始化呢?是初始化为空集,还是本身,抑或其它?
开始时,每一个元素的集合都是未知的,我们规定:一开始的时候每一个元素的所在集合都是在以自己为名的集合。因此,集合初始化长这样:for(i=1;i<=n;i++)sets[i]=i;
。
其中,元素从1~n,sets[i]代表第i个元素所在的集合名。
合并后的新集合名是什么?
一开始1在集合1,3在集合3,5在集合5,现在要把135合并到一个集合中,那么这个集合是集合几?
类似于集合初始化,我们继续规定,新集合中的最小值为合并后的新集合名。
并查集和树有什么关系?
本质上来说,一颗树结构就是一类集合,不过是一类有联系的集合。
树的命名规则还是同集合,以树中最小的节点作为树名,作为祖先。同集合中以最小的元素作为祖先,如下图所示,树1本质上也是集合5={1,2,3,4},树5本质上也是集合5={5,6,7}。
并查集,一个操作是“查”。对于任意一个元素i,我们既可以查它的父亲(Sets[i]),也可以查它的祖先([Find(i)),如下图所示(sets是我们定义的集合,sets[i]的值代表第i个元素的父节点信息;find是我们定义的一个查找函数,find(i)代表的值第i个元素的祖先节点信息,如图下代码所示):
“查”的代码:
//1.并查集中查元素i的父亲
// sets[i]即可
//2.并查集中的查祖先(先定义查函数后定义并函数,因为并的过程中要用到查函数)
int find(int x){ //并查集中的查
int r=x; //不能改变x的值,因此定义一个变量r来等价x
while(sets[r]!=r){ //通过不断地访问父亲的父亲,找到祖先
r=sets[r];
}
return r; //返回祖先的值
}
“并”的操作,本质上就是将两颗单独的树进行连接,具体怎么连接,由连接节点决定,如下图所示,就是连接节点3和节点5:
以下是并查集中并的代码:
// 并查集中的并
void merge(int a,int b){ //并查集中的并,只需要改变,不需要返回值,因此就用void即可
int fa=find(a),fb=find(b); //找元素a,元素b的祖先,存于fa,fb中
if(fa!=fb) sets[fb]=fa; //若祖先不同,就将fb的祖先改成fa,即可实现合并
}
以上,就是“并查集”在“树”中的应用的形象阐述。
什么是最小生成树?
假设有如下的一个带权图
在这个图中,总共有6个节点,10条带权值的边。最小生成树指的具有最小边权值和的全节点联通树。
举个例子,我们将这6个节点理解为6座城市,这10条边理解为相应的城市与城市之间可以搭建的桥,边上的权值对应建桥的经济费用。此时的最小生成树,指的就是花最小经济费用下实现6座城市联通的桥梁搭建形式。
如何在图上选边构成最小生成树?
克鲁斯卡尔算法是解决最小生成树问题的一种常见方法。
根据贪心策略,先用结构体数组存储所有的边信息(包括边的两个节点,以及边上的权值),例如“1 4 5”代表节点1和节点4之间的距离是5。然后再根据节点间距的大小进行升序排序。然后进入条件选边模式,从1~N(总边数)依次选边,如果满足不会构成回路==(待连接的两个点的祖先不相同,就不会构成回路;反之依然)==,就选择这条边,直至到达顶点数-1条边,终止选边。此时构成的树,就是最小生成树。
以下就是利用克鲁斯卡尔算法求解最小生成树问题的核心代码示例(有用到前面讲的find查函数):
struct node{ //将图中的边信息存储到edge数组中
int u,v,w; //两个节点的信息以及两个节点之间的距离
}edge[105*105]; //edge存储“边”信息,数组规模可修改
bool cmp(node a,node b){ //定义排序规则:按照权重从小到大排序
return a.w<b.w;
}
int cnt=0,sum=0; //cnt表示选中的边数,sum表示目前的权值和
//“克鲁斯卡尔算法”的应用,n代表边数,m代表节点数
sort(edge+1,edge+n+1,cmp);//将边数组的信息按照距离进行升序排序,
for(int i=1;i<=n;++i){ //遍历所有边,进行边选择
if(cnt==m-1)break;
int c=find(edge[i].u),d=find(edge[i].v); //将桥的两个节点的祖先信息存储在c和d中
if(c!=d) sets[c]=d,cnt++,sum+=edge[i].w; //环路判断,不是同祖先,就可以选入,同时更新cnt和sum的信息
}
并查集的核心代码模板
// 1.并查集中的集合定义+最小生成树的“边”信息定义(边信息包括“边的两个节点”以及“边的权重”)
//只能用sets而不能用set,因为set是c++中的一种容器,用set就和它冲突了,会报错
int sets[1005]; //长度不一定是1005,并查集中集合的代码一定要在"最前面定义",作为“全局变量”,更合适
struct node{ //将图中的边信息存储到edge数组中
int u,v,w; //两个节点的信息以及两个节点之间的距离
}edge[105*105]; //edge存储“边”信息,数组规模可修改
bool cmp(node a,node b){ //定义排序规则:按照权重从小到大排序
return a.w<b.w;
}
// 2.并查集中的查(先定义查函数后定义并函数,因为并的过程中要用到查函数)
int find(int x){ //并查集中的查
int r=x; //不能改变x的值,因此定义一个变量r来等价x
while(sets[r]!=r){ //通过不断地访问父亲的父亲,找到祖先
r=sets[r];
}
return r; //返回祖先的值
}
// 3.并查集中的并
void merge(int a,int b){ //并查集中的并,只需要改变,不需要返回值,因此就用void即可
int fa=find(a),fb=find(b); //找元素a,元素b的父亲,存于fa,fb中
if(fa!=fb) sets[fa]=fb; //老大不是一个人,说明不在一个集合里,让fa的领导改成fb,那么就只有一个老大fb了
}
// 4.并查集中的集合初始化,均自己先是自己的祖先
//以下,n代表边数,m代表节点数,节点编号默认从1到m
for(int i=1;i<=m;i++)sets[i]=i;
// 5.最小生成树权值和计算(考虑了无法构成最小生成树的情况,可以根据题境灵活修改)
int cnt=0,sum=0; //cnt表示选中的边数,sum表示目前的权值和
//“克鲁斯卡尔算法”的应用,n代表边数,m代表节点数
sort(edge+1,edge+n+1,cmp);//将边数组的信息按照距离进行升序排序,
for(int i=1;i<=n;++i){ //遍历所有边,进行边选择
if(cnt==m-1)break;
if(find(edge[i].u)!=find(edge[i].v)) cnt++,sum+=edge[i].w;//不构成环路,就选择
merge(edge[i].u,edge[i].v); //连接两个节点
}
//以上模板经过题境适当修改后即可用
并查集可以解决哪些问题?
主要包括三类问题:
①图的连通性问题:是不是各个节点之间可以相互到达?至少还差几条边才能相互到达?
图联通之后就只能有一个祖先,那么只需要判断,距离一个祖先还差多少,即可求出还差多少边就能相互到达。例如,现在4个祖先,那么就还差4-1=3条边即可实现联通。
利用并查集中的“查”,查祖先是自己的元素个数,然后减1,即为距离连通还差的边数。
②最小生成树问题:给我们一张图,怎么在图中选边构造树,才能让生成树的各边权值和最小?如,权值是钱,就是问怎么搭桥最省钱;权值是里程,就是问怎么搭桥里程最短?
利用克鲁斯卡尔算法模板(用到了并查集中的并和查)即可,最后的sum值就是最小生成树的权值。
③图类的判断问题:规定一类图,比如有没有环路,怎么判断输入的图是不是符合定义规则的图?
在每一次连接两个元素的时候,查看这两个元素是否同祖先。如果同祖先,再连接一定会形成环路。
相关题目
图的连通性问题
畅通工程
参考思路
查祖先是自己的元素个数,然后减1,即为距离连通还差的道路数目。
参考代码
#include<bits/stdc++.h>
using namespace std;
#define endl "\n"
int sets[1002];
int find(int x){
int r=x;
while(sets[r]!=r){
r=sets[r];
}
return r;
}
void merge(int a,int b){
int fa=find(a),fb=find(b);
if(fa!=fb) sets[fa]=fb;
}
int main(){
ios::sync_with_stdio(false);
std::cin.tie(0);
int a,b,cnt,city,qiao;
while(cin>>city&&city){
// if(city==0)return 0;
cin>>qiao;
for(int i=1;i<=city;i++)sets[i]=i;
for(int i=1;i<=qiao;i++){
cin>>a>>b;
merge(a,b);
}
cnt=0;
for(int i=1;i<=city;i++){
if(sets[i]==i)cnt++;
}
cout<<cnt-1<<endl;
}
return 0;
}
最小生成树问题
1、还是畅通工程(基本版最小生成树)
参考思路
遍历每条边,通过克鲁斯卡尔算法选边连接,求得最终的最小生成树的权值和。
参考代码
#include<bits/stdc++.h>
using namespace std;
int n,vis[100];
struct ct{
int c1,c2,d;
};
bool cmp(ct a,ct b){ //按照距离从小到大排序
return a.d<b.d;
}
int find(int x){
int r=x;
while(vis[r]!=r){
r=vis[r];
}
return r;
}
void merge(int a,int b){
int fa=find(a),fb=find(b);
if(fa!=fb) vis[fa]=fb;
}
int main() {
ios::sync_with_stdio(false);
ct c[4852];
while(cin>>n&&n){ //输入城市数量
int m=n*(n-1)/2;
int sum=0;
for(int i=1;i<=n;i++){ //初始化并查集
vis[i]=i;
}
for(int i=1;i<=m;i++){
cin>>c[i].c1>>c[i].c2>>c[i].d;
}
sort(c+1,c+m+1,cmp); //将输入的数据按距离从小到大排序
for(int i=1,k=0;i<=m;i++){
if(find(c[i].c1)!=find(c[i].c2)) k++,sum+=c[i].d; //不会串联,就加上距离于sum,增加一次相连次数k
merge(c[i].c1,c[i].c2); //然后再相连两座城市
if(k==n-1) break; //如果连了n-1条桥,说明足够了,可以跳出for循环
}
cout<<sum<<"\n"; //减少代码运行时间
}
return 0;
}
2、畅通工程-2007(不一定能畅通的考虑)
参考思路
大体思路同上题,克鲁斯卡尔算法循环出来后,看选边数有没有达到m-1,没有就输出?,有就正常输出即可。
参考代码
#include<bits/stdc++.h>
using namespace std;
int n,m,vis[105];
int find(int x){
return vis[x]==x?x:vis[x]=find(vis[x]);
}
void merge(int a,int b){
int fa=find(a),fb=find(b);
if(fa!=fb) vis[fa]=fb;
}
struct node{
int u,v,w;
}edge[105*105];
bool cmp(node a,node b){
return a.w<b.w;
}
void solve(){
for(int i=1;i<=m;++i) vis[i]=i;
for(int i=1;i<=n;++i){
int u,v,w;
cin>>u>>v>>w;
edge[i]={u,v,w};
}
sort(edge+1,edge+n+1,cmp);
int cnt=0,sum=0;
for(int i=1;i<=n;++i){
if(cnt==m-1)break;
if(find(edge[i].u)!=find(edge[i].v))
merge(edge[i].u,edge[i].v),cnt++,sum+=edge[i].w;
}
if(cnt==m-1) cout<<sum<<'\n'; //如果不生成环路的情况下能搭建的桥数能够等于n-1,那么就能输出数值,否则输出问号
else cout<<"?\n";
}
int main(){
ios::sync_with_stdio(0);
while(cin>>n>>m&&n) solve(); //题目意思就是,如果n=0,还是要把m输入才行
}
3、继续畅通工程(已经有连接的考虑)
参考思路
基本思路仍如上,只不过需要对连好的和没有连好的进行分类讨论——已经连好的,我们就用merge把它们连上,没连好的,我们就先把它们存在edge数组里面,后续用克鲁斯卡尔算法进行筛选即可。
参考代码
#include<bits/stdc++.h>
using namespace std;
int n,vis[105];
int find(int x){
return vis[x]==x?x:vis[x]=find(vis[x]);
}
void merge(int a,int b){
int fa=find(a),fb=find(b);
if(fa!=fb) vis[fa]=fb;
}
struct node{
int u,v,w;
}edge[105*105];
int cmp(node a,node b){
return a.w<b.w;
}
void solve(){
for(int i=1;i<=n;++i) vis[i]=i;
int m=n*(n-1)/2,num=0,sum=0; //m代表可连接总数
for(int i=1;i<=m;++i){ //for循环存储可连接的长度和状态w表示长度,op表示状态
int u,v,w,op;
cin>>u>>v>>w>>op;
if(op){ //如果是已经连接好的,就建立连接
merge(u,v);
}
else edge[++num]={u,v,w}; //没连接的先不直接连,存在边信息里,到时候用克鲁斯卡尔算法筛选一下
}
sort(edge+1,edge+num+1,cmp); //根据成本从小到大排序
for(int i=1;i<=num;++i){
int c=find(edge[i].u),d=find(edge[i].v);
if(c!=d) vis[c]=d,sum+=edge[i].w;
}
cout<<sum<<'\n';
}
int main(){
ios::sync_with_stdio(0);
while(cin>>n&&n) solve(); //输入城镇数n
}
图类的判断问题
参考思路
初始化
初始化的时候要注意,由于这题并不是1~m,因此我们先把所有元素的祖先都定义为0,导入数据时,如果祖先为0,那么就让它们初始化为自己的祖先,即用到啥元素初始化啥元素。
迷宫判断
先判断是不是一个迷宫,迷宫的话各节点只能有一个祖先,例如下图有3个祖先的图,就连迷宫都算不上。
再者,题中说不能有回头路,那么就在每次输入两个节点的时候看是否为同祖先,同祖先相连必然构成环路,也就是环路,如下图所示,2和3具有相同的祖先1,如果相连,就会构成回路
参考代码
#include<stdio.h>
#include<string.h>
#define N 100005 //大数值用常量运行更快,效率更高
int sets[N]={0}; //由于此时数据并不是数组数据全部都初始化为0
int find(int x)
{
//return x==sets[x]?x:sets[x]=find(sets[x]);
while(x!=sets[x])
x=sets[x];
return x;
}
void merge(int x,int y)
{
int fy,fx;
fy=find(y);
fx=find(x);
if(fy!=fx)
{
sets[fy]=fx;
}
}
int main()
{
int k,i,a,b,flag;
int sum;
k=1;
while(1)
{
flag=0;
while(scanf("%d %d",&a,&b)!=EOF&&a&&b) //当输入0 0的时候跳出此循环,进入下一个循环判断
{
if(a==-1||b==-1) //输入某个数值全部都结束,最快的方式是直接return 0;
return 0;
if(sets[a]==0) sets[a]=a; //如果某房间没有老大,那么我们就让他做老大
if(sets[b]==0) sets[b]=b;
if(find(a)==find(b)) flag=1; //如果同祖先,就会导致环路,用flag=1记录这种情况
merge(a,b); //连接该两点
}
for(i=1,sum=0;i<=N;i++) //查找祖先数
{
if(sets[i]==i) sum++;
sets[i]=0;
}
if(sum>1||flag==1) //多祖先\环路任意一种情况存在,都不是小希的迷宫
printf("No\n");
else printf("Yes\n");
}
return 0;
}
参考
《2021杭电ACM-LCY算法培训入门班》
有任何问题,可以在评论区留言~
祝点赞的各位码力飞进!!!谢谢~