|
|
#include <bits/stdc++.h>
|
|
|
|
|
|
// https://www.cnblogs.com/yym2013/p/3845448.html
|
|
|
// https://www.cnblogs.com/zhxmdefj/p/11117791.html#4000267478
|
|
|
|
|
|
|
|
|
// 董晓老师讲解 按秩合并并查集
|
|
|
// https://www.bilibili.com/video/av838604930
|
|
|
|
|
|
/**
|
|
|
按秩合并的并查集
|
|
|
Q:为什么要将数量小的向数量大的连边,而不是倒过来?
|
|
|
A:1.通过比较并查集的大小来连边,将小的(u)向大的(v)连边,这样对于大的并查集,查询代价不变,而小的并查集查询代价每个点增加了1,
|
|
|
相当于增加了siz[u],(反过来则变成了siz[v]),siz[u]<siz[v]所以这样更优。
|
|
|
2.相较于路径压缩的并查集,他可以保留原始的信息。
|
|
|
*/
|
|
|
|
|
|
/**
|
|
|
// 上面是一采用递归的方式压缩路径, 但是,递归压缩路径可能会造成溢出栈,我曾经因为这个RE了n次,
|
|
|
下面我们说一下非递归方式进行的路径压缩:
|
|
|
int find(int x){
|
|
|
int k, j, r;
|
|
|
r = x;
|
|
|
while(r != parent[r]) //查找跟节点
|
|
|
r = parent[r]; //找到跟节点,用r记录下
|
|
|
k = x;
|
|
|
while(parent[k] != r) //非递归路径压缩操作
|
|
|
{
|
|
|
j = parent[k]; //用j暂存parent[k]的父节点
|
|
|
parent[k] = r; //parent[x]指向跟节点
|
|
|
k = j; //k移到父节点
|
|
|
}
|
|
|
return r; //返回根节点的值
|
|
|
}
|
|
|
*/
|
|
|
// https://www.cnblogs.com/ARTlover/p/9752355.html
|
|
|
|
|
|
using namespace std;
|
|
|
|
|
|
const int N = 30001;
|
|
|
int s[N]; //每个集合人数
|
|
|
int fa[N]; //并查集数组
|
|
|
int rnk[N]; //树高,不能使用rank变量名,与std中的已定义概念冲突
|
|
|
/**
|
|
|
测试数据
|
|
|
100 4
|
|
|
2 1 2
|
|
|
5 10 13 11 12 14
|
|
|
2 0 1
|
|
|
2 99 2
|
|
|
200 2
|
|
|
1 5
|
|
|
5 1 2 3 4 5
|
|
|
1 0
|
|
|
0 0
|
|
|
|
|
|
答案:
|
|
|
4
|
|
|
1
|
|
|
1
|
|
|
*/
|
|
|
|
|
|
//查询+带路径压缩
|
|
|
int find(int x) {
|
|
|
if (x == fa[x]) return x;
|
|
|
else return fa[x] = find(fa[x]); //带路径压缩
|
|
|
}
|
|
|
|
|
|
//合并并查集
|
|
|
void join(int x, int y) {
|
|
|
//找出双方的根
|
|
|
int xRoot = find(x);
|
|
|
int yRoot = find(y);
|
|
|
//同根则结束
|
|
|
if (xRoot == yRoot) return;
|
|
|
|
|
|
//让rank比较高的作为父结点
|
|
|
if (rnk[xRoot] > rnk[yRoot]) {
|
|
|
fa[yRoot] = xRoot;
|
|
|
s[xRoot] += s[yRoot];//人数也需要合并进去
|
|
|
} else {//小于等于都进这里~
|
|
|
fa[xRoot] = yRoot;
|
|
|
if (rnk[xRoot] == rnk[yRoot]) rnk[yRoot]++; //树的高度是在这里修改的,是什么意思?看董晓老师的视频,讲解的很清楚
|
|
|
s[yRoot] += s[xRoot];//人数也需要合并进去
|
|
|
}
|
|
|
}
|
|
|
|
|
|
int n, m, k, x, y;
|
|
|
|
|
|
int main() {
|
|
|
while (cin >> n >> m) {
|
|
|
//输入为 0 0 结束
|
|
|
if (n == 0 && m == 0) return 0;
|
|
|
//初始化
|
|
|
for (int i = 0; i < n; i++) { //因为有0号学生,所以数组遍历从0开始
|
|
|
fa[i] = i; //每个学生自己是自己的祖先
|
|
|
s[i] = 1; //集合中总人数,初始值是1
|
|
|
rnk[i] = 0; //树的高度为0
|
|
|
}
|
|
|
for (int i = 0; i < m; i++) {
|
|
|
cin >> k >> x;
|
|
|
k--;
|
|
|
while (k--) {
|
|
|
cin >> y;
|
|
|
join(x, y);
|
|
|
}
|
|
|
}
|
|
|
cout << s[find(0)] << endl; //寻找0号学生相关集合的总人数
|
|
|
}
|
|
|
return 0;
|
|
|
} |