并查集

本文介绍了并查集这一数据结构,用于解决动态连接问题,如像素操作、网络计算机、社交网络好友关系等。文中详细讲解了Quick Find、Quick Union、Weighted Quick Union和Path Compression四种实现方式,并分析了它们的性能和实际应用。

1. 动态连接问题

实际问题


Union Find 常用来解决动态连接 (Dynamic connectivity) 问题。在很多实际应用中,都存在动态连接问题,比如:

  • 一张图里面的像素点的相关操作
  • 网络中的计算机
  • 社交网络中的好友关系
  • 数学集合中的元素

抽象问题


简单来说,动态连接问题可以抽象为如下问题:

有若 N 个节点 0,1,…,n-1,希望对它能有如下操作:

  • Union:将两个节点连接在一起
  • Find / connected query:询问两个节点是否连通

比如经过如下操作后:

union(4, 3)
union(3, 8)
union(6, 5)
union(9, 4)
union(2, 1)

可以得到连通图:

此时,

connected(0, 7)		//false
connected(8, 9)		//true

如果再做如下连通操作:

union(5, 0)
union(7, 2)
union(6, 1)
union(1, 0)

此时更新后的连通图如下:

此时,

connected(0, 7)		//true

直观的来说,动态连接问题应该有如下等价关系:

  • 自等性:
connected(p, p)		//true
  • 对称性
if(connected(p, q))
then connected(q, p) == true
  • 传递性
if(connected(p, q) && connected(q, r))
then connected(p, r) == true

2. 解决思路

连通集


我们可以维护若干个连通集,每个连通集内部的元素都相互连通,不属于同一个连通集的元素则尚未连通:

所以,现在的问题就变成,如何确定并实时更新连通集??

类的设计


  • 用节点数 N 来构造一个 UF 类实例
  • union 函数用来将两个节点连通在一起
  • connected 函数用来查询两个节点是否连通
  • 注意这里并没有涉及到节点内部的具体细节,每一个节点是用一个 index 来表示,那节点和 index 之间的关系需要另外保存(比如可以用一个 map, 或者 hashTable)

3. 代码实现

3.1 Quick Find

数据结构


  • 用一个大小为 N 的整数数组 id[N]
  • 当且仅当 id[p] == id[q] 时,说明 p, q 连通

逻辑实现


  • 初始时,id[i] = i,表明每一个节点属于一个单独的连通集,各个节点之间互不连通:
  • union(p, q) 操作时,将 id[p]id[q] 的值设为一致,注意要将当前 id[p] 所在的连通集合内的所有元素都加入 id[q] 所在的连通集。(当然你也可以反过来添加)
  • connected(p, q) 函数只需要判断 id[p] ==? id[q]

代码实现


class QuickFindUF {

public:
	QuickFindUF(int n) {

		eleCount = n;
		id = new int[eleCount];

		//初始化为不同集合
		for (int i = 0; i < eleCount; ++i) {
			id[i] = i;
		}
	}

	void union(int p, int q) {

		if (!connected(p, q)) {

			int pid = id[p];
			int qid = id[q];

			//遍历查找 p 所在的集合,然后将其加入到 q 所在的集合
			for (int i = 0; i < eleCount; ++i) {
				if (id[i] == pid) {
					id[i] = qid;
				}
			}
		}
	}

	bool connected(int p, int q) {

		return id[p] == id[q];

	}


	~QuickFindUF() {
		delete[] id;
	}

private:
	int *id;
	int eleCount;
};

性能分析


algorithminitializeunionfind
quick findnn1
  • 可以看到,union 操作时间效率是 O(n),那如果我们进行 n 次 union 操作,时间效率将会是平方指数级别,效率太低

3.2 Quick Union

数据结构


  • 用一个大小为 N 的整数数组 id[N]
  • id[p] 中存储的是它的父节点的 index,也就是整个的会呈现出树状的关系
  • 如果 id[p] = p, 说明 p 节点就是根节点

逻辑实现


  • 初始时,id[i] = i,表明每一个节点属于一个单独的连通集,各个节点之间互不连通:
  • union(p, q) 操作时,找到 p 的根节点 pRoot 以及 q 的根节点 qRoot,将 pRoot 挂在 qRoot 上,使之成为 qRoot 的子节点。(当然你也可以反过来挂)

