在之前的文章中,讨论了并查集的朴素版本
// Naive implementation of find
static int find(int parent[], int i)
{
if (parent[i] == -1)
return i;
return find(parent, parent[i]);
}
// Naive implementation of union()
static void Union(int parent[], int x, int y)
{
int xset = find(parent, x);
int yset = find(parent, y);
parent[xset] = yset;
}
上面的 u n i o n ( ) union() union()和 f i n d ( ) find() find()是朴素的,最坏情况下的时间复杂度是线性的。为表示子集而创建的树可以倾斜,并且可以变得像一个链表。以下是最坏情况的示例
Let there be 4 elements 0, 1, 2, 3
Initially, all elements are single element subsets.
0 1 2 3
Do Union(0, 1)
1 2 3
/
0
Do Union(1, 2)
2 3
/
1
/
0
Do Union(2, 3)
3
/
2
/
1
/
0
- 上述操作在最坏情况下可以优化为 O ( L o g n ) O(Log n) O(Logn)。这个想法是总是在更深的树的根下附加更小的深度树。这种技术称为union by rank。
- 首选术语秩rank而不是高度height,因为如果使用路径压缩技术(我们在下面讨论),那么秩rank并不总是等于高度height。
- 此外,树的大小size(代替高度height)也可以用作秩rank。使用大小size作为秩rank也会产生 O(Logn) 的最坏情况时间复杂度。
Let us see the above example with union by rank
Initially, all elements are single element subsets.
0 1 2 3
Do Union(0, 1)
1 2 3
/
0
Do Union(1, 2)
1 3
/ \
0 2
Do Union(2, 3)
1
/ | \
0 2 3
- 对朴素方法的第二个优化是Path Compression。这个想法是在调用**find()**时压平树。当为元素 x x x调用find(),返回树的根。在find() 操作从 x x x出发找到根。
- 路径压缩Path Compression的想法是将找到的根作为 x x x 的父节点,这样我们就不必再次遍历所有中间节点。如果 x 是子树的根,那么 x 下所有节点的路径(到根)也会压缩。
Let the subset {0, 1, .. 9} be represented as below and find() is called
for element 3.
9
/ | \
4 5 6
/ \ / \
0 3 7 8
/ \
1 2
When find() is called for 3, we traverse up and find 9 as representative
of this subset. With path compression, we also make 3 as the child of 9 so
that when find() is called next time for 1, 2 or 3, the path to root is reduced.
9
/ / \ \
4 5 6 3
/ / \ / \
0 7 8 1 2
秩rank与路径压缩Path Compression带来的好处也很明显,每个操作的时间复杂度变得甚至小于 O(Logn),摊销的时间复杂度甚至能达到常数级别
Code:
static class Graph {
int V, E;//顶点与边的数量
Edge[] edges;//边
//初始化
public Graph(int V, int E) {
this.V = V;
this.E = E;
edges = new Edge[E];
for (int i = 0; i < E; i++) {
edges[i] = new Edge();
}
}
class Edge {
int u, v;//边的两个点
}
class Subset {
int parent;
int rank;
}
//路径压缩找到i的子集中的根节点
int find(Subset[] subsets, int i) {
if (subsets[i].parent != i) {
subsets[i].parent = find(subsets, subsets[i].parent);
}
return subsets[i].parent;
}
//按秩进行合并
void union(Subset[] subsets, int x, int y) {
int xroot = find(subsets, x);
int yroot = find(subsets, y);
if (subsets[xroot].rank < subsets[yroot].rank) subsets[xroot].parent = yroot;
else if (subsets[xroot].rank > subsets[yroot].rank) subsets[yroot].parent = xroot;
else {
subsets[xroot].parent = yroot;
subsets[yroot].rank++;
}
}
int isCycle(Graph graph) {
//初始化subsets
int V = graph.V, E = graph.E;
Subset[] subsets = new Subset[V];
for (int v = 0; v < V; v++) {
subsets[v] = new Subset();
subsets[v].parent = v;//根节点是其自身
subsets[v].rank = 0;//初始化时秩为0
}
//遍历每一条边
for (int e = 0; e < E; e++) {//边的索引
Edge edge = graph.edges[e];//当前的边
int x = edge.u, y = edge.v;//分别找到x y 的根节点
int xroot = find(subsets, x);
int yroot = find(subsets, y);
if (xroot == yroot) return 1;//出现根一样的,出现了环
union(subsets, xroot, yroot);
}
return 0;
}
public static void main(String[] args) {
/* Let us create the following graph
0
| \
| \
1-----2 */
int V = 3, E = 3;
Graph graph = new Graph(V, E);
// add edge 0-1
graph.edges[0].u = 0;
graph.edges[0].v = 1;
// add edge 1-2
graph.edges[1].u = 1;
graph.edges[1].v = 2;
// add edge 0-2
graph.edges[2].u = 0;
graph.edges[2].v = 2;
if (graph.isCycle(graph) == 1)
System.out.println("Graph contains cycle");
else
System.out.println("Graph doesn't contain cycle");
}
}