并查集

并查集

并查集是一种树形结构,用于处理集合的基本操作

并查集的思想就是让每一个元素指向另一个元素,形成森林,而相同集合内的元素放在一起

加入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\) 行,每行一个 YesNo。表示第 \(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\)

posted @ 2025-07-21 20:04  Yangyihao  阅读(6)  评论(0)    收藏  举报