union-find算法

问题描述

动态连通性问题:给定n个顶点,仅支持输入一对整数,一对整数p和q可以被理解为“p和q是相连的”。给定任何一对顶点,判断其是否连通。


Union-find数据模型

目标:为union和find操作设计一个高效的数据结构

先设计一份API来封装所需的基本操作:初始化、连接两个连通分量、判断包含顶点的连通分量、判断两个顶点是否在同一连通分量以及所有连通分量的个数



Union-find算法

1)Quick-find

数据结构:id[i]是顶点i的所属连通分量的标识


那么,

Find操作:找出顶点i的id,即id[i]

Connected操作:p和q是否有相同的id

Union操作:将p和q归入到一个连通分量中,将p所在的连通分量的所有顶点id改成id[q]

/**
 * find操作只需要访问一次数组,而union操作访问数组次数在(N+3)~(2N+1)之间
 * @author zhang
 *
 */
public class QuickFindUF {
	private int[] id;   //代表所属的连通分量
	private int count;  //连通分量的总数量
	public QuickFindUF(int n) {
		count = n;
		id = new int[n];  // 对数组id进行初始化
		for(int i = 0; i < n; i++) {
			id[i] = i;    //初始时,每个顶点就是一个连通分量
		}
	}
	
	public void union(int p ,int q) {
		//在p和q之间添加一条连接,将p和q归入到一个连通分量中
		int pID = find(p);
		int qID = find(q);
		//如果q和p同属一个连通分量,则不做任何操作
		if(pID == qID) return;
		//否则,将p所在的连通分量的所有顶点id改成qID
		for(int i = 0; i < id.length; i++) {
			if(id[i] == pID) id[i] = qID;   //访问数组次数为O(n)
		}
		count--;
	}
	
	public int find(int p) {
		return id[p];
	}
	
	public boolean connected(int p ,int q) {
		return find(p) == find(q);
	}
	
	public int count() {
		return count;
	}

}

其各个操作的cost如下:


存在的问题:Union操作代价太高了!!!一共要进行n-1次union,那么时间复杂度为O(n^2),这显然代价太高了!

2)Quick-union

数据结构:与Quick-find不同,id[i]代表的是i的parent,那么顶点i的root就是id[id[id[...id[i]...]]]


那么,

Find操作:找到p所属连通分量的根节点,即id[id[id[...id[i]...]]]

Connected操作:p和q是否有相同的root

Union操作:找到p、q所在连通分量的根节点,将p的根节点的父节点改成q的根节点

public class QuickUnionUF {
	private int[] id;   //与QuickFindUF不同,id[i]代表的是i的parent
	private int count;  //连通分量的总数量
	public QuickUnionUF(int n) {
		count = n;
		id = new int[n];
		for(int i = 0; i < n; i++) {
			id[i] = i;  //初始时,每个顶点就是一个连通分量,它的父顶点都是自己
		}
	}
	
	public void union(int p, int q) {
		//即找到p、q所在连通分量的根节点,将p的根节点的父节点改成q的根节点
		int i = find(p);
		int j = find(q);
		//若在同一连通分量上,不做操作
		if(i == j) return;
		
		id[i] = j;
		count--;
	}
	
	public int find(int p) {
		//找到p所属连通分量的根节点
		while(p != id[p]) {
			p = id[p];
		}
		return p;
	}
	
	public boolean connected(int p ,int q) {
		return find(p) == find(q);
	}
	
	public int count() {
		return count;
	}
}
各个操作代价:


这种数据结构,可能会造成树很高,一条直链的情况,则Find/connected操作代价会很高,最坏情况为树的高度即N


3)  加权Quick-union
加权quick-union就是为了解决其可能会出现树很高的情况,它保存了每棵树的节点数作为其权重,每次进行union操作时,将小树连接到大树,这样可以有效地降低树的高度,加权quick-union算法构造的森林中的任意节点的深度最多为lgN。
/**
 * 在QuickUnionUF中,union可能会出现长链的情况,现在记录每一棵树的大小,并总是将最小的树加到最大的
 * 树上,以减少树的高度,这样find、union、connected操作复杂度为O(lgn)
 * @author zhang
 *
 */
public class WeightedQuickUnionUF {
	private int[] id;     //与QuickFindUF不同,id[i]代表的是i的parent
	 int[] weight; //记录每棵树的大小
	private int count;    //连通分量的总数量
	public WeightedQuickUnionUF(int n) {
		count = n;
		id = new int[n];
		weight = new int[n];
		for(int i = 0; i < n; i++) {
			id[i] = i;     //初始时,每个顶点就是一个连通分量,它的父顶点都是自己
			weight[i] = 1; //初始时,每棵树的大小为1
		}
	}
	
	public void union(int p, int q) {
		//即找到p、q所在连通分量的根节点,并将小的树的根节点加到大树的根节点上
		int i = find(p);
		int j = find(q);
		//若在同一连通分量上,不做操作
		if(i == j) return;
		
		if(weight[i] < weight[j]) { id[i] = j; weight[j] += weight[i]; }//只需要更改root的权值即可,没必要对每个顶点做更改
		else                      { id[j] = i; weight[i] += weight[j]; }
		count--;
	}
	
	public int find(int p) {
		//找到p所属连通分量的根节点
		while(p != id[p]) {
			p = id[p];
		}
		return p;
	}
	
	public boolean connected(int p ,int q) {
		return find(p) == find(q);
	}
	
	public int count() {
		return count;
	}

}
加权quick-union算法构造的森林中的任意节点的深度最多为lgN,证明如下:

其各个操作的代价:

4) 加权Quick-union的继续优化:路径压缩

理想情况下,我们希望每个节点直接链接到根节点,但又不想像quick-find那样修改大量的链接。实现的方式很简单:就在检查节点的同时将这些节点全部链接到根节点。实现这样的路径压缩,只需要在find()添加一个循环,将在路径上遇到的所有节点直接连接到根节点上即可。

修改find的代码:

这样,我们得到的结果几乎是扁平化的树,实际上也很难在对加权quick-union进行优化了,路径压缩的加权quick-union算法已经是最优的算法,但并非所有操作都能在常数时间内完成。其union和find非常非常接近但人没有达到1(均摊成本)

在最坏情况下,各个算法的对比:


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值