首页 > 编程语言 > 详细

Union-Find算法分析以及路径压缩优化

时间:2021-02-07 23:14:19      阅读:53      评论:0      收藏:0      [点我收藏+]

1.前言

原文链接:https://blog.csdn.net/anhongning123/article/details/111033762
算法设计其实无非需要理解一下几点:
1.算法的目的是为了解决实际问题,应当为了更简便的解决实际问题设计算法(当然一些以安全为主的算法属于例外)

2.解决一个问题的方法并不唯一,但是总能通过“有效”的方法找到优秀的算法

3.优秀的算法往往代码也很简单
4.迭代式改进能够进一步改进算法
建议大家看看Algorithms原版(不要看汉化的,这是一本奇书),本文的内容就是书上的总结

2. 动态连通性,基础准备

假设问题的输入是一列元素对,其中每个元素都表示一个某种类型的对象,一对元素AB可以理解为“A与B具有连通性”。
相连性具有以下特点:

  • 自反性:A与B是相连的;
  • 对称性:如果A与B相连的,那么A与B也是相连
  • 传递性:如果A和C是相连的且B与C也是相连的,B与A也是相连的
    动态连通性具有较强的实用性,在此博主以一个较为简单的实例对动态连通性进行阐述,这个例子也是大多数教程都是在使用的:
    技术分享图片
    如上图,在输入过程中,图中的动态连通性是在不断变化的,那么可以过滤掉图中能够连通的元素对,最终能够发现,

(1)6号与7号(6号图)
(2)4号与5号(12号图)
(3)8号与9号(11号图)

三对元素是符合连通的(或者称其为相连或者等价也可以),那么在输入处理时可以直接将以上三对元素忽略。
连通性的实用性很强,当然,学过数据结构的同学应该对这个定义很熟悉了。例如在处理网络(减少布线或者最短路径),引用(变量名称等价)等方面都具有较强实用性。

3. 算法设计

3.1 分析建模

3.1.1 明确任务目标

前言中的第一点——
算法的目的是为了解决实际问题,应当为了更简便的解决实际问题设计算法(当然一些以安全为主的算法属于例外)
对于连通性而言,讨论元素间是否连通,一般是为了求得所需连通元素之间的具体分量(也就是每一个分量的具体路径)
以下列出连通性与输出分量路径的关系

连通 不连通
输出分量的具体路径(注意是否有向) 1 \
不输出分量的具体路径(注意是否有向) 2 3

3.1.2 数据结构操作

以上就是模型需要处理的问题(本文Union-Find算法仅解释第二种情况),当然,为了更好的对问题进行程序化表述,需要定义一组数据结构,
然后基于定义的数据结构完成分项的任务——
1. 定义数据结构(影响算法效率最直接的是数据结构)
2. 基于定义的数据结构完成分项操作
对于动态连通性的具体操作可以定义如下:

操作 解释
初始化 大多数算法在运行前都需要完成初始化操作
归并 完成两个独立的连通分量之间的连接
判断连接 判断两个元素是否属于同一连通分量
查找 找到需要的连通分量
计数 计算连通分量的数量

3.1.3 建模

其实和数学建模的方法是一致的,设计算法都需要抽象量化。连通性的本质实际上就是带有连接方式的分组,在Union-Find算法中我们并不需要考虑连接方式的问题
因此可以有如下假设方式:
对于N个元素可以使用一个统一的id[]数组进行元素标记,如果两个元素连通,那么就可以修改其标记的数组为同一编号。在初始化时每个元素必然是不连通的,因此可以初始化id[i]=i

可以定义API如下:

public class UnionFind UnionFind 初始化
int find(int p) 返回所在分量的标识
vioid union(int p,int q) 连通两个元素
boolean connected(int p,int q) 判断元素是否连通
int count() 统计连通分量的数量
Union-Find算法可以如下实现
public class UnionFind
{
	private int[] id; // 分量标识
	private int count; // 分量数量
	public UnionFind(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);
	public void union(int p, int q);
	public static void main(string[] args)
	{
	 //某输入输出操作
	}
	
}

其实 Union-Find的具体设计就由
public int find(int p);
public void union(int p, int q);
两个函数确定,所以接下来主要探讨find和union的方法。

3.2 Quick-Find 算法

实现union其实就是改变每个分量的标识,那么最简单的思路就是判断两个元素的标号,如果不同就将其分量修改为相同的的标识

public class QuickFind
{
	private int[] id; // 分量标识
	private int count; // 分量数量
	public QuickFind(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);
		// 元素已经连通则不做处理
		if (pID == qID) return;
		// 若不连通,直接改变标识
		for (int i = 0; i < id.length; i++)
			if (id[i] == pID) id[i] = qID;
		count--;
	}
	public static void main(string[] args)
	{
	 //某输入输出操作
	}
	
}

以上代码可以使用下图阐述:
技术分享图片
每次在输入后,都进行一次union遍历,如果元素处于不同分量,就将其修改为同一分量的标识,达到连通的效果。
Quick-Find的思路无疑是较为简便的,如果处理N个元素,意味着也要调用N-1次union,也就是说union的数量级是平方级别的。在数据规模较小的时候该算法会很简便,但如果数据规模较大,该算法显然不可行。
所以需要对以上算法进行优化,首先要明白调用union的次数是无法改变的,优化的目的就在
优化union上。

