普林斯顿算法第一周笔记

问题引入

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

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

    例:7与8并无路径相连;0、一、二、五、六、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链接。web

\ 0 1 2 3 4 5 6 7 8 9
id[] 0 1 1 8 8 0 0 1 8 8

算法思路:算法

  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棵树。每一个结点为本身所在的树的根节点。
在这里插入图片描述数据结构

\ 0 1 2 3 4 5 6 7 8 9
id[] 0 1 9 4 9 6 6 7 8 9

id[] 的值规定:svg

  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[]
\ 0 1 2 3 4 5 6 7 8 9
id[] 0 1 9 4 9 6 6 7 8 6
代码
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记录值
	}
}
例子

在这里插入图片描述
缺点flex

  1. 树有可能会过高
  2. 查找操做的代价可能会很大。好比须要回溯一棵瘦长的树的时候。

3. Weighted quick-union算法

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

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

在这里插入图片描述

思路:在原先基础上,新添加的一组数组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;
}