📚 DAA Tutorial

Design and Analysis of Algorithms

Learn Design and Analysis of Algorithms concepts with simple explanations, algorithms, examples and practical notes.

← Back to DAA

Union and Find Algorithms

Union

S1 ∪ S2 = {1, 7, 8, 9, 2, 5, 10}

To perform the union, make one of the trees a subtree of the other.

17895210 52110789 Fig (a): SimpleUnion(5, 1)Fig (b): SimpleUnion(1, 5)

To obtain the union of two sets, set the parent field of one of the roots to the other root.

Data representation for S1, S2 and S3

Set namePointer (to root)
S11
S25
S33

To determine which set an element is currently in, follow the parent links to the root of its tree and use the pointer to the set name.

Represent set elements 1 to n by using an array p[1:n]. The ith element of this array represents the tree node that contains element i. A root stores −1.

i[1][2][3][4][5][6][7][8][9][10]
p−15−13−131115

Array representation for S1, S2, S3.

Find(i)

Find(i) is implemented by following the indices, starting at i, until we reach a node with parent value −1.

Find(6): P[6] = 3, then P[3] = −1. Once −1 is reached, stop and declare that 6's tree root is 3.

Simple algorithms

Algorithm SimpleUnion(i, j)
{
    p[i] := j;
}

Algorithm SimpleFind(i)
{
    while (p[i] ≥ 0) do i := p[i];
    return i;
}

Trace: In Fig (b), after SimpleUnion(1, 5), perform Find(9).

ip[i] ≥ 0 ?Action
91 ≥ 0, truei := p[9] = 1
15 ≥ 0, truei := p[1] = 5
5−1 ≥ 0, falsereturn 5

Performance of SimpleUnion and SimpleFind

The performance of SimpleUnion and SimpleFind is not very good.

Example: If q elements are in a set Si = {i}, 1 ≤ i ≤ q, then the initial configuration consists of a forest with q nodes, each a root with p[i] = −1.

Let the sequence of operations performed on the sets be:

Union(1, 2), Union(2, 3), Union(3, 4), Union(4, 5), …, Union(n−1, n), Find(1), Find(2), …, Find(n)

This sequence results in a degenerate tree.

nn−1⋮1
  • The time taken by Union is constant, so n−1 unions take O(n).
  • Find requires following a sequence of parent pointers from the element to be found to the root.
  • Since the time required to process a find for an element at level i of a tree is O(i), the total time needed to process the n finds is:
O(∑i=1ni)=O(n2)

Weighting rule for Union(i, j)

This time complexity can be improved by avoiding the creation of degenerate trees. To accomplish this, use the weighting rule for Union(i, j).

Weighting rule: If the number of nodes in the tree with root i is less than the number in the tree with root j, then make j the parent of i; otherwise make i the parent of j.

Trees obtained using the weighting rule

12…n 123…n 1234…n 12345…n 1234…n InitialUnion(1, 2)Union(1, 3)Union(1, 4)Union(1, n)

To implement the weighting rule, we maintain a count field in the root of every tree. If i is a root node, then count[i] is the number of nodes in the tree. The array stores p[i] = −count[i] for a root.

[1][2][3][4][5][6][7][8]
Initial−1−1−1−1−1−1−1−1
Union(1, 2)−21−1−1−1−1−1−1
Union(1, 3)−311−1−1−1−1−1
Union(1, 4)−4111−1−1−1−1

Algorithm WeightedUnion(i, j)

Algorithm WeightedUnion(i, j)
/* Union sets with roots i and j, i ≠ j, using the weighting rule.
   p[i] = -count[i] and p[j] = -count[j]. */
{
    temp := p[i] + p[j];
    if (p[i] > p[j]) then
    {   // i has fewer nodes
        p[i] := j;  p[j] := temp;
    }
    else
    {   // j has fewer or equal nodes
        p[j] := i;  p[i] := temp;
    }
}

Time complexity of WeightedUnion is O(1).

The height of each tree with m nodes is:

⌊log2m⌋+1

Example

n = 8 and p[i] = −count[i] = −1, 1 ≤ i ≤ 8. Let S1 = {1}, S2 = {2}, …, S8 = {8}. The sequence is:

Union(1, 2), Union(3, 4), Union(5, 6), Union(7, 8), Union(1, 3), Union(5, 7), Union(1, 5)

