小引
并查集是求解等价关系的得力助手,具体应用如求无向图连通分支数,至少还需几条路才能将一个城市串通,迷宫生成,克鲁斯卡尔算法求解最小生成树。它的听起来高大上,实际上却是极为简单的数据结构–森林
并查集顾名思义是并、查集合的操作的实现关键在于一个数组father,该数组下标表示相应的点,值表示该点对应的双亲,初始化全为-1,表示每一个点都构成仅有一个结点且该结点为根的树,每次得到两个点的关系,利用查找函数找到两个点的祖先,并将他们合并构建为一颗二叉树,以此反复,直到所有关系均处理完
最终father状态含义:
- 负数的个数表示连通分支个数(集合个数)
- 负数的绝对值表示相应连通分支的节点个数(相应集合的大小)
朋友圈求解
[问题描述]
某学校有N个学生,形成M个俱乐部。每个俱乐部里的学生有着相似的兴趣爱好,形成一个朋友圈。一个学生可以同时属于若干个不同的俱乐部。根据“我的朋友的朋友也是我的朋友”这个推论可以得出,如果A和B是朋友,且B和C是朋友,则A和C也是朋友。请编写程序计算最大朋友圈中有多少人。
[基本要求]
(1)输入说明:输入的第一行包含两个正整数N (N<=30 000)和M (M<=1000),分别代表学校的学生总数和俱乐部的个数。随后的M行每行按以下格式给出一个俱乐部的信息,其中学生从1-N编号:
第i个俱乐部的人数Mi(空格)学生1(空格)学生2… 学生Mi
(2)输出说明:输出一个整数,表示在最大朋友圈中有多少人。
(3)测试用例:
输入 7 4
3 1 2 3
2 1 4
3 5 6 7
1 6
输出 4
数据结构
father数组最终状态:负数的个数表示朋友圈个数;数组值的绝对值表示该朋友圈人数
int father[30005];//下标表示人的标号,值表示双亲节点
查找
- 查找并返回当前点所在树的根节点
- 优化:压缩路径法,即将一棵树中所有的点均直接指向根,可以提高查找效率
//查找当前点所在树的根节点
int find(int child)
{
int f = child;
while(father[f] > 0){
f = father[f];
}
//优化:压缩路径,即将一棵树中所有的点均直接指向根,可以提高查找效率
int j = child;
while(j != f){
int t = father[j]; // 保存下一个父节点
father[j] = f;
j = father[t];
}
return f;
}
合并
将两个根合并,等价于将两棵树合并,但二叉树只能有一个根,所以必须抉择,本来选择谁都是可行的,但为了避免树退化,高度过大,每次选择高度小的根接入高度大的根
//合并两个点
void Union(int fa,int fb)
{
//优化:为了避免树的退化,每次将高度小的根接到高度大的根
if(father[fa] < father[fb]){ // 负数!!!
father[fa] += father[fb];//个数相加
father[fb] = fa;//fb为fa父亲
}
else{
father[fb] += father[fa];
father[fa] = fb;
}
}
核心处理
从文件读取数据
文件朋友圈.txt内容
7 4
3 1 2 3
2 1 4
3 5 6 7
1 6
依次处理每一条边即可
//计算
void Cal()
{
//文件读取数据
fstream inFile("朋友圈.txt",ios::in);
if(!inFile)cout<<"fail to open file!"<<endl;
int n,m;//总人数,社团个数
inFile>>n>>m;//cout<<"n:"<<n<<" m:"<<m<<endl;
for(int i = 1; i <= n; i++){//全初始化为-1
father[i] = -1;
}
int sum,a,b,fa,fb;
for(int i = 0; i < m; i++){//处理m条信息
inFile>>sum>>a;
if(sum != 1){
for(int j = 0; j < sum-1; j++){
inFile>>b;
fa = Find(a);//一次合并
fb = Find(b);
Union(fa,fb);
}
}
}
inFile.close();
int min = 999999;//由于初始值为-1,叠加后为负数,所以寻找最小值
for(int i = 0; i < n; i++){
if(father[i] < 0){
if(min > father[i]){
min = father[i];
}
}
}
cout<<-min<<" ";
}
完整Code
#include<iostream>
using namespace std;
#include<fstream>
//并查集应用
//可用于求关联集合个数及其总个数
//father数组最终状态:负数的个数表示朋友圈个数;数组值的绝对值表示该朋友圈人数
int father[30005];//下标表示人的标号,值表示双亲节点
//查找当前点的祖先,根节点
int Find(int child)
{
int f = child;
while(father[f] > 0){
f = father[f];
}
//压缩路径,可有可无,不过可以提高效率
int j = child;
while(j != f){
int t = father[j]; // 保存下一个父节点
father[j] = f;
j = father[t];
}
return f;
}
//合并两个点
void Union(int fa,int fb)
{
//优化:为了避免树的退化,每次将高度小的根接到高度大的根
if(father[fa] < father[fb]){
father[fa] += father[fb];//个数相加
father[fb] = fa;//fb为fa父亲
}
else{
father[fb] += father[fa];
father[fa] = fb;
}
}
//计算
void Cal()
{
//文件读取数据
fstream inFile("朋友圈.txt",ios::in);
if(!inFile)cout<<"fail to open file!"<<endl;
int n,m;//总人数,社团个数
inFile>>n>>m;//cout<<"n:"<<n<<" m:"<<m<<endl;
for(int i = 1; i <= n; i++){//全初始化为-1
father[i] = -1;
}
int sum,a,b,fa,fb;
for(int i = 0; i < m; i++){//处理m条信息
inFile>>sum>>a;
if(sum != 1){
for(int j = 0; j < sum-1; j++){
inFile>>b;
fa = Find(a);//一次合并
fb = Find(b);
Union(fa,fb);
}
}
}
inFile.close();
int min = 999999;//由于初始值为-1,叠加后为负数,所以寻找最小值
for(int i = 0; i < n; i++){
if(father[i] < 0){
if(min > father[i]){
min = father[i];
}
}
}
cout<<-min<<" ";
}
int main()
{
Cal();
return 0;
}
收获
- 不得不佩服前人的精妙思维,如此简单的结构却如此强大,果真大道至简
- 必须锻炼自己明白算法思想就写出程序的能力
- 学习算法时得明白他的来龙去脉,如何被创造而成,而不是仅仅学会如何使用
- 同时也该多想想算法思想之间的关联性,异同之处,才可能融汇贯通。如哈夫曼树利用数组思想,和堆排序类似;二叉排序树先查找,后插入,与并查集先查找,再确定是否合并有些类似