并查集(Union-Find)

并查集是一种巧妙的算法思想,主要解决连通性问题的场景,比如:一组对象被划分成了若干个区域,求解被划分区域的数量,或者求解如何将这几个区域连接起来。

1. 并查集(Union-Find)算法介绍

1.1 动态连通性

问题的输入是一列整数对,其中每个整数都表示一个某种类型的对象,一对整数p和q可以被理解为“p和q是相连的”。我们假设“相连”是一种等价关系,这意味着它具有:

  • 自反性:p和p是相连的
  • 对称性:如果p和q是相连的,那么q和p也是相连的
  • 传递性:如果p和q是相连的且q和r是相连的,那么p和r也是相连的

我们需要设计一个数据结构来保存程序已知的所有整数对的足够多的信息,并用它们来判断一对象是否是相连的。我们将这个问题通俗地叫做动态连通性问题。

之后使用网络方面的术语,将对象称为触点,将整数对称为连接,将等价类称为连通分量或是简称分量。简单起见,假设我们有用0到N - 1 整数所表示的N个触点。这样做并不会降低算法的通用性。

1.2 union-find算法API

用一份API来封装所需的基本操作:初始化、连接两个触点、判断包含某个触点的分量、判断两个触点是否存在于同一个分量之中以及返回所有分量的数量。

在这里插入图片描述

  • 用一个以触点为索引的数组id[]作为基本数据结构来表示所有分量。一开始,我们有N个分量,每个触点都构成了一个只含有它自己的分量,因此我们将id[i]的值初始化为i,其中i在0到N-1之间。
  • 对于每个触点i,我们将find()方法用来判定它所在的分量所需的信息保存在id[i]之中。
  • connected()方法的实现只用一条语句find(p) == find(q),它返回一个布尔值,我们在所有方法的实现中都会用到connected()方法。

find()方法和union()方法的实现是以下内容要讨论的重点。

2 union-find的实现

public class UF {
	private int[] id;    //分量id(以触点作为索引)
	private int count;   //分量数量
	
	public UF(int N) {
		//初始化分量id数组,每个点指向自己
		count = N;
		id = new int[N];
		for(int i = 0; i < N; i++) {
			id[i] = i;
		}
	}
	
	public int count() {  
	    // 返回连通分量的个数
		return count;
	}
	
	// 判断 p 和 q 是否在同一个集合中
	public boolean connected(int p, int q) {
		return find(p) == find(q);
	}
	
	public int find(int p) {
		//见 quick-find、qucik-union、加权uick-union
		return -1;    //省略此条代码
	}
	
	public void union(int p, int q) {
		//见 quick-find、qucik-union、加权uick-union
	}
}

union-find的成本模型:在研究实现union-find的API的各种算法时,我们统计的是数组的访问次数访问任意数组元素的次数,无论读写)。

2.1 quick-find算法

此方法保证当且仅当id[p] 等于 id[q]时p和q是连通的。

  • 在同一个连通分量中的所有触点在id[]中的值必须全部相同。这意味着connected(p, q)只需要判断id[p] == id[q],当且仅当p和q在同一连通分量之中该语句才会返回true。
  • 为了调用union(p, q)确保这一点,我们首先要检查它们是否已经存于同一个连通分量之中。如果存在于同一分量中我们不需要采取任何行动,否则我们面对的情况就是p所在的连通分量中的所有触点的id[]值均为同一个值,而q所在的连通分量中的所有触点的id[]均为另一个值。
  • 要将两个分量合二为一,我们必须将两个集合中所有触点对应的id[]元素变为同一个值。为此,我们需要遍历整个数组,将所有和id[p]相等的元素的值变为id[q]的值。我们也可以将所有和id[q]相等的元素的值变为id[p]的值,两者均可。
public class QuickFindUF {
	private int[] id;    //分量id(以触点作为索引)
	private int count;   //分量数量
	
	public QuickFindUF(int N) {
		//初始化分量id数组
		count = N;
		id = new int[N];
		for(int i = 0; i < N; i++) {
			id[i] = i;
		}
	}
	