12345678 [−1][−1][−1][−1][−1][−1][−1][−1]
Initial height-1 trees
13572468 [−2][−2][−2][−2]
Height-2 trees following Union(1, 2), (3, 4), (5, 6), (7, 8)
1234 5678 [−4][−4]
Height-3 trees following Union(1, 3), (5, 7)
12354678 [−8]
Height-4 tree following Union(1, 5)
  • Now the time to process a find is O(logm) if there are m elements in a tree.
  • If an intermixed sequence of u union and f find operations is to be processed, it takes:
O(u+flogu)

WeightedUnion(i, j) trace

i[1][2][3][4][5][6][7][8]
p[i]−1−1−1−1−1−1−1−1initial
−21−23−25−27after Union(1,2), (3,4), (5,6), (7,8)
−4113−4557after Union(1,3) and (5,7)
Finally−81131557after Union(1,5)

Collapsing rule

Collapsing rule: If j is a node on the path from i to its root and p[j] ≠ root[i], then set p[j] to root[i].

Algorithm CollapsingFind(i)
/* Find the root of the tree containing element i.
   Use the collapsing rule to collapse all nodes from i to the root. */
{
    r := i;
    while (p[r] > 0) do r := p[r];     // find the root
    while (i ≠ r) do
    {
        s := p[i]; p[i] := r; i := s;
    }
    return r;
}

SimpleFind and CollapsingFind compared

Follow the example above to process Find(8), Find(8), …, Find(8): eight find operations on element 8.

  • In SimpleFind, Find(8) requires going up three parent link fields, for a total of 24 moves to process all 8 finds.
  • In CollapsingFind, the first Find(8) goes up three links to find the root and three more to collapse the path. Each of the remaining seven finds requires going up only one link field. The total cost is now only 13 moves.

CollapsingFind(i) trace for i = 8

Starting array: p = [−8, 1, 1, 3, 1, 5, 5, 7].

First while loop

rp[r] > 0 ?
87 > 0
75 > 0
51 > 0
1−8 ≯ 0, so r = 1

Second while loop

i ≠ rs = p[i]p[i] := ri := s
8 ≠ 1s = 7p[8] = 1i = 7
7 ≠ 1s = 5p[7] = 1i = 5
5 ≠ 1s = 1p[5] = 1i = 1
1 ≠ 1 falsereturn 1

Resulting array:

i[1][2][3][4][5][6][7][8]
p[i]−81131511
12357846
After CollapsingFind(8)

Application: connected components

Using disjoint sets, find the connected components in the undirected graph G = (V, E), where the vertices are V = {a, b, c, d, e, f, g, h, i, j} and the edges are E = {(a,c), (a,b), (e,f), (h,i), (e,g), (a,d), (e,d), (b,d), (e,a), (h,j)}. The edges are processed in the order given. List the vertices in each connected component after each step.

To use union-find to find the connected components of a graph, we can perform the following steps:

  1. Initialize each vertex as a separate component.
  2. For each edge in the graph, perform a union operation on the two components that the edge connects.
  3. After all edges have been processed, the connected components of the graph will be the disjoint sets in the union-find data structure.

Assume the graph is undirected always, if not specified as directed.

Implementation

Let S = {S1, S2, …, Sn}.

  • MAKE-SET(x): Si = {x}, add Si to S.
  • UNION(x, y): x ∈ Sx and y ∈ Sy, then S=S−Sx−Sy∪{Sx∪Sy}
  • FIND-SET(x): returns the representative of the set that contains x.

Algorithm

CONNECTED_COMPONENTS(G)
    for each vertex v ∈ G.V
        MAKE-SET(v)
    for each (u, v) ∈ G.E
        if FIND-SET(u) ≠ FIND-SET(v)
            UNION(u, v)

Trace

EdgeConnected components
–{a} {b} {c} {d} {e} {f} {g} {h} {i} {j}
(a, c){a, c} {b} {d} {e} {f} {g} {h} {i} {j}
(a, b){a, b, c} {d} {e} {f} {g} {h} {i} {j}
(e, f){a, b, c} {d} {e, f} {g} {h} {i} {j}
(h, i){a, b, c} {d} {e, f} {g} {h, i} {j}
(e, g){a, b, c} {d} {e, f, g} {h, i} {j}
(a, d){a, b, c, d} {e, f, g} {h, i} {j}
(e, d){a, b, c, d, e, f, g} {h, i} {j}
(b, d)no change
(e, a)no change
(h, j){a, b, c, d, e, f, g} {h, i, j}

Two connected components exist in the given graph: (1) {a, b, c, d, e, f, g} and (2) {h, i, j}.