核心思想:通过“合并”和“查找”两种操作来动态维护集合元素之间的关系。
适用问题:具有 传递性 和 对称性 的二元关系的结点,这类问题可以抽象为不同集合之间的问题。
传递性:
对称性:
问题转化:
抽象成集合之间的问题之后,原问题就进行了转化:
判断元素之间是否有联系 → 判断两个元素是否属于同一集合
两个元素建立联系 → 将两个元素所属集合合并
实现方式:常用数组实现,其他方法亦可
基本实现:
集合初始化:创建一个数组用于存储该结点所在集合的代表结点(也可以说是根结点),所有该集合的结点都指向他。一开始默认都指向自己,各自是独立的集合。
//创建存储该结点所在集合的代表结点数组
int father[N];
int init(int n){
for(int i=1;i<=n;i++)
//一开始默认都指向自己,各自是独立的集合。
father[i] = i;
}
合并集合:将两个集合进行合并。将其中一个集合的代表结点指向另一个结合的代表结点。
(注意:由于两个集合里的结点都指向各自的代表结点,而当前进行关联的两个结点并不一定是各自的代表结点,如果直接将其中一个指向另一个,并不能将两个集合合并,只是该结点与另一集合合并,从而造成错误。
)
void unionn(int x,int y){
//将其中一个集合的代表结点指向另一个结合的代表结点。
father[find(x)] = find(y);
}
查找当前集合的代表结点:查找当前结点所处集合的代表结点。由于可能存在链状结构,即代表结点是当前结点的祖先结点,而非直接父结点,所以需要迭代找出代表结点。
//非递归形式
int find(int x){
while(father[x] != x) x = father[x];
return x;
}
//递归形式
int find(int x){
if (father[x] != x) return find(father[x]);
return x;
}
优化:路径压缩
如上边所说,有可能会形成链状结构,这样在查找某一集合的代表结点时会很耗时,所以可以进行优化,避免产生过长的链状结构。
基本思路:查找过程中顺带将沿途的全部结点一起指向代表结点。
int find(int x){
if (father[x] != x) father[x] = find(father[x]);
return father[x];
}
集合数量的求解:
基本思路:每个集合的代表结点是唯一的,即 代表结点数 = 集合数。而代表结点的特征是指向自己,所以只需要数一下指向自己的结点数即可。
int getSetCnt(int n){
int cnt = 0;
for(int i=1;i<=n;i++){
if(father[i] == i) cnt++;
}
return cnt;
}
各集合内结点个数求解:
基本思路:创建新的数组专门存储集合的结点个数。假设初始状态下,每个结点是单独的集合,值都为1。建立全部联系后,根据集合关系,将数值分别加入到各自集合的代表结点上。
int count1[N];
int init(int n){
for(int i=1;i<=n;i++){
//一开始默认都指向自己,各自是独立的集合。
father[i] = i;
//一开始各自是单独结合,所以集合个数为1。
count1[i] = 1;
}
void getNodeCntPerSet(int n){
for(int i=1;i<=n;i++){
//将数值分别加入到各自集合的代表结点上
if (father[i] != i) count1[find(i)] += count1[i], count1[i] = 0;
}
}
参考资料:
[1]《信息学奥赛一本通(c++版)》第五版,董永建,科学技术文献出版社,p496-507
[2]https://blog.csdn.net/u011575841/article/details/78992099
[3]https://blog.csdn.net/wangwei6125/article/details/68954171