代码实现


class QuickUnionUF {

public:
	QuickUnionUF(int n) : eleCount(n) {
		
		id = new int[eleCount];

		//初始化为 n 个不同的连通集
		for (int i = 0; i < eleCount; ++i) {
			id[i] = i;
		}
	}

	void unionTwo(int p, int q) {

		//找到 p 的根节点合 q 的根节点
		int pRoot = getRoot(p), qRoot = getRoot(q);
		
		//把 pRoot 挂到 qRoot 上
		id[pRoot] = qRoot;
	}

	bool connected(int p, int q) {
		
		return getRoot(p) == getRoot(q);
	}

	~QuickUnionUF() {

		delete[] id;
	}

private:

	//计算 p 的根节点
	int getRoot(int p) {

		while (p != id[p]) {
			p = id[p];
		}
		return p;
	}
private:
	int *id;
	int eleCount;
};

性能分析


algorithminitializeunionfind
quick findnn1
quick unionnn (worst)n (worst)
  • 就平均性能而言,quick union 的 union 操作应该是比 quick find 的 union 操作要快的,但是 find 操作却比 quick find 的 find 操作要慢
  • 在最差的情况下, 如果连通集被连成了一个链表结构,此时需要遍历整个链表来寻找根节点,所以此时对于 unionfind 操作都会变成 O(n),性能变差

3.3 Weighted Quick Union

quick union 最大的问题,就是所构建的 tree 的高度可能会很高(极端情况下变成链表),导致查找 root 时效率低,所以一个很直观的解决办法,就是 union 操作构建树结构的时候,尽量让 smaller tree 挂在 big tree 上,这样构建的树的高度就会降低:


数据结构


  • 在 quick union 的基础上,多维护一个 size[eleCount] 数组,size[i] 用来记录以节点 i 为根节点的树的大小,注意这里的大小用树中元素的个数表示,而不是树的高度。

代码实现


  • connected 操作不变
  • union 操作只需要多记录一下树的大小
	void unionTwo(int p, int q) {

		//找到 p 的根节点合 q 的根节点
		int pRoot = getRoot(p), qRoot = getRoot(q);
		if(pRoot == qRoot)
			return;
		
		//p 所在的树小,q 所在的树大
		//将 p 树挂到 q 上
		if(size[pRoot] < size[qRoot]){
			id[pRoot] = qRoot;
			size[qRoot] += size[pRoot]; 
		}
		else{
			id[qRoot] = pRoot;
			size[pRoot] += size[qRoot]; 
		}
	}

性能分析


algorithminitializeunionfind
quick findnn1
quick unionnn (worst)n (worst)
weight quick unionnlog2nlog2n

3.4 Path Compression

上面的 weight quick union 看上去已经很不错了,但是,优化之路并没有结束,这个问题还可以继续优化。

在查找节点 p 的根节点 root 时,我们会从节点 p 出发,一直向上查找祖辈节点,直到根节点。那我们完全可以在遍历的时候,顺手就把路径上某个节点 i 的父节点指向更远的祖辈节点,压缩路径,进一步减少了树的高度。

代码实现


	//计算 p 的根节点
	int getRoot(int p) {

		while (p != id[p]) {			
			//设置 p 的父节点是其祖父节点
			id[p] = id[id[p]];
			p = id[p];
		}
		return p;
	}

只需要一行代码就可以实现路径的压缩,何乐而不为呢!!


4. 实际应用

有这样一个物理连通性问题抽象如下:

  • 有 N X N 的矩阵,每个格子有打开和未打开两种状态
  • 打开的概率为 p,未打开的概率就是 1 - p

现在想知道,整个 N 阶矩阵连通的概率是多少?连通是指从上到下至少有一条连通的路径:

在这里插入图片描述

Union Find 解决


  • 初始化:每一个格子作为一个节点,将互相连通的格子用 union 操作将对应的节点连接起来

在这里插入图片描述

  • 每添加一个打开的节点,就需要与它相邻的打开的节点进行连通

在这里插入图片描述

  • 判断上下节点之间是否有连通的。如果采用暴力解法,那么时间复杂度将会达到 O(n2)。这里可以在上下分别添加一个节点,分别和最上行和最下行的节点相连,那么我们只需要判断这两个节点是否连通就可以了

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值