并查集
并查集
并查集是一种树形结构,用于处理集合的基本操作
并查集的思想就是让每一个元素指向另一个元素,形成森林,而相同集合内的元素放在一起
加入fa[]数组,表示元素的指向,fa[x] = y表示\(x\)指向\(y\)
最开始,对于\(\forall x\),有fa[x] = x,这就是并查集的初始化
//查找根节点,根节点一定满足fa[root] = root
int find(int x) {
while (x != fa[x])
x = fa[x];
return x;
}
//合并两个节点
void merge(int x, int y) {
int rootx = find(x), rooty = find(y);
if (rootx != rooty)
swap(rootx, rooty);
fa[rootx] = fa[rooty];
}
这种写法的缺点十分明显,遇到树退化成链的情况,时间复杂度就变为\(O(n)\),十分的不方便
那么如果在查找的路上就将沿途的节点指向根节点,那么下一次查询就是\(O(1)\)
// 非递归写法,再定义一个temp数组
int find(int x) {
if (fa[x] == x)
return x;
int cnt=0;
while(x!=fa[x]){
temp[++cnt]=x; // 记录沿途节点
x=fa[x];
}
for(int i=1;i<=cnt;i++)
fa[temp[i]]=x;
return x;
}
这种写法时间虽然很优秀,但是还是有缺点,如果遇到题目要求不能修改合并顺序时,就无能为力
既然如此,那么不妨从merge()函数入手
注意到merge总是无差别的合并两个节点,那么如果一个节点所在集合的基很大,另一个很小,这种情况就会使find的复杂度上升
那么不妨记录下每个节点的个数,然后再比较合并
int size[MAXN];
void merge(int x, int y) {
int rootX = find(x);
int rootY = find(y);
if (rootX == rootY)
return;
if (size_[rootX] < size_[rootY])
swap(rootX, rootY);
size_[rootY] += size_[rootX];
fa[rootX] = rootY;
}
这个写法的时间复杂度平摊是\(O(\log_2 n)\)
这种写法的名字叫做启发式算法,挺有逼格的
既然可以记录节点数,那么也可以记录合并次数,这就叫按秩合并
也挺有逼格的
int rank[MAXN];
void merge(int x, int y){
int a = find(x), b = find(y);
if (a == b)
return;
if (rank[a] <= rank[b])
fa[a] = b;
else
fa[b] = a;
if (rank[a] == rank[b])
rank[b]++;
}
例题:
洛谷P1551P1551 亲戚
题目背景
若某个家族人员过于庞大,要判断两个是否是亲戚,确实还很不容易,现在给出某个亲戚关系图,求任意给出的两个人是否具有亲戚关系。
题目描述
规定:\(x\) 和 \(y\) 是亲戚,\(y\) 和 \(z\) 是亲戚,那么 \(x\) 和 \(z\) 也是亲戚。如果 \(x\),\(y\) 是亲戚,那么 \(x\) 的亲戚都是 \(y\) 的亲戚,\(y\) 的亲戚也都是 \(x\) 的亲戚。
输入格式
第一行:三个整数 \(n,m,p\),(\(n,m,p \le 5000\)),分别表示有 \(n\) 个人,\(m\) 个亲戚关系,询问 \(p\) 对亲戚关系。
以下 \(m\) 行:每行两个数 \(M_i\),\(M_j\),\(1 \le M_i,~M_j\le n\),表示 \(M_i\) 和 \(M_j\) 具有亲戚关系。
接下来 \(p\) 行:每行两个数 \(P_i,P_j\),询问 \(P_i\) 和 \(P_j\) 是否具有亲戚关系。
输出格式
\(p\) 行,每行一个 Yes 或 No。表示第 \(i\) 个询问的答案为“具有”或“不具有”亲戚关系。
输入输出样例 #1
输入 #1
6 5 3
1 2
1 5
3 4
5 2
1 3
1 4
2 3
5 6
输出 #1
Yes
Yes
No
板子题(我封装了),不要想抄代码,这个代码过不了
#include<bits/stdc++.h>
namespace Set {
const int Null = 0;
const int Maxn = 1e6 + 5;
template<typename T>
void swap(T& x, T& y) {
T temp = x;
x = y;
y = temp;
}
template <typename T1, typename T2>
struct pair {
T1 x;
T2 y;
};
template <typename T>
class set {
private:
int set_[Maxn], rank[Maxn];
int cnt = 0;
public:
void init(int n) {
cnt = n;
for (int i = 1; i <= n; i++)
set_[i] = i, rank[i] = 1;
}
int size() {
return cnt;
}
// ---------------------------------
int find(int x) {
if (x == set_[x])
return x;
return set_[x] = find(set_[x]);
}
// --------------------------------
int find_(int x) {
if (x == set_[x])
return x;
return find_(x);
}
void merge(int x, int y) {
int ano_x = find(x), ano_y = find(y);
if (ano_x != ano_y)
swap(ano_x, ano_y);
set_[ano_y] = set_[ano_x];
}
void merge_withRank(int x, int y) {
int rootx = find_(x), rooty = find_(y);
if (rootx == rooty)
return;
if (rank[rootx] <= rank[rooty])
set_[rootx] = rooty;
else
set_[rooty] = rootx;
if (rank[rootx] == rank[rooty])
rank[rooty]++;
}
};
}
using namespace std;
Set::set<int> s;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
int n, m;
cin >> n >> m;
s.init(m);
for (int i = 1; i <= m; i++){
int x, y;
cin >> x >> y;
s.merge(x, y);
}
int q;
cin >> q;
for (int i = 1; i <= q; i++){
int x, y;
cin >> x >> y;
if (s.find(x) == s.find(y))
cout << "Yes\n";
else
cout << "No\n";
}
return 0;
}
题目描述
现在有个岛屿和座桥。
第\(i\)座桥双向连接着第\(A_i\)和\(B_i\)个岛屿。
刚开始的时候,我们可以用其中的一些桥在任何两个岛之间旅行。
然而,经调查显示,这些桥都将会因老旧而倒塌,而且倒塌的顺序为从第\(1\)座桥到第\(M\)座桥。
由于剩下的桥不能够使第个岛和第个岛互相到达,这会使岛屿对变得很不方便。
对于每一个,找出在第\(i\)座桥倒塌之后,这样不方便的岛屿对有多少?
输入格式
第一行有两个正整数\(N\)和\(M\),分别表示岛屿的数量和桥的数量。
接下来有行,每一行有两个整数\(A_i\)和\(B_i\),分别表示第\(i\)座桥连接的两个岛屿的编号。
输出格式
输出有行,每一行输出一个整数,表示第座桥倒塌之后不方便到达的岛屿对的数量,结果可能会超出位的大小。
样例
样例输入1
4 5
1 2
3 4
1 3
2 3
1 4
样例输出1
0
0
4
5
6
样例输入2
6 5
2 3
1 2
5 6
3 4
4 5
样例输出2
8
9
12
14
15
样例输入3
2 1
1 2
样例输出3
1
数据范围与提示
所有的输入都是整数。
初始不方便到达的岛屿的数量为\(0\)

浙公网安备 33010602011771号