并查集模板

并查集模板

一拖再拖,总结一下吧 : )

并查集常常用于处理一些合并和查询的问题,其中合并(union)和查询(find)是其最基本的两种操作,并查集、算法主要有两种,一种是quick find,另一种quick union,顾名思义,就是分别跟倾向于其中一种操作。从一个题目入手,简要说明一下这个算法,并且给出解题的通用模板!

1. 解题模板

1.1 例题

题目链接:洛谷 P1551 亲戚

题目背景

若某个家族人员过于庞大,要判断两个是否是亲戚,确实还很不容易,现在给出某个亲戚关系图,求任意给出的两个人是否具有亲戚关系。

题目描述

规定:x和y是亲戚,y和z是亲戚,那么x和z也是亲戚。如果x,y是亲戚,那么x的亲戚都是y的亲戚,y的亲戚也都是x的亲戚。

输入格式

第一行:三个整数n,m,p,(n<=5000,m<=5000,p<=5000),分别表示有n个人,m个亲戚关系,询问p对亲戚关系。

以下m行:每行两个数Mi,Mj,1<=Mi,Mj<=N,表示Mi和Mj具有亲戚关系。

接下来p行:每行两个数Pi,Pj,询问Pi和Pj是否具有亲戚关系。

输出格式

P行,每行一个’Yes’或’No’。表示第i个询问的答案为“具有”或“不具有”亲戚关系。

(具体测试样例可以去洛谷网站上去看看~)


1.2 解题思路

其实解题思路非常简单,就是将n个人划分为不同的集合,集合内的人就都是互相为亲戚关系了,整个解题的过程也就涉及到两个基本的操作,合并(输入一对对亲戚关系时合并)和查找(判断两人是否有亲戚关系时)

解决该问题可以使用如下的通用接口

ByhBAP.png

下面分别介绍两种解法~

1.2.1 quick-find

quick-find的思路非常简单,我们建立一个长度为 n 的数组 id[n],我们先将每一个 id[i] 初始化为 i, 然后当我们合并结点时,我们就将他们的 id[i] 置为相等,如下(分为三个集合 : {0, 5, 6}, {1, 2, 7}, {3, 4, 8, 9})

By4AHI.png

  • 这样当我们进行查找操作时(即判断两个结点是否连接),只需要比较两个结点对应的 id 值即可(比如判断 i, j 两个结点是否连接,就是判断 id[i] 和 id[j] 是否相等即可),时间复杂度为 O ( 1 ) O(1) O(1),非常高效。

  • 但是当我们进行合并操作时(合并两个集合),我们需要将其中一个集合的 id 值全部修改为另一个集合的 id 值,并且查找一个集合的所有元素只能通过遍历 id 数组来实现(可以参考下图栗子),所以合并操作的时间复杂度为 O ( n ) O(n) O(n) ,比较慢

By5JLd.png


有了上述思路,我们就可以按照通用接口写出quick-find算法了~

#include<iostream>
using namespace std;
#define N 1000
int id[N], n = 0;
void init() {	// 初始化id值
	for (int i = 1; i <= n; i++)
		id[i] = i;
}
void unionNode(int p, int q) {	// 合并节点
	int temp = id[p];
	if(temp != id[q])
		for (int i = 1; i <= n; i++) {
			if (id[i] == temp)
				id[i] = id[q];
		}
}
int find(int i) {	// 查找节点集合
	return id[i];
}
bool connected(int p, int q) {	// 判断节点是否连接
	return find(p) == find(q);
}
int main()
{
	int m, p, x, y;
	cin >> n >> m >> p;
	init();
	for (int i = 0; i < m; i++) {
		cin >> x >> y;
		unionNode(x, y);
	}
	for (int i = 0; i < p; i++) {
		cin >> x >> y;
		if (connected(x, y))
			cout << "Yes" << endl;
		else
			cout << "No" << endl;
	}
	return 0;
}

1.2.2 quick-union

为了降低合并(union)的时间复杂度,quick-union算法不是直接使用一个数组作为标记一个集合的tag,而是使用来表示一个集合(当然树可能就是用数组实现的),我们将同一个集合中的元素合并在同一颗树下。

  • 当我们进行查找操作时,我们就只需要判断两个节点其对应的两个根节点是不是同一个(根节点是同一个就说明是同一颗树下,也就是是同一个集合),由于树的特性,这个过程的时间复杂度为 l o g ( n ) log(n) log(n)
  • 当我们进行合并操作时,我们只需要将一棵树链接到另一颗树上即可(作为子树),但是合并之前我们一般先查找根结点,如果根节点不同,再将一个根节点作为子节点链接上,所以时间复杂度也为 l o g ( n ) log(n) log(n)

