\(\newcommand{\Java}{\href{http://java.com/en/}{Java}}\) \(\newcommand{\Python}{\href{https://www.python.org/}{Python}}\) \(\newcommand{\CPP}{\href{http://www.cplusplus.com/}{C++}}\) \(\newcommand{\ST}[1]{{\Blue{\textsf{#1}}}}\) \(\newcommand{\PseudoCode}[1]{{\color{blue}\textsf{#1}}}\) \(%\newcommand{\subheading}[1]{\textbf{\large\color{aaltodgreen}#1}}\) \(\newcommand{\subheading}[1]{\large{\usebeamercolor[fg]{frametitle} #1}}\) \(\newcommand{\Blue}[1]{{\color{flagblue}#1}}\) \(\newcommand{\Red}[1]{{\color{aaltored}#1}}\) \(\newcommand{\Emph}[1]{\emph{\color{flagblue}#1}}\) \(\newcommand{\Engl}[1]{({\em engl.}\ #1)}\) \(\newcommand{\Pointer}{\raisebox{-1ex}{\huge\ding{43}}\ }\) \(\newcommand{\Set}[1]{\{#1\}}\) \(\newcommand{\Setdef}[2]{\{{#1}\mid{#2}\}}\) \(\newcommand{\PSet}[1]{\mathcal{P}(#1)}\) \(\newcommand{\Card}[1]{{\vert{#1}\vert}}\) \(\newcommand{\Tuple}[1]{(#1)}\) \(\newcommand{\Implies}{\Rightarrow}\) \(\newcommand{\Reals}{\mathbb{R}}\) \(\newcommand{\Seq}[1]{(#1)}\) \(\newcommand{\Arr}[1]{[#1]}\) \(\newcommand{\Floor}[1]{{\lfloor{#1}\rfloor}}\) \(\newcommand{\Ceil}[1]{{\lceil{#1}\rceil}}\) \(\newcommand{\Path}[1]{(#1)}\) \(%\newcommand{\Lg}{\lg}\) \(\newcommand{\Lg}{\log_2}\) \(\newcommand{\BigOh}{O}\) \(\newcommand{\Oh}[1]{\BigOh(#1)}\) \(\newcommand{\todo}[1]{\Red{\textbf{TO DO: #1}}}\) \(\newcommand{\NULL}{\textsf{null}}\) \(\newcommand{\Insert}{\ensuremath{\textsc{insert}}}\) \(\newcommand{\Search}{\ensuremath{\textsc{search}}}\) \(\newcommand{\Delete}{\ensuremath{\textsc{delete}}}\) \(\newcommand{\Remove}{\ensuremath{\textsc{remove}}}\) \(\newcommand{\Parent}[1]{\mathop{parent}(#1)}\) \(\newcommand{\ALengthOf}[1]{{#1}.\textit{length}}\) \(\newcommand{\TRootOf}[1]{{#1}.\textit{root}}\) \(\newcommand{\TLChildOf}[1]{{#1}.\textit{leftChild}}\) \(\newcommand{\TRChildOf}[1]{{#1}.\textit{rightChild}}\) \(\newcommand{\TNode}{x}\) \(\newcommand{\TNodeI}{y}\) \(\newcommand{\TKeyOf}[1]{{#1}.\textit{key}}\) \(\newcommand{\PEnqueue}[2]{{#1}.\textsf{enqueue}(#2)}\) \(\newcommand{\PDequeue}[1]{{#1}.\textsf{dequeue}()}\) \(\newcommand{\Def}{\mathrel{:=}}\) \(\newcommand{\Eq}{\mathrel{=}}\) \(\newcommand{\Asgn}{\mathrel{\leftarrow}}\) \(%\newcommand{\Asgn}{\mathrel{:=}}\) \(%\) \(% Heaps\) \(%\) \(\newcommand{\Downheap}{\textsc{downheap}}\) \(\newcommand{\Upheap}{\textsc{upheap}}\) \(\newcommand{\Makeheap}{\textsc{makeheap}}\) \(%\) \(% Dynamic sets\) \(%\) \(\newcommand{\SInsert}[1]{\textsc{insert}(#1)}\) \(\newcommand{\SSearch}[1]{\textsc{search}(#1)}\) \(\newcommand{\SDelete}[1]{\textsc{delete}(#1)}\) \(\newcommand{\SMin}{\textsc{min}()}\) \(\newcommand{\SMax}{\textsc{max}()}\) \(\newcommand{\SPredecessor}[1]{\textsc{predecessor}(#1)}\) \(\newcommand{\SSuccessor}[1]{\textsc{successor}(#1)}\) \(%\) \(% Union-find\) \(%\) \(\newcommand{\UFMS}[1]{\textsc{make-set}(#1)}\) \(\newcommand{\UFFS}[1]{\textsc{find-set}(#1)}\) \(\newcommand{\UFCompress}[1]{\textsc{find-and-compress}(#1)}\) \(\newcommand{\UFUnion}[2]{\textsc{union}(#1,#2)}\) \(%\) \(% Graphs\) \(%\) \(\newcommand{\Verts}{V}\) \(\newcommand{\Vtx}{v}\) \(\newcommand{\VtxA}{v_1}\) \(\newcommand{\VtxB}{v_2}\) \(\newcommand{\VertsA}{V_\textup{A}}\) \(\newcommand{\VertsB}{V_\textup{B}}\) \(\newcommand{\Edges}{E}\) \(\newcommand{\Edge}{e}\) \(\newcommand{\NofV}{\Card{V}}\) \(\newcommand{\NofE}{\Card{E}}\) \(\newcommand{\Graph}{G}\) \(\newcommand{\SCC}{C}\) \(\newcommand{\GraphSCC}{G^\text{SCC}}\) \(\newcommand{\VertsSCC}{V^\text{SCC}}\) \(\newcommand{\EdgesSCC}{E^\text{SCC}}\) \(\newcommand{\GraphT}{G^\text{T}}\) \(%\newcommand{\VertsT}{V^\textup{T}}\) \(\newcommand{\EdgesT}{E^\text{T}}\) \(%\) \(% NP-completeness etc\) \(%\) \(\newcommand{\Poly}{\textbf{P}}\) \(\newcommand{\NP}{\textbf{NP}}\) \(\newcommand{\PSPACE}{\textbf{PSPACE}}\) \(\newcommand{\EXPTIME}{\textbf{EXPTIME}}\)
\(\newcommand{\PQInsertF}{\textsf{insert}}\) \(\newcommand{\PQInsert}[1]{\PQInsertF(#1)}\) \(\newcommand{\PQMaxF}{\textsf{max}}\) \(\newcommand{\PQMax}{\PQMaxF()}\) \(\newcommand{\PQRemoveF}{\textsf{remove-max}}\) \(\newcommand{\PQRemove}{\PQRemoveF()}\) \(\newcommand{\PQIncreaseF}{\textsf{increase-priority}}\) \(\newcommand{\PQIncrease}[2]{\PQIncreaseF(#1,#2)}\) \(\newcommand{\PQDecreaseF}{\textsf{decrease-priority}}\) \(\newcommand{\PQDecrease}[2]{\PQDecreaseF(#1,#2)}\) \(\renewcommand{\UFMS}[1]{\textsf{make-set}(#1)}\) \(\renewcommand{\UFFS}[1]{\textsf{find-set}(#1)}\) \(\renewcommand{\UFCompress}[1]{\textsf{find-and-compress}(#1)}\) \(\renewcommand{\UFUnion}[2]{\textsf{union}(#1,#2)}\) \(\newcommand{\UFParent}[1]{{#1}.\textsf{parent}}\) \(\newcommand{\UFNil}{\textsf{nil}}\) \(\newcommand{\UFRank}[1]{{#1}.\textsf{rank}}\) \(\newcommand{\PParam}{s}\)

Disjoint-sets with the union-find data structure

Another example of using trees as a data structure is representing disjoint sets. Such a disjoint sets data structure, or a union-find data structure, maintains a partitioning of a finite set \(S\) of elements into mutually disjoint subsets \(S_1,...,S_k\), meaning that

  • \(S_1 \cup ... \cup S_k = S\) and

  • \(S_i \cap S_j = \emptyset\) for all \(1 \le i < j \le k\).

In the beginning, each element \(a\) is in its own singleton set \(\Set{a}\). Given two elements \(x\) and \(y\), one can merge the sets \(S_x\) and \(S_y\) holding \(x\) and \(y\) into their union \(S_x \cup S_y\). A representative element is defined for each set and, for each element in the set, one can find this representative. Therefore, one can check whether two elements are in the same set by testing whether their representative elements are the same.

The abstract data type

The abstract data type for disjoint sets has the following methods:

  • \(\UFMS{x}\) introduces a new element \(x\) and puts it in its own singleton set \(\Set{x}\).

  • \(\UFFS{x}\) finds the current representative of the set that contains \(x\). Two elements \(x\) and \(y\) are in the same set if and only if their representatives are the same, i.e. when \(\UFFS{x} = \UFFS{y}\).

  • \(\UFUnion{x}{y}\) merges the sets of \(x\) and \(y\) to one set. This may change the representative of the elements in the resulting set.

Example: Testing whether an undirected graph is connected

Consider the graph below.

_images/graph-ex3.png

Let’s use a disjoint sets data structure to find out whether the graph is connected.

  1. Initialize the disjoint-sets data structure by calling \(\UFMS{x}\) for each \(x \in \Set{a,b,c,d,e,f,g}\).

    The disjoint sets are now \(\Set{a},\Set{b},\Set{c},\Set{d},\Set{e},\Set{f},\Set{g}\).

  2. Next call \(\UFUnion{x}{y}\) for each edge \(\Set{x,y}\) in the graph.

    After each call, two vertices \(x'\) and \(y'\) are in the same set if it is possible to reach \(x'\) and \(y'\) from each other by using only the edges considered so far.

    For instance, after calling \(\UFUnion{a}{b}\) and \(\UFUnion{a}{e}\) because of the edges \(\Set{a,b}\) and \(\Set{a,e}\), the disjoint-sets are \(\Set{a,b,e},\Set{c},\Set{d},\Set{f},\Set{g}\).

  3. At the end, the sets are \(\Set{a,b,e,f}\) and \(\Set{c,d,g}\).

    As there is more than one set, the graph is not connected.

    Two vertices \(x\) and \(y\) belong to the same connected component of the graph if \(\UFFS{x} = \UFFS{y}\).

The data structure and operations on it

Forests of rooted trees provide a simple way to represent disjoint sets. The idea is that each set is represented as a tree, the nodes in a tree containing the elements in the set. The root of the tree is the representative element of the set represented by that tree.

As the algorithms for the operations will only traverse the trees upwards, from nodes towards the root, we only need the parent reference for each node.

Example

Suppose that we have introduced the elements \(a,b,...,f\) with \(\UFMS{}\) and then made unions so that the current sets are \(\Set{a,c,d,e}\) and \(\Set{b,f}\). The graph below shows one possible representation for the current situation. The representatives of the two sets are \(e\) and \(f\), respectively.

_images/uf-ex1-e.png

The make-set operation

Introducing a new element in the set data structure is easy: just introduce a new node that has no parent and is not a parent of any other node. As pseudocode:

// Insert the element \(x\) as a singleton set if not already in any set \(\UFMS{x}\): if \(x\) is not yet in any set: insert \(x\) in some data structure recording the elements in the sets \(\UFParent{x} \Asgn \UFNil\) // as a field or with a map

As mentioned earlier, we’ll only traverse the trees upwards and thus child references are not needed. For recording which elements appear in any of the sets (line 2 above) and associating elements to parents etc, one can use some standard map data structure (Scala’s HashMap or TreeMap, for instance) whose implementations are discussed in the next rounds of the course.

The find-set operation

To find the representative element for an element \(x\), we just

  • ​ find the node of \(x\), and

  • ​ traverse the parent pointers upwards to the root of the tree. The element at the root of the tree is the (current) representative of \(x\).

As pseudocode:

// Find the representative of the set containing \(x\) \(\UFFS{x}\): raise an error if \(x\) is not in any set while \(\UFParent{x} \neq \UFNil\): \(x \Asgn \UFParent{x}\) return \(x\)

The union operation

Merging the sets of two elements to their union is quite easy as well:

  • ​ First, find the roots of the trees containing the two elements.

  • ​ If the roots are not the same, meaning that the elements are not yet in the same set, make the other tree to be a subtree of the another by making the parent pointer of its root to point to the root of the another tree.

As pseudocode

\(\UFUnion{x}{y}\): Check that \(y\) and \(y\) are in some set \(x' \Asgn \UFFS{x}\) // Find the root of the tree with \(x\) \(y' \Asgn \UFFS{y}\) // Find the root of the tree with \(y\) if \(x' \neq y'\): // Not yet in the same set? \(\UFParent{x'} = y'\) // Merge the trees by making \(x'\) a subtree of \(y'\)

Example: Inserting elements and making unions

  • ​ First, call \(\UFMS{}\) for the elements \(a,...,f\). The forests are as follows.

    _images/uf-ex1-a.png
  • ​ After calling \(\UFUnion{a}{d}\):

    _images/uf-ex1-b.png
  • ​ After calling \(\UFUnion{c}{a}\):

    _images/uf-ex1-c.png
  • ​ After calling \(\UFUnion{b}{f}\):

    _images/uf-ex1-d.png
  • ​ After calling \(\UFUnion{c}{e}\):

    _images/uf-ex1-e.png
  • ​ After calling \(\UFUnion{d}{b}\):

    _images/uf-ex1-f.png

As one may expect, the basic versions of the operations given above do not guarantee the desired efficiency. In the worst case, the trees degenerate to “linear” trees that resemble linked lists and the find and union operations take linear time.

Example: Worst-case behaviour

Consider the following initial situation.

_images/uf-ex1-a.png

The result of applying \(\UFUnion{a}{b}\), \(\UFUnion{a}{c}\), \(\UFUnion{a}{d}\), \(\UFUnion{a}{e}\), and \(\UFUnion{a}{f}\) is the “linear” tree shown below.

_images/uf-ex2-b.png

Balancing with ranks

The problem with the tree in the previous example is that it is not “balanced”. If we can force the trees to be (at least roughly) balanced trees so that non-leaves have at least two children and the subtrees rooted at the children are roughly of the same size, a tree with \(n\) elements would have height \(\Oh{\Lg{n}}\) and the find-set and union operations would run in logarithmic time. Fortunately, there is a relatively easy way to do this:

  • ​ Associate each node with a rank which is an upper bound of the height of the subtree rooted at the node,

  • ​ When making an union, make the tree with smaller rank to be a child of the other one (if the ranks are the same, choose arbitrarily).

The updated pseudocode for the make-set operation is

// Insert the element \(x\) as a singleton set if not already in any set \(\UFMS{x}\): if \(x\) is not yet in any set: insert \(x\) in some data structure recording the elements in the sets \(\UFParent{x} \Asgn \UFNil\) // as a field or with a map \(\UFRank{x} \Asgn 0\) // as a field or with a map

and for the union operation:

\(\UFUnion{x}{y}\): Check that \(y\) and \(y\) are in some set \(x' \Asgn \UFFS{x}\) // Find the root of the tree with \(x\) \(y' \Asgn \UFFS{y}\) // Find the root of the tree with \(y\) if \(x' \neq y'\): // Not yet in the same set? if \(\UFRank{x'} > \UFRank{y'}\): // Subtree at \(x'\) has more height? \(\UFParent{y'} \Asgn x'\) // Merge the trees by making \(y'\) a subtree of \(x'\) else: \(\UFParent{x'} \Asgn y'\) // Merge the trees by making \(x'\) a subtree of \(y'\) if \(\UFRank{x'} = \UFRank{y'}\): \(\UFRank{y'} \Asgn \UFRank{y'}+1\)

Example

Replay of our earlier example but now with ranks (shown besides the nodes):

  • ​ In the beginning.

    _images/uf-rex1-a.png
  • ​ After calling \(\UFUnion{a}{d}\)

    _images/uf-rex1-b.png
  • ​ After calling \(\UFUnion{c}{a}\)

    _images/uf-rex1-c.png
  • ​ After calling \(\UFUnion{b}{f}\)

    _images/uf-rex1-d.png
  • ​ After calling \(\UFUnion{c}{e}\)

    _images/uf-rex1-e.png
  • ​ After calling \(\UFUnion{d}{b}\)

    _images/uf-rex1-f.png

Example

Let’s replay of our earlier worst-case example but now with ranks. The result of applying \(\UFUnion{a}{b}\), \(\UFUnion{a}{c}\), \(\UFUnion{a}{d}\), \(\UFUnion{a}{e}\), and \(\UFUnion{a}{f}\) is no longer a linear tree but a very shallow one.

_images/uf-rankex1-b.png

Theorem

With ranks, the height of each tree in the forest is at most \(\Floor{\Lg{\PParam}}\), where \(\PParam\) is the number of nodes in the tree.

Proof

By induction on the performed operations.

Induction base: the claim holds after \(0\) operations as there no trees in the forest.

Induction hypothesis: assume that the claim holds after \(m\) operations.

Induction step:

  • ​ If we next perform a find operation, the trees do not change and thus the claim holds for \(m+1\) operations.

  • ​ If we insert a new element, we insert a new isolated node (a tree with \(1\) node and of height \(0 = \Floor{\Lg 1}\)) and the other trees stay the same. As any other tree was of height \(\Floor{\Lg{\PParam}}\) or less by the induction hypothesis, the tree is of height \(\Floor{\Lg(\PParam+1)}\) or less after the operation.

  • ​ If we make union operation on \(x\) and \(y\), we have two cases.

    1. If the elements are in the same set, the trees stays unmodified.

    2. If the elements are in different sets of sizes \(\PParam_x\) and \(\PParam_y\), then the heights of the corresponding trees are, by induction hypothesis, \(h_x \le \Floor{\Lg{\PParam_x}}\) and \(h_y \le \Floor{\Lg{\PParam_y}}\).

      • ​ If \(h_x < h_y\), then the resulting tree has \(\PParam_x + \PParam_y\) nodes and height \(h_y\). As \(h_y \le \Floor{\Lg{\PParam_y}}\), \(h_y \le \Floor{\Lg(\PParam_x + \PParam_y)}\).

      • ​ The case \(h_y < h_x\) is symmetric to the previous one.

      • ​ If \(h_x = h_y\), then the resulting tree has \(\PParam' = \PParam_x + \PParam_y\) nodes and height \(h' = h_x + 1 = h_y + 1\).

        If \(\PParam_x \le \PParam_y\), then \(\PParam' \ge 2\PParam_x\) and \(\Floor{\Lg(\PParam')} \ge \Floor{\Lg(2 \PParam_x)} = \Floor{\Lg(2) + \Lg \PParam_x} = 1+\Floor{\Lg \PParam_x} = h'\).

        Similarly, if \(\PParam_x \ge \PParam_y\), then \(\PParam' \ge 2\PParam_y\) and \(\Floor{\Lg(\PParam')} \ge \Floor{\Lg(2 \PParam_y)} = \Floor{\Lg(2) + \Lg \PParam_y} = 1+\Floor{\Lg \PParam_y} = h'\).

Therefore, the claim holds after \(m+1\) operations and by the induction principle, the claim holds for all values \(m\).

Let \(n\) be the number of elements in the current disjoint sets. Assume that we can

  1. find the node of each element in \(\Oh{\Lg{n}}\) time, and

  2. access and modify the parent and rank values in constant time.

Under these assumptions and by using ranks, the make-set, find-set and union operations take \(\Oh{\Lg{n}}\) time.

Path compression

Path compression is another idea for making the trees even shallower. As it name implies, it compresses the paths from the nodes to the roots during the find-set operations. We perform a (recursive) search to find the root from a node, and after each recursive call update the parent of the current node to be the root. As pseudocode:

// Find the representative of the set containing \(x\) \(\UFFS{x}\): raise an error if \(x\) is not in any set def \(\UFCompress{y}\): if \(\UFParent{y} = \UFNil\): // In the root? return \(y\) else \(r \Asgn \UFCompress{\UFParent{y}}\) \(\UFParent{y} \Asgn r\) return \(r\) return \(\UFCompress{x}\)

Observe that the find-set operation is also called during the union operation, and thus path compression may occur there as well.

Example

Assume that we have the disjoint-sets forest shown below.

_images/uf-ex1-e.png

When we call \(\UFFS{c}\), the forest is modified to illustrated below.

_images/uf-ex1-e-compress.png

Theorem

With union-by-rank and path compression, making \(m\) disjoint-set operations on \(n\) elements takes \(\Oh{m \alpha(m)}\) time, where \(\alpha\) is a very slowly growing function with \(\alpha(m) \le 4\) for all \(m \le 2^{2048}\).

Proof

See Section 21.4 in Introduction to Algorithms (Aalto access). The proof is not required in the course.