Algorithm Search⌘K

Proofs: Persistent union-find

These are the human proofs of Persistent union-find, at full length.

The persistent union-find gives every version its own partition full

This proof shows: correctness.

Setting. Each persistent array version tt has fixed contents cont(t)\text{cont}(t) in the abstract model: the first version holds its initial list, and SET(t,i,v)(t, i, v) returns a version whose contents are cont(t)\text{cont}(t) with slot ii set to vv. A handle hh denotes the partition P(h)P(h) whose blocks are the trees of the forest cont(h.parent)\text{cont}(h.\text{parent}).

Lemma 1 (rerooting). Let the diffs and the shared array AA of one family satisfy: the Arr version t∗t_* has cont(t∗)=A\text{cont}(t_*) = A, and each Diff(i,v,t′)\text{Diff}(i, v, t') version tt has cont(t)=cont(t′)\text{cont}(t) = \text{cont}(t') with slot ii set to vv. Then after REROOT(t)(t) the same holds, tt is the Arr version, and no version has changed contents.

Proof. We also keep the fact that the Diff pointers form a tree whose root is the Arr version; it holds for a single version, SET puts a new root above the old one, and each step below reverses one edge of the path to the root, which keeps a tree with the new root. So the path t0,…,tmt_0, \ldots, t_m of line A1 ends at the Arr version. Consider the step for jj (lines A3 to A6). Before it, tj+1t_{j+1} is Arr with A=cont(tj+1)A = \text{cont}(t_{j+1}), and tj=Diff(i,vj,tj+1)t_j = \text{Diff}(i, v_j, t_{j+1}). After it, AA has slot ii set to vjv_j, which is cont(tj)\text{cont}(t_j), and tjt_j is Arr. The version tj+1t_{j+1} becomes Diff(i,a,tj)\text{Diff}(i, a, t_j) with aa the old A[i]A[i]; since cont(tj)\text{cont}(t_j) and cont(tj+1)\text{cont}(t_{j+1}) differ at most in slot ii, the contents of tj+1t_{j+1} equal cont(tj)\text{cont}(t_j) with slot ii set to aa. No other version changes. So the hypothesis holds after each step, and at the end t=t0t = t_0 is Arr. □\square

Corollary 2. GET(t,i)(t, i) returns slot ii of cont(t)\text{cont}(t), and SET(t,i,v)(t, i, v) returns a version with the contents of the model, while no existing version changes contents.

Proof. GET reroots and reads A=cont(t)A = \text{cont}(t). SET reroots, makes t′t' the Arr version, writes vv into AA so that A=cont(t′)A = \text{cont}(t'), and turns tt into Diff(i,a,t′)\text{Diff}(i, a, t') with aa the old value, which keeps the hypothesis of Lemma 1. (When vv equals the old value, the implementation returns tt itself, which has the right contents.) □\square

By Corollary 2 we can reason about each handle as if its arrays were plain arrays with the contents cont\text{cont}, which no later operation changes, except where a line writes to a handle field.

Invariant. A handle is good if its forest F=cont(h.parent)F = \text{cont}(h.\text{parent}) and ranks R=cont(h.rank)R = \text{cont}(h.\text{rank}) satisfy: the roots of FF are its fixed points, and R[F[x]]>R[x]R[F[x]] > R[x] for each non-root xx.

Lemma 3 (find). On a good handle, FIND(h,x)(h, x) returns the root of the tree of xx in FF, and afterwards hh is good and P(h)P(h) is unchanged.

Proof. The walk of line L2 visits nodes of strictly growing rank, so it ends, at a fixed point rr, which is the root of the tree of xx. The loop of line L4 visits the same nodes x=y0,y1,…,yk=rx = y_0, y_1, \ldots, y_k = r and sets the parent of each yjy_j with j<kj < k to rr. Each yjy_j stays in the tree of rr, every other node keeps its parent, and the subtrees below each yjy_j move with it, so the trees, and hence the blocks, are the same. The ranks along the walk grow strictly, so R[r]>R[yj]R[r] > R[y_j] for j<kj < k, and the invariant holds for the new parent of each yjy_j. Line L5 stores the new parent array, so hh is good and P(h)P(h) is unchanged. □\square

Lemma 4 (union). On a good handle, UNION(h,x,y)(h, x, y) returns a good handle h′h' with P(h′)P(h') equal to P(h)P(h) with the blocks of xx and yy merged, and P(h)P(h) is unchanged.

Proof. By Lemma 3, line L7 gives the roots rxr_x and ryr_y and leaves P(h)P(h) as it was. If they are equal (line L8), xx and yy share a block and hh is the answer. Otherwise lines L10 to L12 set the parent of one root to the other root, which joins the two trees into one and leaves all other trees alone, so the blocks of xx and yy are merged. If the ranks differ, the new child has the smaller rank, so the invariant holds. If they are equal, the new parent's rank goes up by one, which keeps the invariant at the new child and at the old children of rxr_x. All writes are SET calls, which make new versions by Corollary 2, so hh keeps its contents. □\square

Theorem. For every sequence of operations, every handle is good and denotes the partition that the abstract type gives to it, and FIND and SAME return answers that agree with that partition, on every version.

Proof. MAKE(n)(n) gives the forest of nn roots with rank 0, which is good and denotes the singletons. By Lemmas 3 and 4, by induction on the number of operations, every handle is good and denotes the right partition. The only write to an existing handle is line L5, which by Lemma 3 does not change the partition, so no later answer on any version changes. FIND returns a root of the tree of xx; two elements share a root exactly when they share a tree, so SAME is correct. ■\blacksquare

Back to Persistent union-find