关于优化

优化一:按秩合并

为了减少查找时的时间开销,我们希望减少我们树的高度,所以我们在合并的时候希望将深度更小的树作为深度更大的树的子节点,这样就能有效的降低树的高度。所以我们需要记录每棵树的深度,记作depth,如下图所示:

B6CwtK.png

优化二:路径压缩

另外我们在查找根节点的时候也可以缩短树的高度,有两种方式:① 找到某个节点的根节点后,直接将该节点链接到根节点上,作为根的直接子节点,降低树的高度;② 查找某个节点的根节点时,是一个不断的向上寻找父亲节点的过程,这个过程中可以不断的将一个节点的父亲节点修改为爷爷节点(即父亲的父亲),这样也能有效的降低高度。


可能上述两个优化有点懵,其实非常好懂,可以参考下面的代码,很简单直接~

#include<iostream>
using namespace std;
#define N 1000
int parent[N], depth[N];
void init(int n) {
	for (int i = 0; i <= n; i++) {
		parent[i] = i;	// 初始定义各个结点都为根节点
		depth[i] = 1;	// 初始定义树的深度都为1(即树结点数量)
	}
}
// **********优化点二**********
// 不优化的find
//int find(int i) {
//	while (i != parent[i]) {
//		i = parent[i];	// 向上找父结点
//	}
//	return i;
//}

