目录
一. 并查集
1. 基本概念
并查集(Union-Find)是一种用来判断一个集合中相互关联的元素属于几个集合,也可以用来判断图结构中的两点是否是连通, 它也是一种树型的数据结构,用于处理一些不相交集合(Disjoint Sets)的合并及查询问题。简而言之,并查集就是合并集合或者在集合里查找元素的算法,可求连通子图、求最小生成树等。 贴一个比较有意思的并查集讲解:好玩的并查集详解
2. 引例
杭电HDU1232畅通工程
Problem Description
某省调查城镇交通状况,得到现有城镇道路统计表,表中列出了每条道路直接连通的城镇。省政府“畅通工程”的目标是使全省任何两个城镇间都可以实现交通(但不一定有直接的道路相连,只要互相间接通过道路可达即可)。问最少还需要建设多少条道路?
Input
测试输入包含若干测试用例。每个测试用例的第1行给出两个正整数,分别是城镇数目N ( < 1000 )和道路数目M;随后的M行对应M条道路,每行给出一对正整数,分别是该条道路直接连通的两个城镇的编号。为简单起见,城镇从1到N编号。
注意:两个城市之间可以有多条道路相通,也就是说
3 3
1 2
1 2
2 1
这种输入也是合法的
当N为0时,输入结束,该用例不被处理。Output
最少还需要建设道路的数量
题意就是求有几个独立的联通块,比如有三个独立的联通块,那么我只要修两条路就可以把这三个联通为一个了,那么城镇有几百个,还可能有环,这个怎么判断呢?不要怕!接下来我们来讲并查集。
3. 并查集的实现
3.1 实现思路
用集合中的某个元素来代表这个集合,则该元素称为集合的代表元
。一个集合内的所有元素组织成以代表元为根的树形结构。对于每一个元素 parent[x] 指向x在树形结构上的父亲节点。如果x是根节点,则令parent[x] = x。
对于查找操作,假设需要确定x所在的的集合,也就是确定集合的代表元。可以沿着parent[x]不断在树形结构中向上移动,直到到达根节点。因此,并查集的实现包含三个部分:
- pre数组,用来保存每个节点的前导元素节点
- find函数,用于查找元素的根节点,并且压缩路径
- join函数,用于合并集合
我们每次输入数据,就用find函数查找他们的根节点,然后用join将两根结点连到一起,于是数据就有了共同的根节点,就代表在一个集合里有了共同的代表元素!所以,判断两个元素是否属于同一集合,只需要看他们的代表元是否相同即可。
路径压缩:是为了加快查找元素的速度,查找时将x到根节点路径上的所有点的parent设为根节点,该优化方法称为压缩路径。
3.2 核心代码
(1)pre[maxn] //记录前导点
(2)初始化前导数组pre
for(int i=1; i<=maxn; i++)
pre[i]=i;//每个点的前导点设为自己
(3)查找元素代表元
int finded(int a)
{
int r=a;
while(pre[r]!=r)
r=pre[r];//找到a的根节点r
int i=a,j;
while(i!=r)//路径压缩,将a以上的所有节点全部连接到根节点上!
{
j=pre[i];
pre[i]=r;
i=j;
}
return r;//返回根节点
}
(4)合并集合
void join(int x,int y)
{
int fx=finded(x);//找到根结点
int fy=finded(y);
if(fx!=fy)
{
pre[fx]=fy;//合并
}
}
(5) 秩优化
为了解决大数据下的退化问题,提高查找效率,使用rank数组记录每个节点为根下的深度上界,深度上界小的连接在深度上界大的上面,从而防止退化!而路径压缩过程中不更新rank数组。其代码如下:
void join(int x,int y)
{
int fx=finded(x);
int fy=finded(y);
if(fx!=fy)
{
if(ranked[fx]<ranked[fy])//深度小的连接在深度大的根节点上
pre[fx]=fy;
else
{
pre[fy]=fx;
if(ranked[fx]==ranked[fy])
ranked[fx]++;
}
}
}
4. 引例解决
注意输入完数据以后,所有节点的pre并不能全部更新为最终根节点,所以如果想求是否在一个集合里,还要跑一边find函数。
#include <iostream>
#include<bits/stdc++.h>
using namespace std;
int pre[1001];
int fined(int a)
{
int r=a;
while(pre[a]!=a)
a=pre[a];
int i=r,j;
while(i!=a)
{
j=pre[i];
pre[i]=a;
i=j;
}
return a;
}
void join(int x,int y)
{
int fx=fined(x),fy=fined(y);
if(fx!=fy)
pre[fx]=fy;
}
int main()
{
int n,m;
while(cin>>n&&n)
{
cin>>m;
int sum=0;
memset(vis,0,sizeof(vis));
for(int i=1;i<=n;i++)
pre[i]=i;
for(int j=0;j<m;j++)
{
int a,b;
cin>>a>>b;
join(a,b);
}
for(int i=1;i<=n;i++)
{
if(pre[i]=i)
sum++;
}
cout<<sum-1<<endl;
}
return 0;
}
5. 例题分析
5.1 天梯赛 部落
(1)问题描述
Problem Description
在一个社区里,每个人都有自己的小圈子,还可能同时属于很多不同的朋友圈。我们认为朋友的朋友都算在一个部落里,于是要请你统计一下,在一个给定社区中,到底有多少个互不相交的部落?并且检查任意两个人是否属于同一个部落。Input
输入在第一行给出一个正整数N(≤104),是已知小圈子的个数。随后N行,每行按下列格式给出一个小圈子里的人:
KP[1]P[2]⋯P[K]
其中K是小圈子里的人数,P[i](i=1,⋯,K)是小圈子里每个人的编号。这里所有人的编号从1开始连续编号,最大编号不会超过104。
之后一行给出一个非负整数Q(≤104),是查询次数。随后Q行,每行给出一对被查询的人的编号。
Output
对每个查询输出"Y"或"N"
(2)题解代码
可以每组圈子都与第一个人join建立集合,对于如何查人数,只要统计最大的编号就可以。最大的编号数就代表人的个数。对于圈子个数,其实就类似于畅通工程里的求联通块个数,然后要查询所以需要跑一边find,然后判断pre是否相等即可。
#include <iostream>
#include<bits/stdc++.h>
using namespace std;
int pre[10000];
int ranked[10000];
int finded(int a)
{
int r=a;
while(pre[r]!=r)
r=pre[r];
int i=a,j;
while(i!=r)
{
j=pre[i];
pre[i]=r;
i=j;
}
return r;
}
void join(int x,int y)
{
int fx=finded(x);
int fy=finded(y);
if(fx!=fy)
{
if(ranked[fx]<ranked[fy])
pre[fx]=fy;
else
{
pre[fy]=fx;
if(ranked[fx]==ranked[fy])
ranked[fx]++;
}
}
}
int main()
{
int n;
cin>>n;
memset(pre,0,sizeof(pre));
memset(ranked,0,sizeof(ranked));
int sum=0;
for(int i=1; i<=10000; i++)
pre[i]=i;
while(n--)
{
int a,b,c;
cin>>a>>b;
if(b>sum)
sum=b;
a--;
while(a--)
{
cin>>c;
if(c>sum)
sum=c;
join(b,c);
}
}
int ans=0;
for(int i=1; i<=sum; i++)
{
finded(i);
if(pre[i]==i)
ans++;
}
cout<<sum<<" "<<ans<<endl;
int k;
cin>>k;
while(k--)
{
int t1,t2;
cin>>t1>>t2;
if(pre[t1]==pre[t2])
cout<<"Y"<<endl;
else
cout<<"N"<<endl;
}
return 0;
}
5.2 小鑫的城堡
(1)问题描述
Problem Description
从前有一个国王,他叫小鑫。有一天,他想建一座城堡,于是,设计师给他设计了好多简易图纸,主要是房间的连通的图纸。小鑫希望任意两个房间有且仅有一条路径可以相通。小鑫现在把设计图给你,让你帮忙判断设计图是否符合他的想法。比如下面的例子,第一个是符合条件的,但是,第二个不符合,因为从5到4有两条路径(5-3-4和5-6-4)。
Input
多组输入,每组第一行包含一个整数m(m < 100000),接下来m行,每行两个整数,表示了一条通道连接的两个房间的编号。房间的编号至少为1,且不超过100000。
Output
每组数据输出一行,如果该城堡符合小鑫的想法,那么输出"Yes",否则输出"No"。
(2)题解代码
首先题意就是给出的所有点是否形成一个两两之间只有一条路的联通整体,而且不能存在环,对于判断是否有一个联通块可以用并查集实现,对于两两之间是否只有一条道路,可以用点数m=k(边数)+1来判断。
#include <iostream>
#include<bits/stdc++.h>
using namespace std;
int pre[100000];
bool num[100000];
bool judge[100000];
int finded(int a)
{
int r=a;
while(r!=pre[r])
r=pre[r];
int i=a,j;
while(i!=r)
{
j=pre[i];
pre[i]=r;
i=j;
}
return r;
}
void join(int x,int y)
{
int fx=finded(x);
int fy=finded(y);
if(fx!=fy)
pre[fx]=fy;
}
int main()
{
int m;
while(cin>>m)
{
int pp=m;
memset(num,0,sizeof(num));
memset(judge,0,sizeof(judge));
for(int i=1;i<=100000;i++)
pre[i]=i;
int sum=0;
while(m--)
{
int a,b;
cin>>a>>b;
join(a,b);
if(a>sum)
sum=a;
if(b>sum)
sum=b;
judge[a]=1;
judge[b]=1;
}
int c=0,k=0;
for(int i=1;i<=sum;i++)
{
if(judge[i]==1)
k++;
if(judge[i]&&pre[i]==i)
c++;
}
if(c==1&&k==pp+1)//这里容易错,原来写的k==m+1,忘记了m--,好菜啊。//如果c等于1,则说明途中路全通,当路不全通时,c会大于1. //当然c=1;只是其中一个条件,因为当图中点与点之间都联通时, //假设其中有两个点之间有2条路可通,此时的c也等于1,但不满 //足"任意两个点有且仅有一条路径可以相通"这一条件,所以还需 //加上 k == m + 1 这一条件,(字母含义详见代码)
cout<<"Yes"<<endl;
else
cout<<"No"<<endl;
}
return 0;
}
//当然c=1;只是其中一个条件,因为当图中点与点之间都联通时, //假设其中有两个点之间有2条路可通,此时的c也等于1,但不满 //足"任意两个点有且仅有一条路径可以相通"这一条件,所以还需 //加上 k == m + 1 这一条件,(字母含义详见代码)
cout<<"Yes"<<endl;
else
cout<<"No"<<endl;
}
return 0;
}
5.3 POJ 2236 Wireless Network
(1)问题描述
有n台计算机,给出他们的坐标,只有距离小于等于d的才可以直接通信,若a与b可通信,b与c可通信,则a与c可通信,以此类推。O x表示维修x号计算机可以通信,S x y表示询问xy之间是否可以通信,是输出SUCCESS,否则输出FAIL。
(2)题解代码
循环所有计算机寻找建立通信(同一集合的+可以直接通信的),不循环的话会漏掉后面直接通信的!
#include <iostream>
#include<cstring>
#include<cmath>
#include<cstdio>
using namespace std;
bool judge[1005];
int pre[1005];
int ranked[1005];
int N,d;
struct Node
{
int x;int y;
};
Node node[1005];
int fined(int x)
{
int v=x;
while(pre[v]!=v)v=pre[v];
int i=x,j;
while(i!=v)
{
j=pre[i];
pre[i]=v;
i=j;
}
return v;
// return pre[x]==x?x:pre[x]=fined(pre[x]);//递归运行
}
void join(int a,int b)
{
int fx=fined(a);
int fy=fined(b);
if(fx!=fy)
{
if(ranked[fx]<ranked[fy])
{
pre[fx]=fy;
}
else
{
pre[fy]=fx;
if(ranked[fx]==ranked[fy])
ranked[fx]++;
}
}
}
int main()
{
scanf("%d%d",&N,&d);
memset(judge,0,sizeof(judge));
memset(ranked,0,sizeof(ranked));
for(int i=1;i<=N;i++)
pre[i]=i;
for(int i=1;i<=N;i++)
{
scanf("%d%d",&node[i].x,&node[i].y);//直接输入
// node[i].x=dx;
// node[i].y=dy;
}
char instruct[10];
getchar();
while(scanf("%s",instruct)!=EOF)
{
if(instruct[0]=='O')
{
int computer;
scanf("%d",&computer);
judge[computer]=1;
for(int i=1;i<=N;i++)//必须全都循环一边,防止后面能直接交流的没join上!只join上了前面同父亲的!
{
if(i!=computer&&judge[i]&&((node[i].x-node[computer].x)*(node[i].x-node[computer].x)+(node[i].y-node[computer].y)*(node[i].y-node[computer].y))<=d*d)
{
join(i,computer);
//break;
}
}
}
else if(instruct[0]=='S')
{
int s,e;
scanf("%d%d",&s,&e);
int fs=fined(s);
int fe=fined(e);
if(fs!=fe)
printf("FAIL\n");
else
printf("SUCCESS\n");
}
}
return 0;
}
二. 最小生成树
1. 基本概念
在连通网的所有生成树中,所有边的代价和最小的生成树,称为最小生成树。最小生成树其实是最小权重生成树的简称。在基本概念中包含两个条件,即使所有点联通且建立所有边的代价和最小。
最小生成树应用举个例子:要在n个城市之间铺设光缆,主要目标是要使这 n 个城市的任意两个之间都可以通信,但铺设光缆的费用很高,且各个城市之间铺设光缆的费用不同,因此另一个目标是要使铺设光缆的总费用最低。这就需要找到带权的最小生成树。
2. Prim 算法(加点法)
2.1 实现思路
以任意一点为树根出发,集合V是已经确定最短路的点集合,集合U是没有确立最短路的集合。初始时只有树根点在V中。每一次循环就代表要修建一条最短路,到达没到达的点(U),我们只能从已经建成的局部最短路点集V中选取V中所有已确定点能到达的所有其他点里面最小的来建设,有点贪心思想,每次选取代价最小的路,逐渐完善点,直到恰好覆盖所有的点。
2.2 实现代码
(1)核心代码
int G[1000][1000];//邻接矩阵存图
int dis[1000];//存储 集合V 里面所有点的可到到达其他点总的最小距离
bool judge[1000];//判断该点是否已经加入最小点集合
int pre[1000];//记录每个点的前导,用于输出路径
int n,m;
int prim(int a)
{
int sum=0;//记录路径总和
int pos;//记录下一个加入V中的点位置
int minn;
judge[a]=1;
pos=a;
for(int i=1; i<=n-1; i++)
{
minn=INF;
for(int j=1; j<=n; j++)
{
if(!judge[j]&&dis[j]<minn)//寻找集合V中能到达其他所有点的最短路径
{
pos=j;
minn=dis[j];
}
}
judge[pos]=1;//找到下一个加入V中的点
sum+=minn;//加上最小路径
cout<<"V"<<pre[pos]<<" -- "<<"V"<<pos<<" is "<<minn<<endl;
for(int j=1; j<=n; j++)//从新加入的点更新V的最小距离dis,便于下次寻找最小点
{
if(dis[j]>G[pos][j]&&!judge[j])
{
dis[j]=G[pos][j];
pre[j]=pos;//记录前导
}
}
}
return sum;
}
(2)完整代码
#include <iostream>
#include<bits/stdc++.h>
using namespace std;
#define INF 0x3f3f3f
int G[1000][1000];//邻接矩阵存图
int dis[1000];//存储最小距离(总的集合U里的)
bool judge[1000];//判断该点是否已经加入最小点集合
int pre[1000];//记录每个点的前导,用于输出路径
int n,m;
int prim(int a)
{
int sum=0;//记录总和
int pos;//记录位置
int minn;
judge[a]=1;
pos=a;
for(int i=1; i<=n-1; i++)
{
minn=INF;
for(int j=1; j<=n; j++)
{
if(!judge[j]&&dis[j]<minn)
{
pos=j;
minn=dis[j];
}
}
judge[pos]=1;
sum+=minn;
cout<<"V"<<pre[pos]<<" -- "<<"V"<<pos<<" is "<<minn<<endl;
for(int j=1; j<=n; j++)
{
if(dis[j]>G[pos][j]&&!judge[j])
{
dis[j]=G[pos][j];
pre[j]=pos;
}
}
}
return sum;
}
int main()
{
int T;
cin>>T;
while(T--)
{
cin>>n>>m;
for(int i=1; i<=n; i++)
{
for(int j=1; j<=n; j++)
{
if(i==j)
G[i][j]=0;
else
G[i][j]=INF;
}
}
memset(judge,0,sizeof(judge));
for(int i=0; i<m; i++)
{
int a,b,c;
cin>>a>>b>>c;
G[a][b]=G[b][a]=c;
}
int s;
cin>>s;
for(int i=1; i<=n; i++)
{
pre[i]=s;
dis[i]=G[s][i];
}
int k=prim(s);
cout<<k<<endl;
}
return 0;
}
(3)邻接表优化
#include <iostream>
#include<bits/stdc++.h>
using namespace std;
#define INF 0x3f3f3f
int dis[1000];//存储最小距离(总的集合U里的)
bool judge[1000];//判断该点是否已经加入最小点集合
int pre[1000];//记录每个点的前导,用于输出
struct Node//记录终点和路径长度
{
int e,v;
Node(int a,int b):e(a),v(b) {}
};
int n,m;
vector<Node> num[1000];
int prim(int a)
{
int sum=0;//记录总和
int pos;//记录位置
int minn;
judge[a]=1;
pos=a;
for(int i=1; i<=n-1; i++)
{
minn=INF;
for(int j=1; j<=n; j++)
{
if(!judge[j]&&dis[j]<minn)
{
pos=j;
minn=dis[j];
}
}
judge[pos]=1;
sum+=minn;
cout<<"V"<<pre[pos]<<" -- "<<"V"<<pos<<" is "<<minn<<endl;
for(int i=0;i<num[pos].size();i++)
{
Node d=num[pos][i];
if(dis[d.e]>d.v&&!judge[d.e])
{
dis[d.e]=d.v;
pre[d.e]=pos;
}
}
}
return sum;
}
int main()
{
int T;
cin>>T;
while(T--)
{
int s;
cin>>n>>m>>s;
for(int i=1;i<=n;i++)
{
dis[i]=INF;
num[i].clear();
pre[i]=s;
}
memset(judge,0,sizeof(judge));
for(int i=0; i<m; i++)
{
int a,b,c;
cin>>a>>b>>c;
num[a].push_back(Node(b,c));
num[b].push_back(Node(a,c));
}
for(int i=0;i<num[s].size();i++)
{
dis[num[s][i].e]=num[s][i].v;
}
int k=prim(s);
cout<<k<<endl;
}
return 0;
}
3. Kruskal 算法(加边法)
3.1 实现思路
最小生成树最后一定是只有n-1条边!所以我们只要选取最小的n-1条边来吧n个点联通起来即可,但是注意不能产生回路,于是我们就用到了并查集!
- 记Graph中有v个顶点,e条边;
- 新建图Graphnew,Graphnew中拥有原图中的v个顶点,但没有边;
- 将原图Graph中所有e条边按权值从小到大排序;
- 循环:从权值最小的边开始,判断并添加每条边,直至添加了n-1条边:
注意加边的条件是不产生回路,即要连接的两定点不在一个集合里面才行!所以使用并查集判断是否可以加边。
3.2 实现代码
(1)核心代码
struct Node//建立边(起点+终点+权值)
{
int s,e,v;
bool operator <(const Node &n)const{//排序规则,由小到大权值
return v<n.v;
}
};
int Kruskal()
{
sort(node,node+m);//排序权值
int sizen=0;//建立路径条数
int sum=0;//最小值
for(int i=0;i<m&&sizen!=n-1;i++)
{
if(finded(node[i].s)!=finded(node[i].e))//如果一条边的起点终点不在同一个集合,即可连接成为一条最短路,并且把这两个集合join为一个
{
join(node[i].s,node[i].e);//join
sum+=node[i].v;
sizen++;
}
}
if(sizen<n-1)return -1;//不足n-1
return sum;
}
(2)完整代码
#include <iostream>
#include<bits/stdc++.h>
using namespace std;
struct Node
{
int s,e,v;
bool operator <(const Node &n)const{
return v<n.v;
}
};
Node node[1000];
int pre[1000];
int ranked[1000];
int n,m;
int finded(int v)//查找
{
int i=v;
while(i!=pre[i])//return pre[v]=v?v:pre[v]=find(pre[v]);//递归
i=pre[i];
int j;
while(v!=i)
{
j=pre[v];
pre[v]=i;
v=j;
}
return i;
}
void join(int a,int b)//合并
{
int fx=finded(a);
int fy=finded(b);
if(fx!=fy)
{
if(ranked[fx]<ranked[fy])
{
pre[fx]=fy;
}
else
{
pre[fy]=fx;
if(ranked[fx]==ranked[fy])
ranked[fx]++;
}
}
}
int Kruskal()
{
sort(node,node+m);
int sizen=0;
int sum=0;
for(int i=0;i<m&&sizen!=n-1;i++)
{
if(finded(node[i].s)!=finded(node[i].e))
{
join(node[i].s,node[i].e);
sum+=node[i].v;
sizen++;
}
}
if(sizen<n-1)return -1;
return sum;
}
int main()
{
cin>>n>>m;
memset(ranked,0,sizeof(ranked));
for(int i=1;i<=n;i++)
{
pre[i]=i;
}
for(int i=0;i<m;i++)
{
cin>>node[i].s>>node[i].e>>node[i].v;
}
int k=Kruskal();
cout<<k<<endl;
return 0;
}