并查集

Mr.Hdd数据结构并查集大约 10 分钟...

「并查集 Union Find」

并查集是一种可以动态维护若干个不重叠的集合,并支持查询与合并的数据结构.

  • 查询操作:查询某个元素属于哪个集合
  • 合并操作:将两个集合合并为一个集合

并查集中,每个集合以 “根结点” 作为代表,所以并查集的本质就是一个森林 (由若干个树 (集合) 组成).

路径压缩

如果将并查集的每个集合以 “树” 的形式进行存储,这棵树在极端形况下可以会退化成一个链表,此时如果若要执行查询操作,时间复杂度为 O(n)O(n) ,这并不理想,在并查集中并不关心树的形状,其关键在于集合的代表:【根结点】,也即下面两个树是等价的.

image-20230104151310041

【路径压缩】在每次执行查询操作的同时,将访问过的每个结点 (也就是所查询元素的全部祖先) 都直接指向根结点。此时平均时间复杂度会降到 O(log n)O(log \ n)

struct UnionFind {
	id: Vec<usize>,      // 集合代表 (根结点)  
	count: usize,        // 不相交集合个数
}

impl UnionFind {
	/// 初始化并查集
	pub fn new(n: usize) -> Self {
		let mut id = vec![0;n];
		for i in 0..n {
			id[i] = i;
		}
		Self {id ,  count: n}
	}

	/// 查找并查集所在集合
	pub fn find(&mut self, x: usize) -> usize {
		let mut x = x;
		while x != self.id[x] {
			x = self.id[x];                
			self.id[x] = self.id[self.id[x]];    // 路径压缩
		}
		x
	}

	/// 合并 x 所在的集合与 y 所在的集合
	pub fn merge(&mut self, x: usize, y: usize) {
		let x = self.find(x);
		let y = self.find(y);
		if x == y { return; }
   	self.id[x] = y;        // 直接合并
		self.count -= 1;
	}

	/// 判断两个元素是否属于同一集合
	pub fn is_same_set(&mut self, x: usize, y: usize) -> bool {
		self.find(x) == self.find(y)
	}
	
	/// 所有集合中不相交集合的个数
	pub fn count(&self) -> usize {
		self.count
	}
}

按秩合并

对秩的定义有很多,例如:树高 / 集合的大小,按秩合并是一种启发式合并,将 “小的结构” 合并到 “大的结构” 上,这里使用树高作为秩的定义,也即将较矮的树合并到较高的树,直接将较低的树的根结点的父结点赋值为较高的树的根结点,这样合并后,较低的树每次合并后树高 +11 。这样在平均情况下,整体的时间复杂度会更小,对于一般的并查集,只使用按秩合并的话,将所有集合合并到一起的时间复杂度是 O(nlog n)O(nlog \ n) 那么每次查询操作的时间复杂度为 O(log n)O(log \ n).

相关信息

同时采用 “路径压缩” 和 “按秩合并” 时,时间复杂度会降到 O(α(n))O(α(n)) ,其增长比 log nlog \ n 更慢,所以 O(α(n))O(α(n))近似于常数阶的时间复杂度.

struct UnionFind {
	id: Vec<usize>,      // 集合代表 (根结点)  
	rank: Vec<usize>,    // 秩 (树高)
	count: usize,        // 不相交集合个数
}

impl UnionFind {
	/// 初始化并查集
	pub fn new(n: usize) -> Self {
		let mut id = vec![0;n];
		let mut rank = vec![0;n];
		for i in 0..n {
			id[i] = i;
			rank[i] = 1;
		}
		Self {id , rank , count: n}
	}

	/// 查找并查集所在集合
	pub fn find(&mut self, x: usize) -> usize {
		let mut x = x;
		while x != self.id[x] {
			x = self.id[x];                
			self.id[x] = self.id[self.id[x]];    // 路径压缩
		}
		x
	}

	/// 合并 x 所在的集合与 y 所在的集合
	pub fn merge(&mut self, x: usize, y: usize) {
		let x = self.find(x);
		let y = self.find(y);
		if x == y { return; }
		if self.rank[x] < self.rank[y] {          // y 所在树高于 x, 将 x 合并到 y
			self.id[x] = y;
		}else {
			self.id[y] = x;
			if self.rank[x] == self.rank[y] {       // 树高相同且不是同一集合, 树高 + 1 
				self.rank[x] += 1;
			}
		}
		self.count -= 1;
	}

	/// 判断两个元素是否属于同一集合
	pub fn is_same_set(&mut self, x: usize, y: usize) -> bool {
		self.find(x) == self.find(y)
	}
	
	/// 所有集合中不相交集合的个数
	pub fn count(&self) -> usize {
		self.count
	}
}

并查集的应用

在一般应用中,并查集中的每个集合往往需要维护更多的信息来解决更为复杂的问题,以下是两种常见的应用.

维护集合的大小

在按秩合并中,即可使用树高,也能使用集合大小 (集合中元素的个数),下面为同时维护集合大小的并查集.

struct UnionFind {
	id: Vec<usize>,      // 集合代表 (根结点)  
	size: Vec<usize>,    // 每个集合元素数量
	count: usize,        // 不相交集合个数
}

impl UnionFind {
	/// 初始化并查集
	pub fn new(n: usize) -> Self {
		let mut id = vec![0;n];
		let mut size = vec![0;n];
		for i in 0..n {
			id[i] = i;
			size[i] = 1;
		}
		Self {id , size ,count: n}
	}

	/// 查找并查集所在集合
	pub fn find(&mut self, x: usize) -> usize {
		let mut x = x;
		while x != self.id[x] {
			x = self.id[x];                
			self.id[x] = self.id[self.id[x]];    // 路径压缩
		}
		x
	}

	/// 合并 x 所在的集合与 y 所在的集合
	pub fn merge(&mut self, x: usize, y: usize) {
		let x = self.find(x);
		let y = self.find(y);
		if x == y { return; }
		if self.size[x] < self.size[y] {         // 将 x 合并到 y 中
            self.id[x] = y;
            self.size[y] += self.size[x];        // y 集合中元素增加
        } else {                                 // 将 y 合并到 x 中
            self.id[y] = x;
            self.size[x] += self.size[y];        // x 集合中元素增加
        }
		self.count -= 1;
	}

	/// 判断两个元素是否属于同一集合
	pub fn is_same_set(&mut self, x: usize, y: usize) -> bool {
		self.find(x) == self.find(y)
	}
	
	/// 所有集合中不相交集合的个数
	pub fn count(&self) -> usize {
		self.count
	}
}

带权并查集

并查集本身是由若干树组成的森林,可以在树中每条边上记录一个权值,在代码实现中,维护一个数组 dd ,用 d[x]d[x] 保存结点 xx 到父结点 fa[x]fa[x] 之间的边权。在每次路径压缩过后,每个访问的结点都会直接指向树根,如果在路径压缩过程中同时更新这些结点的 dd 值,也即利用路径压缩的过程来统计每个结点到树根之间的路径上的一些信息。

你认为这篇文章怎么样?
  • 0
  • 0
  • 0
  • 0
  • 0
  • 0
评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v2.14.9