// 使用方式一优化,将查找节点直接链接到根节点上
//int find(int i) {
//	if (i != parent[i]) {
//		parent[i] = find(parent[i]);	// 递归的一个过程
//	}
//	return parent[i];
//}
// 使用方式二优化  不断将父亲节点修改为爷爷节点
int find(int i) {
	while (i != parent[i]) {
		parent[i] = parent[parent[i]];
		i = parent[i];	// 向上找父结点
	}
	return i;
}
bool connected(int p, int q) {
	p = find(p);
	q = find(q);
	return p == q;
}
void unionNode(int p, int q) {
	p = find(p);
	q = find(q);
	// **********优化点一**********
	if (p == q)	// 根节点相同
		return;
	else if (depth[p] > depth[q]) {
		parent[q] = p;	// p的深度更大,将q作为子树
	}
	else if(depth[p] < depth[q]){
		parent[p] = q;	// q的深度更大,将p作为子树
	}
	else {
		parent[q] = p;	// 两棵树的深度相同,随意合并 ,但是此时需要更新树的深度
		depth[p]++;
	}
}
int main() {
	int n, m, p, temp1, temp2;
	cin >> n >> m >> p;
	init(n);
	for (int i = 0; i < m; i++) {
		cin >> temp1 >> temp2;
		unionNode(temp1, temp2);
	}
	for (int i = 0; i < p; i++) {
		cin >> temp1 >> temp2;
		if(connected(temp1, temp2))
			cout << "Yes" << endl;
		else
			cout << "No" << endl;
	}
	return 0;
}
<think>嗯,用户现在问的是BFS、DFS和查并集(也就是并查集)。我需要先回顾一下这三个概念的定义、用途和区别。用户之前已经问过图论的应用,现在可能是在深入学习相关算法,或者是为面试或考试做准备。 首先,BFS(广度优先搜索)和DFS(深度优先搜索)是图遍历的基本算法,而并查集(Disjoint Set Union, DSU)用于处理集合的合并与查询问题。用户可能想了解这三者的基本概念、应用场景以及它们之间的不同。 接下来要考虑用户的需求层次。他们可能想知道每个算法的基本原理、代码实现、时间复杂度,以及实际应用例子。或者,他们可能需要比较这三者的优缺点,了解在什么情况下使用哪种算法更合适。 需要确保回答结构清晰,分点解释每个算法,然后比较它们。可能还需要举一些实际的例子,比如BFS用于最短路径,DFS用于拓扑排序,并查集用于连通性问题。同时,要注意用户之前的问题是关于图论的应用,所以这里需要保持连贯性,说明这些算法如何应用于实际问题。 另外,用户可能混淆了“查并集”和“并查集”,正确的术语是并查集,所以回答中需要明确指出,避免混淆。同时,要注意用中文术语,比如“节点”而不是“结点”,保持一致性。 要检查是否有常见的误区需要澄清,比如BFS和DFS的时间复杂度在不同数据结构(邻接矩阵 vs 邻接表)下的差异,或者并查集的路径压缩和按秩合并优化。 最后,总结部分需要简明扼要,强调三者的不同应用场景和选择依据,帮助用户根据具体问题选择合适的算法。</think>--- ### **BFS(广度优先搜索)** #### **核心思想** - **逐层扩散**:从起点出发,按层次遍历所有可达节点(先访问距离起点最近的节点)。 - **数据结构**:队列(FIFO)。 - **时间复杂度**:$O(V + E)$($V$为节点数,$E$为边数)。 #### **典型应用** 1. **无权图的最短路径**(如迷宫逃生、社交网络中的最少中间人); 2. **状态空间搜索**(如八数码问题、单词接龙); 3. **网络爬虫**(按层级抓取网页,避免重复访问)。 #### **代码模板(Python)** ```python from collections import deque def bfs(start, graph): visited = set() queue = deque([start]) visited.add(start) while queue: node = queue.popleft() for neighbor in graph[node]: if neighbor not in visited: visited.add(neighbor) queue.append(neighbor) ``` --- ### **DFS(深度优先搜索)** #### **核心思想** - **递归深入**:从起点出发,沿一条路径走到尽头,再回溯探索其他分支。 - **数据结构**:栈(LIFO)或递归调用栈。 - **时间复杂度**:$O(V + E)$。 #### **典型应用** 1. **拓扑排序**(如课程选修顺序、任务依赖关系); 2. **连通性问题**(如岛屿数量、图的连通分量); 3. **回溯法**(如数独求解、全排列生成)。 #### **代码模板(Python)** ```python def dfs(node, graph, visited): if node in visited: return visited.add(node) for neighbor in graph[node]: dfs(neighbor, graph, visited) ``` --- ### **并查集(Disjoint Set Union, DSU)** #### **核心思想** - **动态连通性管理**:通过树结构合并集合,并快速判断两个元素是否属于同一集合。 - **优化手段**:路径压缩(降低树高) + 按秩合并(平衡树结构)。 - **时间复杂度**:接近$O(1)$(经过优化后)。 #### **典型应用** 1. **动态连通性问题**(如社交网络好友关系合并、电路连通性检测); 2. **最小生成树(Kruskal算法)**:判断边是否会形成环; 3. **图像像素连通区域标记**。 #### **代码模板(Python)** ```python class DSU: def __init__(self, size): self.parent = list(range(size)) self.rank = [1] * size def find(self, x): if self.parent[x] != x: self.parent[x] = self.find(self.parent[x]) # 路径压缩 return self.parent[x] def union(self, x, y): x_root = self.find(x) y_root = self.find(y) if x_root == y_root: return # 按秩合并 if self.rank[x_root] < self.rank[y_root]: self.parent[x_root] = y_root else: self.parent[y_root] = x_root if self.rank[x_root] == self.rank[y_root]: self.rank[x_root] += 1 ``` --- ### **三者对比与选择原则** | **算法** | **核心目标** | **适用场景** | **优势** | **局限性** | |----------|---------------------------|----------------------------------|----------------------------|-------------------------| | BFS | 最短路径(无权图) | 层级遍历、状态扩散 | 保证最优解 | 空间复杂度高(存储队列) | | DFS | 路径存在性、连通性 | 回溯问题、拓扑排序 | 空间复杂度低(仅递归栈) | 可能陷入深层无效路径 | | 并查集 | 动态集合合并与查询 | 连通性判断、集合动态合并 | 近乎常数时间操作 | 无法回溯合并操作 | --- ### **实际场景示例** 1. **社交网络好友推荐** - **BFS**:计算用户A的N度人脉(二度好友优先推荐); - **并查集**:判断用户A和用户B是否属于同一社群(如“校友群”)。 2. **游戏地图生成** - **DFS**:生成随机迷宫(确保起点到终点有唯一通路); - **BFS**:AI自动寻路(寻找最短逃生路径)。 3. **电路设计验证** - **并查集**:检查所有元件是否连通(避免断路)。 --- ### **总结** - **BFS**:**最短路径**问题优先选择,需注意空间消耗; - **DFS**:适合**探索所有可能性**或**连通性检测**,注意递归深度限制; - **并查集**:**动态集合操作**的终极工具,高效解决连通性问题。 **选择策略**: - 需要最短路径 → BFS - 需要遍历所有可能或递归结构 → DFS - 动态合并与查询 → 并查集
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值