普林斯顿算法第一周笔记

问题引入

给定一组N个对象,支持两个操作:

  1. 连接两个对象。
  2. 是否有连接两个对象的路径?

    例:7与8并没有路径相连;0、1、2、5、6、7之间有路径相连

理论基础

  • 等价关系:
  1. 自反性(Reflexive):∀x∈A, (x,x)∈R. p is connected to p.
  2. 对称性(Symmetric):(x,y)∈R, (y,x)∈R. if p is connected to q, then q is connected to p.
  3. 传递性(Transitive):(x,y)∈R ∧ (y,z)∈R → (x,z)∈R. if p is connected to q and q is connected to r, then p is connected to r.
  • 连通分量(Connected component):相互连接的对象的最大集合
    例如:{0}是一个连通分量。同样{1 4 5}也为一个连通分量
    在这里插入图片描述

实现

一些说法
  1. 触点:对象
  2. 连接:整数对
  3. 分量(连通分量):等价类
  4. 链接:每个触点所对应的id[]元素都是同一个分量中的另一个触点的名称(也可能是它自己),这种联系称为链接
1. Quick-find算法

数据结构:int id[].
解释:在同一连通分量的id[i],赋相同的值
在这里插入图片描述
下面的数组表示:0,5和6连接 ;1,2和7连接 ;3,4,8和9连接。

\0123456789
id[]0118800188

算法思路:

  1. Find.找到p对应的id[p]
  2. Connected.当id[p] == id[q]时,p与q连通
  3. Union. union(p,q)要保证p所在的连通分量中所有的触点的id[]均为同一值
    在这里插入图片描述
    例:union(2,5):连接2和5.即将{1 4 5}与{2 3 6 7 }合并了。连通分量数量由三个变为两个。
代码
函数(方法)解释
void union(int p, int q)在p与q之间添加一条连接
int find(int p)p所在的连通分量的标识符
boolean connected(int p, int q)如果p和q存在同一个分量中则返回true
int count()连通分量的数量
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;
	
	//遍历数组,合并连通分量
	for(int i = 0; i < id.length; i++)
	{
		if(id[i] == pID)//访问n次数组
			id[i] = qID;//最好的情况下执行1次,最坏的情况下(n-1)次
	}
	count--;//连通分量的数量减一
}
算法分析

在quick-find算法中,每次find()调用只需要访问数组一次,而归并两个分量的union()操作访问数组的次数在(N+3)~(2N+1)次
证明:每次connected()调用都会检查id[]数组中的两个元素是否相等,即调用两次find()方法。归并两个分量的union()操作会调用两次find(),检查id[]数组中的全部N个元素并改变它们中1~N-1个的元素的值

2. Quick-union算法

目的:提高union()方法的速度
数据结构:int id[].
解释:id[i]存放i的双亲结点.即把所有的对象看作森林,每一个连通分量为一棵树。初始化时,共有n个结点,即有n棵树。每个结点为自己所在的树的根节点。
在这里插入图片描述

\0123456789
id[]0194966789

id[] 的值规定:

  1. 每个连通分量选定一个作为根结点。
  2. 若i为根结点,则id[i]指向自己,即id[i] = i

算法思路:改进union(p, q)方法
由p和q的链接分别找到它们的根触点,然后只需将一个根触点链接到另一个即可将一个分量重命名为另一个分量

  1. Find.通过回溯找到p和q对应的根结点
  2. Connected. p和q所在树的根结点是否相等
  3. Union.将p的根结点的id值设置为q的根结点的id值。就是将q的根结点变为p的根结点的双亲结点
    在这里插入图片描述
    这里将9的根节点设为5的根节点6(也就是合并了3和5所在的分量)
    p和q合并以后的id[]
\0123456789
id[]0194966786
代码
public class QuickUnionUF
{
	private int[] id;
	public QuickUnionUF(int N)
	{
		id = new int[N];
		for (int i = 0; i < N; i++) 
			id[i] = i;//现将双亲结点(根结点)设置为自己
	}
	
	//查找分量名称
	public int find(int i)
	{
		while (i != id[i]) //通过回溯寻找根节。如果是父节点,return;否则,将i在树中上移一层,直到找到根节点。
			i = id[i];
		return i;
	}
	
	//合并操作
	public void union(int p, int q)
	{
		int i = find(p);
		int j = find(q);
		id[i] = j;//将第一个根节点id记录值设为第二个根节点id记录值
	}
}
例子

在这里插入图片描述
缺点

  1. 树有可能会太高
  2. 查找操作的代价可能会很大。比如需要回溯一棵瘦长的树的时候。
3. Weighted quick-union算法

目的:避免回溯一棵瘦长的树,我们希望在合并一大一小的树的时候,避免将大树放在小树下。
在这里插入图片描述

因此我们使用一个数组存储每棵树的的结点数。在合并两棵树时,比较两棵树的结点数,用把结点多的树连接到结点少的树上。这样就确保了小树的根节点作为大树的根节点的子节点(大树的孩子)。

在这里插入图片描述

思路:在原先基础上,新添加的一组数组sz[]存放树的结点数。数组的下标对应根结点,下标对应的值为树的结点数。

代码
if (sz[i] < sz[j]) 
{ 
	id[i] = j; 
	sz[j] += sz[i];
}
else 
{ 
	id[j] = i;
	sz[i] += sz[j];
}

时间复杂度
时间与节点在树中的深度成正比。树中任意节点的深度是lg n
在这里插入图片描述
假设t1中有i个元素,t2中有j个元素。x属于t1,而x所在集合的位置最大为n。
当t1与t2合并时(i ≤ j),t1加到t2根结点下。此时x的深度+1,而合并后的树容量至少扩大2倍(因为p ≤ q,所以t1和t2合并后的集合大小至少是t1的两倍)。
设一开始x所在位置是1,要到达位置为n,就需要树的大小就要翻lg n次倍(每次翻倍,深度都会增加1,所以 2的lgn次方 = x的位置,即 2lgn = n)

4. 最优算法

路径压缩的weighted quick-union是最优化算法。
在检查结点的同时将它们直接连接到根结点上。
在这里插入图片描述
找出p的根结点,将id[p]设置为指向该根结点。
x为下一将与根结点连接的结点。

思路:要实现路径压缩,可以为find()添加一个循环,将在路径上遇到的所有结点都直接连接到根结点上。这样就能得到一棵几乎扁平化的树了。

代码
public int find(int i)
{
	while (i != id[i])
	{
		id[i] = id[id[i]];
		i = id[i];
	}
	return i;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值