For the complete documentation index, see llms.txt. This page is also available as Markdown.

19. Data Structures for Disjoint Sets

Exercises

19.1-1

Use the Data Structure Visualizations tool that preinitializes 16 disjoint sets as a starting point. Each member is denoted by an integer. For this example, just assign zero based indices to vertices. For example, 'a' would be represented with 0, 'b' with 1, etc. You would only use the first 11 elements (0..10). To process an edge convert vertices to indices. For example, the first edge (d,i)(d,i) would be a union between 3 and 8. You should end up with 3 disjoint sets (notice that 2 ('c') is a singleton set).

19.1-2

Available in the latest revision of the IM.

19.1-3

Available in the latest revision of the IM.

19.2-1

Available in the latest revision of the IM.

19.2-2

Available in the latest revision of the IM.

19.2-3

Available in the latest revision of the IM.

19.2-4

Available in the latest revision of the IM.

19.2-5

Available in the latest revision of the IM.

19.2-6

Available in the latest revision of the IM.

19.3-1

Available in the latest revision of the IM.

19.3-2

Available in the latest revision of the IM.

19.3-3

Available in the latest revision of the IM.

19.3-4 🌟

Available in the latest revision of the IM.

★ 19.3-5

Available in the latest revision of the IM.

The worst-case O(mα(n))O(m \alpha(n)) bound in the standard disjoint-set forest (see Section 19.4) happens specifically because we can alternate between operations. We can flatten a tall tree using FIND-SET, and then immediately "bury" that newly flattened tree by using a LINK to attach its root to an even taller tree. By constantly mixing LINK and FIND-SET, we force the algorithm to repeatedly traverse and re-compress paths that keep growing.

When we front-load all the LINK operations, we completely remove the ability to "bury" nodes. This is exactly how that brings the time bound down to strictly O(m)O(m):

19.4-1

Available in the latest revision of the IM.

19.4-2

Available in the latest revision of the IM.

There is a simpler proof. After proving the claim that for all tree roots xx, we have x.size2x.rankx.size \ge 2^{x.rank}, it immediately follows that nx.size    x.ranklgn    x.ranklgnn \ge x.size \implies x.rank \le \lg n \implies x.rank \le \rfloor \lg n \rfloor.

19.4-3

Available in the latest revision of the IM.

19.4-4

Available in the latest revision of the IM.

19.4-5

Available in the latest revision of the IM.

19.4-6

Available in the latest revision of the IM.

★ 19.4-7

Available in the latest revision of the IM.

Problems

19-1 Offline minimum

Available in the latest revision of the IM.

19-2 Depth determination

Available in the latest revision of the IM.

19-3 Tarjan’s offline lowest-common-ancestors algorithm

a.

The LCA procedure executes a depth-first search traversal of the rooted tree TT. Consequently, each node is visited exactly once, and any given pair is examined only twice, once in each direction. During the first examination, the other element of the pair is still WHITE (not yet visited). Only upon the second examination are both elements colored BLACK, at which point their lowest common ancestor is printed.

b.

This follows from the DFS traversal. Lines 1–2 create a singleton set before recursing into any children. Children at the same level, sharing a parent, are processed sequentially. Line 5 merges each child’s set with the parent’s set before the next child is processed; therefore, the number of sets remains fixed throughout the level.

Because siblings are merged and only the nodes on the active DFS path remain unmerged, the only existing sets correspond exactly to uu and its ancestors. By definition, the number of ancestors is the depth of uu.

c.

At the exact moment LCA(u) is evaluating the pairs, the tree is divided into disjoint sets. For any fully processed (BLACK) node vv, FIND-SET(v).ancestor points to the lowest node on the current active recursion stack that is an ancestor of vv. Because node uu is the one currently being processed, the active recursion stack consists perfectly of uu and all of its ancestors. Therefore, the lowest node on the stack that is also an ancestor of vv is, by definition, the lowest common ancestor of uu and vv.

Let ww be the lowest common ancestor of uu and vv. Because line 10 only triggers when uu is currently executing and vv is already BLACK, we know ww finished its execution before uu.

This leaves exactly two possible structural scenarios:

Scenario 1: vv is a proper descendant of uu In this case, the lowest common ancestor is uu itself (w=uw = u).

  • Because vv is a descendant, the DFS traveled down from uu into one of its child branches and completely processed vv.

  • As the recursion unspooled back up that branch toward uu, line 5 (UNION) continuously merged the child sets into the parent sets.

  • Line 6 immediately reset the ancestor pointer of those newly merged sets to the parent.

  • Because we are currently inside LCA(u), the child branch containing vv has just finished. The set containing vv has been merged into uu's set, and its ancestor has been set to uu.

  • Thus, FIND-SET(v).ancestor correctly returns uu.

Scenario 2: uu and vv are in different branches of ww In this case, ww sits somewhere above uu.

  • The DFS reached ww first, then traveled down one child's branch and completely processed vv (coloring it BLACK).

  • The recursion returned to ww. At this moment, line 5 merged the entire branch containing vv into ww's set, and line 6 explicitly set the ancestor of that combined set to ww.

  • The DFS then moved to a different child of ww and traveled down until it reached uu.

  • While we are evaluating pairs inside LCA(u), the recursive call LCA(w) is still active on the stack waiting for uu's branch to finish. Because ww has not finished, it has not yet been merged into its own parent.

  • Therefore, the highest union that vv has participated in stopped exactly at ww.

  • Thus, FIND-SET(v).ancestor correctly returns ww.

d.

We have a sequence of T+P|T|+|P| disjoint-set operations resulting in O((T+P)α(T))O((|T|+|P|)\alpha(|T|)) time. This dominates the overall running time, since the rest of the DFS traversal takes less time.

Last updated