	public int count() {
		return count;
	}
	
	public boolean connected(int p, int q) {
		return find(p) == find(q);
	}
	
	public int find(int p) {
		return id[p];
	}
	
	public void union(int p, int q) {
		//将p和q归并到相同的分量中
		int pID = find(p);
		int qID = find(q);
		
		//如果p和q已经在相同的分量之中则不需要采取任何行动
		if(pID == qID) return;
		
		//将p的分量重命名为q的名称
		for(int i = 0; i < id.length; i++) {
			if(id[i] == pID) id[i] = qID;
		} 
		count--;
	} 
}
2.2 quick-union算法

此算法重点提高union()方法的速度,它和quick-find算法是互补的。

  • 每个触点对应的id[]元素都是同一个分量中另一个触点的名称(也可能是它自己)----我们将这种联系称为链接。
  • 在实现find()方法时,我们从给定的触点开始,由它的链接得到另一个触点,再由这个触点到达第三个触点,如此继续跟随着链接直到到达一个根触点,即链接指向自己的触点(这样的触点必然存在)。当且仅当分别由两个触点开始的这个过程到达了同一个根触点时它们存在于同一个连通分量中。
  • 为了保证这个过程的有效性,我们需要union(p, q)来保证这一点。它的实现很简单:我们由p和q的链接分别找到它们的根触点,然后只需将一个根触点连接到另一个根触点即可将一个分量重命名为另一个分量,因此这个算法叫做quick-union。
public class QuickUnionUF {
	private int[] id;    //分量id(以触点作为索引)
	private int count;   //分量数量
	
	public QuickUnionUF(int N) {
		//初始化分量id数组
		count = N;
		id = new int[N];
		for(int i = 0; i < N; i++) {
			id[i] = i;
		}
	}
	
	public int count() {
		return count;
	}
	
	public boolean connected(int p, int q) {
		return find(p) == find(q);
	}
	
	public int find(int p) {
		//找出分量的名称
		while(p != id[p]) p = id[p];
		return p;
	}
	
	public void union(int p, int q) {
		//将p和q的根节点统一
		int pRoot = find(p);
		int qRoot = find(q);
		if(pRoot == qRoot) return;
		
		id[pRoot] = qRoot;
		// id[find(p)] = find(q);
		count--;
	} 
}
2.3 加权quick-union算法

与其在union()中随意将一棵树连接到另一棵树,我们现在会记录每一棵树的大小并总是将较小的树连接到较大的树上

这项改动需要添加一个数组和一些代码来记录树中的结点数,它能够大大改进算法的效率,提高了查询根触点的速度。该算法构造的树的高度远远小于未加权的版本所构造的树的高度。

public class WeightedQuickUnionUF {
	private int[] id;    //父链接数组(由触点索引)
	private int[] sz;    //(由触点索引的)各个根节点所对应的分量的大小
	private int count;   //连通分量的数量
	
	public WeightedQuickUnionUF(int N) {
		count = N;
		id = new int[N];
		for(int i = 0; i < N; i++) id[i] = i;
		sz = new int[N];
		for(int i = 0; i < N; i++) sz[i] = 1;
	}
	
	public int count() {
		return count;
	}
	
	public boolean connected(int p, int q) {
		return find(p) == find(q);
	}
	
	public int find(int p) {
		//跟随连接找到根节点
		while(p != id[p]) p = id[p];
		return p;
	}
	
	public void union(int p, int q) {
		int pRoot = find(p);
		int qRoot = find(q);
		if(pRoot == qRoot) return;
		
		//将小树的根节点连接到大树的根节点
		if(sz[pRoot] < sz[qRoot]) {
			id[pRoot] = qRoot;
			sz[qRoot] += sz[pRoot]; 
		}else {
			id[qRoot] = id[pRoot];
			sz[pRoot] += sz[qRoot];
		}
		count--;
	}
}
2.4 使用路径压缩的加权quick-union算法

理想情况下,我们都希望每个节点都直接链接到它的根节点上,但我们又不想像quick-union算法那样通过修改大量链接来做到这一点。我们接近这种理想状态的方式很简单,就是在检查节点的同时将他们直接链接到根节点。