3.3 Quick-Union 算法

影响Quick-Find中union的因素是由于id[i]数组无法给出各个分量的具体元素类,如果想要得到 某一分量的全部元素只能遍历id[i].
因此为了提高效率,需要重新设计id[i]中元素的关系,也就是需要给出同一分量中各个元素的继承关系。
如下:
技术分享图片
技术分享图片

修改过程也会更为简单,只需将根节点指向另一根节点即可,如上图。
这里的id[]种存放的不再是元素,而是该元素的父节点,在遍历过程中总能找到一个根节点使得id[root]=root,于是可以用这个id[root]作为该分量的标识。并由此可以设计find函数。
实现代码如下:

private 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;
	id[pRoot] = qRoot;    
	count--;
}

这里不建议使用链表或者图的结构,应为连id[]中需要使用一个标志性元素代表整个分量,在树中可以使用根节点作为唯一标识。而链表是线性结构,无法满足多对一的性质。
此外,对于增删改三种处理,效率最高的还是树。

Quick-Union 解决了union的遍历问题,但是在最坏的情况下,这种方式会退化成一个单链表。这种情况下也是平方级别的。
因为Quick-Union中的find是影响算法效率的主要原因,最坏情况下需要调用find的次数为3+5+7+9+......+(2N-1)
技术分享图片

(尽管对于增删改三种处理,效率最高的还是树,但是树的结构会受到输入数据的结构影响)

3.3.1 路径压缩的Quick-Union(Quick union with path compression)

这里给出路径压缩方式对Quick-Union的优化,对于Simpler one-pass variant优化的算法均摊成本为对数级别
在Quick-Union中,实现find其实是向上遍历直到寻找到$root=id[root]$的节点,但在遍历过程中其实可以事先对遍历的路径进行优化。这里给出两种路径压缩的方法:

  • 双通实现(Two-pass implementation):先找到根节点,然后使得所有节点都指向根节点。
    技术分享图片

- 简化单通变体(路径减半)Simpler one-pass variant :使路径中的每一个其他节点指向它的爷爷节点,即$id[i] = id[id[i]]$;

技术分享图片

// QuickUnionPathCompressionUF
//路径压缩算法主要优化的是find过程
public int find(int p) {
        int root = p;
 双通实现:添加第二个循环,将每个检查节点的id[]设置为根
        while (root != id[root])
            root = id[root];
        while (p != root) {
            int newp = id[p];
            id[p] = root;
            p = newp;
        }
        return root;
   }
简化单通(路径减半):使路径中的每一个其他节点指向它的爷爷节点
public int find(int i){
 	while (i != id[i])
 	{
 	//如果p元素的父亲指针指向的不是自己,说明p并不是集合中的根元素,还需要一直向上查找和路径压缩
 			id[i] = id[id[i]];
 			i = id[i];
 	}
 	return i;
}
public void union(int p, int q) {
       int rootP = find(p);
       int rootQ = find(q);
       if (rootP == rootQ) 
       		return;
       id[rootP] = rootQ;
       count--;
    }

3.4 加权Quick-Union 算法

显然,Quick-Union 算法 依旧存在着弊端,需要引入一种处理方法解决Quick-Union 算法的最坏情况,或者说使用一种方式让连通分量合并时find寻找根节点的路径更短,也就是让合并的后的分量高度更低。
技术分享图片
为此,需要设立一个数组存放根节点对应的分量大小。代码如下:

public class WeightedUnionFind
{
	private int[] id; // 分量标识
	private int[] sz; // 各个根节点所对应的根节点大小
	private int count; // 分量数量
	public WeightedUnionFind(int N)
	{
		// 初始化id数组
		count = N;
		id = new int[N];
		for (int i = 0; i < N; i++)
			id[i] = i;
		// 初始化sz数组	
        sz = new int[N];
		for (int i = 0; i < N; i++)
			sz[i] = i;

	}
	public int count()
	{ return count; }
	public boolean connected(int p, int q)
	{ return find(p) == find(q); }
	private int find(int p)
    { 
	//找到分量的代表元素
	while (p != id[p]) p = id[p];
	return p;
    }
	public void union(int p, int q)
    {
	int i = find(p);
	int j = find(q);
	if (i == j) return;
	//分量连接
	if (sz[i] < sz[j]) { id[i] = j; sz[j] += sz[i]; }
	else { id[j] = i; sz[i] += sz[j]; }
	count--;
    }
	public static void main(string[] args)
	{
	 //某输入输出操作
	}
	
}

同样,也需要对算法的最坏情况进行分析,最坏情况 如下图:
技术分享图片
对于加权Quick-Union算法而言,其目的就是保证find操作的最高效率,也就是使得任意元素到达根节点的距离最短,这里直接给出结论:
在最坏的情况下,对于加权Quick-Union算法find、connected和union的成本数量级为logN。这使得该算法在处理大规模 数据时具有较好的效率。

4.总结

在最坏情况下,各种Union-Find的增长数量级如下:

算法 union find
Quick-Find N N
Quick-Union Height(树的高度) Height(树的高度)
加权Quick-Union lgN lgN
使用路径压缩的加权Quick-Union ~1(趋近1) ~1(趋近1)

Union-Find算法分析以及路径压缩优化

原文:https://www.cnblogs.com/suanfawu/p/14386506.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!