1. 动态连通性
1.1 动态连通性问题
首先我们来详细说明一下问题:
问题的输入是一列整数对,其中每个整数都表示一个某种类型的对象,一对整数p和q可以被理解我"p和q是相连的".我们假设"相连"是一种等价关系,这也就意味着它具有:
- 自反性
- 对称性
- 传递性:如果p和q是相连的其q和r是相连的,那么p和r也是相连的
等价关系能够将对象分为多个等价类.我们的目标是编写一个程序来过滤掉序列中所有无意义的整数对(两个整数对来自同一个等价类中).换句话说当程序从输入中读取了证书对pq时,如果已知的整数对都不能说明p和q是相连的,那么将这一对整数写入到输出中.如果已知的数据可以说明p和q是相连的,那么程序应该忽略pq这对整数并继续处理输入中的下一对整数.
为了达到预期效果我们需要设计一个数据结构保存程序中已知的所有整数对的足够多的信息,并用他来判断一对新的对象是不是相连.我们将这个问题通俗的叫做**动态连通性问题
**
1.2 union-find算法的API
如果两个触点在不同的分量之中,union()操作会将两个分量归并.find()操作会返回触点所在联通分量的标识符.connected()操作能够判断两个触点是否存在于同一个分量之中.count()方法会返回所有连通分量的数目.一开始我们有N个分量,将两个分量并归之后,union()操作会使得分量总数减一
所有的实现都应该:
- 定义一种数据结构标识已知的连接
- 基于此数据结构实现高效的union(),find(),connected()和count()方法
API已经说明触点和分量都是用int来表示,所以我们可以用一个以触点为索引的数组id[]作为基本数据结构来表示所有的分量.我们将使用某个触点的名称作为分量的标识符,因此你可以认为每个分量都是由他的触点之一标识的
一开始,我们有n个分量,每个触点都构成了只含有他自己的分量,因为我们将id[i]的值初始化为i,其中i在0到N-1之中.对于每个触点i,我们将用find()方法他所在的分量保存在id[i]中.connected()方法的实现只有一条语句find§==find(q),他返回一个boolean值,我们所有方法的实现都会用到conencted()方法
总之,我们维护了两个实例变量,一个是连通分量的个数,一个是数组id[].
union-find的实现
package 基础.算法分析;
import java.util.Scanner;
/**
* Union-Find算法的实现
*/
public class UF {
private int[] id;//分量Id(以触点为索引)
private int count;//分量数量
public UF(int N) {
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];
}
/**
* quick-find
* @param p
* @param q
*/
public void union(int p, int q){
if (connected(p,q))return;
int pid = id[p];
int qid = id[q];
for (int i = 0; i < id.length; i ++){
if (id[i] == pid) id[i] = qid;
}
count--;
}
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int N = in.nextInt();
UF uf = new UF(N);
while (!in.hasNext("EOF")){
int p = in.nextInt();
int q = in.nextInt();
if (uf.connected(p,q)) continue;
uf.union(p,q);
System.out.println(p +" "+q);
}
System.out.println(uf.count+" components");
}
}
2. 实现
2.1 quick-find算法
一种方法是保证当且仅当id[p]=id[q]时p和q是连通的.换句话说,在同一个联通分量中所有触点在id[]中的值必须全部相同.
package 基础.算法分析;
import java.util.Scanner;
/**
* Union-Find算法的实现
*/
public class UF {
private int[] id;//分量Id(以触点为索引)
private int count;//分量数量
public UF(int N) {
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];
}
/**
* quick-find
* @param p
* @param q
*/
public void union(int p, int q){
if (connected(p,q))return;
int pid = id[p];
int qid = id[q];
for (int i = 0; i < id.length; i ++){
if (id[i] == pid) id[i] = qid;
}
count--;
}
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int N = in.nextInt();
UF uf = new UF(N);
while (!in.hasNext("EOF")){
int p = in.nextInt();
int q = in.nextInt();
if (uf.connected(p,q)) continue;
uf.union(p,q);
System.out.println(p +" "+q);
}
System.out.println(uf.count+" components");
}
}
2.2 quick-union算法
每个触点对应的id[]元素都是同一个分量中的另一个触点的名称(也可能是他自己)-我们将这种联系称之为链接.
在实现find()方法时,我们从给定的触点开始,有他的链接得到另一个触点,再有这个触点到达第三个触点,如此继续跟随链接直到到达一个根触点,即链接指向自己的触点(你将看到必然有这样一个触点的存在)
当且仅当从两个触点开始的这个过程到达了同一个触点时他们存在于同一个连通分量之中.为了保证这个过程的有效性,我们需要union(p,q)保证这一点.他的实现很简单:我们由p和q的链接分别找到他们的根触点,然后只需将一个根触点链接到另一个根触点即可将一个分量重命名为另一个分量,因此这个算法叫做quick-union
package 基础.算法分析;
/**
* quci-union版本
*/
public class UFqu {
private final int[] id;
private int count;
public UFqu(int count) {
this.count = count;
id = new int[count];
for (int i = 0; i < count; 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);
}
public void union(int p, int q){
if (connected(p,q)) return;
int pRoot = find(p);
int qRoot = find(q);
id[pRoot]=qRoot;
count--;
}
public int count(){
return count;
}
}
2.3 加权quick-union算法
与其在union()中随意将一棵树连接到另外一棵树上,我们现在会记录每棵树的大小并将较小的树连接到较大的树之上.这项改动只需要添加一个数组和一些代码来记录树中的结点书,我们称之为加权quick-union算法
package 基础.算法分析;
import javax.security.auth.login.AccountException;
/**
* 加权qucik-union算法
*/
public class WeightQuickUnionUF {
private int [] id;//父链接数组,有触点索引
private int [] sz;//(由触点索引)哥哥根节点所对应的分量的大小
private int count;//连通分量的数量
public WeightQuickUnionUF(int count) {
this.count = count;
id = new int[count];
sz = new int[count];
for (int i = 0; i < count; i ++){
id[i]=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);
}
public void union(int p, int q){
if (connected(p,q)) return;
int pRoot = find(p);
int qRoot = find(q);
//将小树的根节点连接到大树的根节点
if (sz[pRoot] < sz[qRoot]) {
id[pRoot] = qRoot;
sz[qRoot]+=sz[pRoot];
}
else {
id[qRoot] = pRoot;
sz[pRoot]+=sz[qRoot];
};
count--;
}
}
2.4 路径压缩的加权qucik-union()算法
理想情况下,我们希望每个结点都直接链接到他的根节点.
我们接近这种理想状态的方式很简单,就是在检查结点的同时将他们直接链接到根结点.
他的实现非常简单:要实现路径压缩只需要为find()添加一个循环,将在路径上遇到的所有结点都直接链接到根节点
package 基础.算法分析;
import javax.security.auth.login.AccountException;
import java.util.ArrayList;
import java.util.List;
/**
* 压缩加权qucik-union算法
*/
public class WeightQuickUnionUF {
private int [] id;//父链接数组,有触点索引
private int [] sz;//(由触点索引)哥哥根节点所对应的分量的大小
private int count;//连通分量的数量
public WeightQuickUnionUF(int count) {
this.count = count;
id = new int[count];
sz = new int[count];
for (int i = 0; i < count; i ++){
id[i]=i;
sz[i]=1;
}
}
public int find(int p){
List<Integer> list = new ArrayList<>();
while (p != id[p]) {
list.add(p);
p = id[p];
};
for (int i : list){
id[i]=p;
}
return p;
}
public boolean connected(int p, int q){
return find(p) == find(q);
}
public void union(int p, int q){
if (connected(p,q)) return;
int pRoot = find(p);
int qRoot = find(q);
//将小树的根节点连接到大树的根节点
if (sz[pRoot] < sz[qRoot]) {
id[pRoot] = qRoot;
sz[qRoot]+=sz[pRoot];
}
else {
id[qRoot] = pRoot;
sz[pRoot]+=sz[qRoot];
};
count--;
}
}