等价关系:定义(p,q)表示p与q相连,是一种等价关系,具有自反性,对称性以及传递性。
等价关系定义了等价类,给定多个整数对,判断其是否属于同一类,即是否具有动态连通性。
从集合的角度在处理(p,q)时,判断其是否属于不同的集合,如果属于不同的集合则将p以及p所属的集合进行合并。
给定多个整数对,可以确定多个动态连通分量,定义数据结构
Quick-Find
利用数组id来表示每个元素所在的分量Id,判断两个元素p,q是否属于同一分量,则判断id[p]==id[q]即可。
//某个元素所在分量序号
public int find(int p){
return id[p];
}
public boolean connected(int p, int q) {
return find(p) == find(q);
}
当执行union操作时,如果属于不同分量,则将p所在分量的所有元素id分量索引全部改为p所在分量Id
//如果p,q处于不同分量中,则将p,q分量合并,将p所在分量所有元素改为q分量Id
//合并union性能糟糕
public void union(int p, int 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 class QuickFindUF {
//分量Id id[p]=x表示元素p所在的分量Id为x
private int[] id;
//分量数量
private int count;
//初始化N个元素N个分量
public QuickFindUF(int N) {
count = N;
id = new int[N];
for(int i=0; i<N; i++){
id[i] = i;
}
}
//某个元素所在分量序号
public int find(int p){
return id[p];
}
public boolean connected(int p, int q) {
return find(p) == find(q);
}
//如果p,q处于不同分量中,则将p,q分量合并,将p所在分量所有元素改为q分量Id
//合并union性能糟糕
public void union(int p, int 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 int count(){
return count;
}
}
find方法性能很好,但是union方法为线性,当输入N个整数对时,整体算法来O(N*N)级别。
给出测试如下:
public static void main(String[] args) {
QuickFindUF uf = new QuickFindUF(5);
uf.union(0, 1);
uf.union(0, 2);
uf.union(0, 3);
uf.union(0, 4);
}
id数组变化如下所示:
[0, 1, 2, 3, 4]
[1, 1, 2, 3, 4]
[2, 2, 2, 3, 4]
[3, 3, 3, 3, 4]
[4, 4, 4, 4, 4]
Quick-Union
quick-find算法union方法性能糟糕,进行改进。将id数组看成是父连接数组,即对于x来说,id[x]=p,表示p与x处于同一分量中。
find方法根据p=id[p]不断寻找,直到找个某个根节点
//某个元素所在分量根元素序号
public int find(int p){
while(p != id[p]){
p = id[p];
}
return p;
}
union合并方法,则首先找到两个元素的根节点,如果根节点不同,则将p所在根节点指向q所在根节点
实际上id数组保存多颗树,每个分量利用父连接组织成一棵树,利用树的根节点来表示某个元素所在的分量
//如果p,q处于不同分量中,则将p,q分量合并
//合并算法效率较高 但是find效率较低
public void union(int p, int q) {
int pRoot = find(p);
int qRoot = find(q);
if(pRoot == qRoot) return;
//将p分量根元素变为q所在根元素
id[pRoot] = qRoot;
count--;
}
代码如下:
public class QuickUnionUF {
//同一个分量中另一元素的名称 例如id[p]=x表示p与x处于同一分量
//id数组用父连接表示一片森林
private int[] id;
//分量数量
private int count;
//初始化N个元素N个分量
public QuickUnionUF(int N) {
count = N;
id = new int[N];
for(int i=0; i<N; i++){
id[i] = i;
}
}
//某个元素所在分量根元素序号
public int find(int p){
while(p != id[p]){
p = id[p];
}
return p;
}
public boolean connected(int p, int q) {
return find(p) == find(q);
}
//如果p,q处于不同分量中,则将p,q分量合并
//合并算法效率较高 但是find效率较低
public void union(int p, int q) {
int pRoot = find(p);
int qRoot = find(q);
if(pRoot == qRoot) return;
//将p分量根元素变为q所在根元素
id[pRoot] = qRoot;
count--;
}
public int count(){
return count;
}
}
union会将两个树不断合并,最坏情况下,树会蜕变成链表,导致find算法查找效率低下。
加权Quick-Union
加权Quick-Union主要改进如下:在union合并两棵树时,将小树合并到大树中,避免树的高度过度增长
利用sz数组来保存每颗树的元素个数
代码如下:
public class WeightedQuickUnionUF {
//同一个分量中另一元素的名称
//id数组用父连接表示一片森林
private int[] id;
//分量数量
private int count;
//各个根节点对应分量大小
private int[] sz;
//初始化N个元素N个分量
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 find(int p){
while(p != id[p]){
p = id[p];
}
return p;
}
public boolean connected(int p, int q) {
return find(p) == find(q);
}
//如果p,q处于不同分量中,则将p,q分量合并
//合并算法效率较高 但是find效率较低
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 int count(){
return count;
}
}
执行union时,判断两个树的元素大小,将小树合并到大树中,避免树的高度过度增加,保证算法在对数性能级别。