Six Strangers

Imagine six people arriving at a conference. Nobody knows anyone yet — six strangers in a room. Each one is wearing a name badge with their own name on it. Alice's badge says “Alice.” Bob's badge says “Bob.” Simple. Unremarkable. You barely even notice the badges because of course they say the right names. That normalcy is about to become the most important detail in the room.

You're running registration, and you need a data structure that can answer one question fast: are these two people in the same group? Not “which group are they in.” Not “how big is the group.” Just: same group, yes or no. Union-Find solves this, and it does it with nothing more than a parent[] array — a list of pointers that encode group membership through chains of references.

Here's the core idea. Each slot in the parent array stores a pointer to a parent node in a tree. To find which group someone belongs to, you follow parent pointers until you reach a node that points to itself — a self-loop. That self-referencing node is the root, the group's representative. Think of it as following a chain of introductions at the conference: “Oh, you want the group leader? Talk to Marcus.” You go to Marcus. “Not me — talk to Dana.” You go to Dana. Dana says “That's me.” Done. Dana is the root.

Six isolated people means six separate groups — nobody has been introduced yet. Here's a constructor that tries to set that up:

1
constructor(n: number) {
2
  this.parent = new Array(n).fill(0);
3
  // parent = [0, 0, 0, 0, 0, 0]
4
}

When I first saw parent[i] = i written as the correct initialization, I thought it was a placeholder — like something the real logic would overwrite later. Why would you set a value to its own index? That can't be doing anything useful. It took me an embarrassingly long time to realize that IS the real initialization. Every node pointing to itself means every node is its own root, its own group leader. But we'll get there.

First, look at the buggy version above. new Array(n).fill(0) fills every slot with the number 0. Read that carefully. Node 0 points to 0 — fine, that's a self-loop, node 0 is its own root. But node 1 also points to 0. Node 2 points to 0. Node 3 points to 0. It's as if you walked through the conference before it started and crossed out everyone's name badge, replacing each one with “Node 0.” Five strangers now appear to follow node 0, even though nobody has met. Nobody was introduced. Nobody agreed to join a group. The data structure just silently claimed they're all together.

If you trace find(3), you follow parent[3] to node 0, then check parent[0] — it's 0, a self-loop, so node 0 is the root. You've arrived at an answer, but it's the wrong answer. Node 3 shouldn't be in node 0's group yet. Same for nodes 1, 2, 4, and 5. Every query returns the same root. Every node appears connected. The entire forest has been collapsed into one tree before a single union() was called.

012345
parent =[0, 0, 0, 0, 0, 0]

Every node points to 0 — one accidental mega-group.

This bug is subtle because the array looks perfectly reasonable. Every slot has a value. No nulls, no undefined, no garbage. If you squint at [0, 0, 0, 0, 0, 0], it could even seem intentional — maybe 0 is some kind of default state? But the semantics are completely wrong. The difference between “all zeros” and “each index holds itself” is the difference between “one group” and “six groups.” The values look similar. The meaning is opposite.

Can you trace it? Below, you'll get to pick nodes and watch where find() takes them. Watch for that sinking convergence — every path leading to the same place.

The self-loop

Phase: Trace the bug in the broken forest
6 strangers arrive at a conference. Each should be in their own group. But something is wrong with the initialization...
Tap any node (except 0) to trace find() and follow its parent pointers
012345

The Root Marker

You just watched every trace converge on node 0. That sinking feeling — “wait, there's only one group?” — is exactly what happens when initialization is wrong. The structure looks fine. It runs without errors. But every query returns the same answer, which means the data structure is useless before it even starts.

The fix is a single convention: every node starts by pointing to itself. Back at the conference, every stranger's badge says their own name — Alice is “Alice,” Bob is “Bob,” nobody has been claimed by anyone else. Each person is their own group leader because nobody has introduced them yet.

1
constructor(n: number) {
2
  this.parent = Array.from({ length: n }, (_, i) => i);
3
  // parent = [0, 1, 2, 3, 4, 5]
4
}

Let me walk through this line. Array.from({ length: n }, callback) creates an array of length n and runs the callback for each index. The callback (_, i) => i takes two arguments — the first is the value at that position (which doesn't exist yet, so it's undefined and we ignore it with _), and the second is the index. The callback returns i itself as the value. So position 0 gets 0, position 1 gets 1, position 2 gets 2. Each slot holds its own index. Each person's badge says their own name.

But wait — why a self-loop? It seems like an odd design choice. Why not mark roots with -1? You could check if (parent[x] === -1) return x and it would work. Or you could keep a separate isRoot[] boolean array. That would be even more explicit. Both approaches are valid. Both would produce correct results.

The self-loop is better because it's doing double duty. It's both the initial state (everyone starts as their own leader) and the termination condition for find() (stop walking when parent[x] === x). One value carries two meanings simultaneously. You don't need a special check for “is this a root?” — the ordinary pointer-following logic naturally stops at self-loops. The sentinel and the data are the same thing.

There's something deeper here too. The self-loop isn't just an implementation trick — it encodes a semantic truth. When parent[3] === 3, it means “node 3's representative is node 3.” That's not a special case. That's the base case of the recursive definition: every node is represented by its root, and the root is represented by itself. The self-loop makes the recursive structure well-founded without any special casing.

Compare the two arrays side by side:

  • [0, 0, 0, 0, 0, 0] — every badge says "Node 0." One giant accidental group.
  • [0, 1, 2, 3, 4, 5] — every badge says the wearer's own name. Six independent people.
012345

Tap a node to trace find()

Without the self-loop convention, every chain collapses to node 0 and the entire forest is one giant tree from the start. With it, you get exactly what the conference scenario demands: six independent people, ready to be merged on your terms, through explicit union() calls that you control.

This one convention — parent[i] = i — is the foundation of everything that follows. The self-loop is the root marker, the identity state, and the find() base case, all in one. It's the kind of idea that seems trivial once you understand it but causes silent data corruption if you get it wrong.

Next: what happens when you start merging these isolated trees?