这种方法的实现很容易,而且这些树并没有阻止我们进行这种修改的特殊结构:如果这么做能够改进算法的效率,我们就应该实现它。

要实现路径压缩,只需要为find()添加一个循环将在路径上遇到的所有结点都直接链接到根节点。我们所得到的结果是几乎完全扁平化的树,它和quick-find算法理想情况下所得到的树非常接近。这种方法既简单又高效,但在实际情况下已经不太可能对加权quick-union算法继续进行任何改进了。

public class PathCondenseWeightedQuickUnionUF {
	private int[] id;    //父链接数组(由触点索引)
	private int[] sz;    //(由触点索引的)各个根节点所对应的分量的大小
	private int count;   //连通分量的数量
	
	public PathCondenseWeightedQuickUnionUF(int N) {
		count = N;
		id = new int[N];
		for(int i = 0; i < N; i++) id[i] = i;
		sz = new int[N];
		for(int i = 0; i < N; i++) sz[i] = 1;
	}
	
	public int count() {
		return count;
	}
	
	public boolean connected(int p, int q) {
		return find(p) == find(q);
	}
	
    /*递归版本
	public int find(int p) {
		if(id[p] == p) return p;
		id[p] = find(id[p]);
		return id[p];
	}
	*/
	
	public int find(int p) {
		int root = p;
		while (root != id[root]) {
			root = id[root];
		}
		while (id[p] != root) {
			int temp = p;
			p = id[p];
			id[temp] = root;
		}
		return root;
	}
	
	public void union(int p, int q) {
		int pRoot = find(p);
		int qRoot = find(q);
		if(pRoot == qRoot) return;
		
		//将小树的根节点连接到大树的根节点
		if(sz[pRoot] < sz[qRoot]) {
			id[pRoot] = qRoot;
			sz[qRoot] += sz[pRoot]; 
		}else {
			id[qRoot] = id[pRoot];
			sz[pRoot] += sz[qRoot];
		}
		count--;
	}
}
2.5 算法比较

各种union-find算法的性能特点(存在N个触点时成本的增长数量级(最坏情况下))
在这里插入图片描述

参考:

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
好的,我会尽力回答您的问题。以下是关于带有压缩规则和加权规则的find和union算法的主程序实现方法,并且打印数组存储情况: ``` #include <iostream> #include <vector> using namespace std; class UnionFind { public: UnionFind(int n) { for (int i = 0; i < n; i++) { parent.push_back(i); size.push_back(1); } } int find(int p) { while (p != parent[p]) { parent[p] = parent[parent[p]]; // 压缩规则,路径压缩 p = parent[p]; } return p; } void unite(int p, int q) { int rootP = find(p); int rootQ = find(q); if (rootP == rootQ) { return; } // 加权规则,将小树合并到大树中 if (size[rootP] < size[rootQ]) { parent[rootP] = rootQ; size[rootQ] += size[rootP]; } else { parent[rootQ] = rootP; size[rootP] += size[rootQ]; } } void print() { for (int i = 0; i < parent.size(); i++) { cout << parent[i] << " "; } cout << endl; } private: vector<int> parent; vector<int> size; }; int main() { int n = 10; UnionFind uf(n); cout << "Initial state: "; uf.print(); srand((int) time(0)); // 随机数种子 for (int i = 0; i < n - 1; i++) { int a = rand() % n; int b = rand() % n; uf.unite(a, b); cout << "State after union(" << a << ", " << b << "): "; uf.print(); } for (int i = 0; i < n; i++) { cout << "Find " << i << " -> " << uf.find(i) << endl; } return 0; } ``` 该程序实现了一个带有压缩规则和加权规则的Union-Find算法,可以随机执行union和find的方法,并在每次执行完union后打印数组的状态,以及执行完所有操作后打印每个元素的所属集合。 注意:这只是一个简单的示例程序,只是为了演示Union-Find算法的使用方法和规则,实际应用中需要根据具体情况进行优化和修改。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值