目录
并查集是一种树型的数据结构
初始化:
把每个点所在集合初始化为其自身。
通常来说,这个步骤在每次使用该数据结构时只需要执行一次,无论何种实现方式,时间复杂度均为O(N)。
查找:
查找元素所在的集合,即根节点。
合并:
将两个元素所在的集合合并为一个集合。
通常来说,合并之前,应先判断两个元素是否属于同一集合,这可用上面的“查找”操作实现。
例题
Description(题目描述)
若某个家族人员过于庞大,要判断两个是否是亲戚,确实不容易,给出某个亲戚关系图,求任意给出的两个人是否具有亲戚关系。
规定:x和y是亲戚,y和z是亲戚,那么x和z也是亲戚。如果x,y是亲戚,那么x的亲戚都是y的亲戚,y的亲戚也都是x的亲戚。
Input(输入)
第一行:三个整数n,m,p,(n< =5000,m< =5000,p< =5000)
分别表示有n个人,m个亲戚关系,询问p对亲戚关系。
以下m行:每行两个数m,n,表示m和N具有亲戚关系。
接下来p行:每行两个数p1,p2,询问p1和P2 是否具有亲戚关系。
Output(输出)
共P行,每行一个’Yes’或’No’。表示第
个询问的答案为“有”或“没有”亲戚关系。
分析问题实质
初步分析觉得本题是一个图论中判断两个点是否在同一个连通子图中的问题。对于题目中的样例,以人为点,关系为边,建立无向图:
比如判断3和4是否为亲戚时,我们检查3和4是否在同一个连通子图中,结果是在,于是他们是亲戚。又如7和10不在同一个连通子图中,所以他们不是亲戚。
用图的数据结构的最大问题是,我们无法存下多至(M=)2 000 000条边的图,后面关于算法时效等诸多问题就免谈了。
用图表示关系过于“奢侈”了。其实本题只是一个对分离集合(并查集)操作的问题。
例如样例:
9 7 1
2 4
5 7
1 3
8 9
1 2
5 6
2 3
1 9
我们可以给每个人建立一个集合,集合的元素值有他自己,表示最开始时他不知道任何人是它的亲戚。以后每次给出一个亲戚关系a, b,则a和他的亲戚与b和他的亲戚就互为亲戚了,将a所在集合与b所在集合合并。对于样例数据的操作全过程如下:
初始状态:{1} {2} {3} {4} {5} {6} {7} {8} {9}
输入关系 分离集合
(2,4) {2,4}{1} {3} {5} {6} {7} {8} {9}
(5,7) {2,4} {5,7} {1} {3} {6} {8} {9}
(1,3) {1,3} {2,4} {5,7}{6} {8} {9}
(8,9) {1,3} {2,4} {5,7} {8,9}{6}
(1,2) {1,2,3,4} {5,7} {8,9}{6}
(5,6) {1,2,3,4} {5,6,7} {8,9}
(2,3) {1,2,3,4} {5,6,7} {8,9}
判断亲戚关系
(1,9),因为1,9不在同一集合内,所以输出"NO"。
最后我们得到3个集合{1,2,3,4}、{5,6,7}、{8,9},于是判断两个人是否亲戚的问题就变成判断两个数是否在同一个集合中的问题。如此一来,需要的数据结构就没有图结构那样庞大了。
算法需要以下几个子过程:
(1) 开始时,为每个人建立一个集合FHM_ak_ioi(x);
(2) 得到一个关系a b,合并相应集合FHM_ak_noi(a,b);
(3) 此外我们还需要判断两个人是否在同一个集合中,这就涉及到如何标识集合的问题。我们可以在每个集合中选一个代表标识集合,因此我们需要一个子过程给出每个集合的代表元FHM_ak_csp(a)。于是判断两个人是否在同一个集合中,即两个人是否为亲戚,等价于判断FHM_ak_csp(a)=FHM_ak_csp(b)。
有了以上子过程的支持,我们就有如下算法。
PROBLEM-Relations(N, M, a1,…,aM, b1,…,bM, Q, c1,…,cQ, d1,…,dQ)
for i←1 to N
do FHM_ak_ioi(i)
for i←1 to M
do if FHM_ak_csp(ai) != FHM_ak_csp(bi)
then FHM_ak_noi(ai, bi)
for i←1 to Q
do if FHM_ak_csp(ci)=FHM_ak_csp(di)
then output “Yes”
else output “No”
解决问题的关键便为选择合适的数据结构实现并查集的操作,使算法的实现效率最高。
例题:
婴儿名字||并查集
每年,政府都会公布一万个最常见的婴儿名字和它们出现的频率,也就是同名婴儿的数量。有些名字有多种拼法,
例如,John 和 Jon 本质上是相同的名字,但被当成了两个名字公布出来。给定两个列表,一个是名字及对应的频率,
另一个是本质相同的名字对。设计一个算法打印出每个真实名字的实际频率。注意,如果 John 和 Jon 是相同的,
并且 Jon 和 Johnny 相同,则 John 与 Johnny 也相同,即它们有传递和对称性。
在结果列表中,选择字典序最小的名字作为真实名字。
unordered_map<string,int> fre;// unordered_map可以节约时间
unordered_map<string,string> parent;
public:
vector<string> trulyMostPopular(vector<string>& names, vector<string>& synonyms) {
if( names.empty() ) return vector<string> ( );
//初始化
for(string s:names){
int pos=s.find('(');
string name=s.substr(0,pos);
int ifre=stoi(s.substr(pos+1,s.size()-pos-2));
parent[name]=name;
fre[name]=ifre;
}
//合并
for(string s:synonyms){
int pos=s.find(',');
string s1=s.substr(1,pos-1);
string s2=s.substr(pos+1,s.size()-pos-2);
if(fre.find(s1)==fre.end()||fre.find(s2)==fre.end())//如果有一个名字并没有被使用则不统计
continue;
m_union(s1,s2);
}
//保存结果
vector<string> res;
for(auto& name: parent){ //&可以减少时间
if(name.first == name.second){
string ifre=to_string(fre[name.first]);
res.push_back(name.first+"("+ifre+")");
}
}
return res;
}
private:
void m_union(string &s1,string &s2){//并操作
string n1=findparent(s1);//查询
string n2=findparent(s2);
if(n1!=n2){//如果不是亲戚,就要凑成亲戚
if(n1<n2){
fre[n1]+=fre[n2];
parent[n2]=n1;
}
else{
fre[n2]+=fre[n1];
parent[n1]=n2;
}
}
}
string findparent(string name){//找到他的祖先(字典序最小的一个),这个的理解要结合并操作,因为并操作就是把字典序较小的作为祖先
while(parent[name]!=name){//while迭代寻找,最初的祖先 (a,b,parent=a) a->a ; b->a ; (a,c,parent=c) a->c; c->c ; 这是访问b就要b->a , a->c;
name=parent[name];
}
return name;
}