例题引入:有 \(n\) 个点,\(m\) 条无向边连接,求联通块数
假设我不会 DFS
1.暴力 \(n^2\)
给每个点加一个点权(或可称之为颜色)\(w[i]\),
初始时 \(w[i] = i\)。
若连接两个点权相等的点,不管。
若连接两个点权不相等的点 \(i\) 、 \(j\) ,那么遍历 \(n\) 个点,将所有点权为 \(w[j]\) 的点改为 \(w[i]\)。
最后数一下有多少个不同的点权即可。
其实可以理解为每次连边都暴力地把即将连在一起的连通块染成一色。
考虑优化连边
2.还是暴力 \(n^2\)
将每个点看作是一个人,每个连通块看作是一个集体。
每个集体里都有一个牢大老大。
每个人都有一个直接上司,而老大的上司是自己
连边——即合并两个联通块时,可以看作时一个集体的老大认另一个集体的老大当直接上司。
此时两个集体便合并成了一个。
具体操作:
- 设第i个人的直接上司是 \(id[i]\)
- 初始时 \(\forall i\le n\) , \(id[i]=i\)
- 合并时,譬如说要合并编号为u,v的两个人所在的集体
- 先一步一步递归地找到 \(id[id[...[u]...]]=fu\), 直到使得 \(fu\) 的上司是它自己,此时 \(fu\) 就是 \(u\) 的集体的老大。同理找到 \(fv\) 为 \(v\) 的集体的老大
- 执行 \(id[fv] = fu\)
- 合并完成
- 最后统计不同的老大数,即集体的数量,即联通块的数量。
来看一下第四步找老大怎么写
int find(int x){
if(id[x]!=x) return find(id[x]);
else return x;
}
3.路径压缩 \(\Theta(n\log n)\)
主要是第四步递归中 \(id[id[id[...[u]...]]]\) 太慢了,优化它。
考虑让 \(u\),\(u\) 的上司,\(u\) 的上司的上司……直接认老大为直接上司,下次再查询 \(u\) 或其上司和下属时,时间直接下降很多。
int finds(int x){
if(id[x]!=x) id[x]=finds(id[x]);
return id[x];
}
复杂度是\(\Theta(n\log n)\) ,我不会证
4.按秩合并 \(n\alpha(n)\)
\(\alpha(n)\) 是反阿克曼函数,即若 \(A(m,m)=n\),则 \(\alpha(n)=m\)。 \(A\) 是阿克曼函数。
这个大小其实已经可以近似为 \(O(1)\)
可以发现每个集体都可以看作是一棵树,树根是老大
记录每棵树的深度,每次让深度小的树的老大认深度大的树的老大当直接上司。这样合并下来的树深度会相对较小
实际上这个优化没什么用,毕竟多带个log问题也不大……
然后最基本的并查集就说完了
5.带权并查集
过了“银河英雄传说”就差不多了……没什么好说的…
主要思想就是要维护每个点和其树上的父节点(也就是上司)的一些关系,注重考虑如何合并即可
6.扩展域并查集
这个好玩。
例题引入:有 \(n\) 人,有一些敌对关系和友好关系,满足敌人的敌人是朋友,朋友的朋友是朋友,敌人的朋友是敌人,朋友的敌人是敌人,保证数据不会自相矛盾,求一共有多少个“朋友圈”,满足同一朋友圈内互相是朋友
让每个人都有双重人格,即有一个“我(编号为 \(i\) )”和一个“反我(编号为 \(i+n\) )”
你和我是敌人,那你和“反我”就是朋友,我和“反你”就是朋友
你和我是朋友,那“反你”和“反我”也是朋友
于是敌对关系就可以转化为和第二人格的朋友关系了,
此时敌人的敌人(正--反--正),又回到“正我”,就是朋友
现在就可以用正常的并查集维护了
不但每个人可以有双重人格,还可以有多重人格
这种情况参见“食物链”一题
最后要注意,统计“朋友圈”个数时,不要把仅含二重人格的朋友圈也算在总数内!
浙公网安备 33010602011771号