How to model the dynamic-connectivity problem using union-find?
Maintain disjoint sets that correspond to connected components
union(2, 5)
union-find data type (api)
Goal: design an efficient union-find data type
number of elements \(N\) can be huge
number of operations \(M\) can be huge
union and find operations can be intermixed
public class UF {
// initialize union-find data structure with N singleton sets (0 to N-1)
UF(int N) { ... }
// merge sets containing elements p and q
void union(int p, int q) { ... }
// identifier for set containing element p (0 to N-1)
int find(int p) { ... }
}
dynamic-connectivity client
read in number of elements \(N\) from standard input
repeat:
read in pair of integers from standard input
if they are not yet connected, connect them and print pair
public static void main(String[] args) {
int N = StdIn.readInt();
UF uf = new UF(N);
while(!StdIn.isEmpty()) {
int p = StdIn.readInt();
int q = StdIn.readInt();
if(uf.find(p) != uf.find(q)) {
uf.union(p, q);
StdOut.println(p + " " + q);
}
}
}
dynamic-connectivity client
Note with input below, lines 7, 11, and 12 (highlighted) are already connected and therefore will not print.
Input:
10
4 3
3 8
6 5
9 4
2 1
8 9
5 0
7 2
6 1
1 0
6 7
Output:
4 3
3 8
6 5
9 4
2 1
5 0
7 2
6 1
Union-Find
quick find implementation
quick-find (eager approach)
Data Structure
Integer array id[] of length N
Interpretation: id[p] identifies the set containing element p
\[ \{0,5,6\}\ \{1,2,7\}\ \{3,4,8,9\} \]
// 0 1 2 3 4 5 6 7 8 9 index
int [] id = {0,1,1,8,8,0,0,1,8,8};
// find(5) == 0
Q: How to implement find(p)?
quick-find (eager approach)
Data Structure
Integer array id[] of length N
Interpretation: id[p] identifies the set containing element p
\[ \{0,5,6\}\ \{1,2,7\}\ \{3,4,8,9\} \]
// 0 1 2 3 4 5 6 7 8 9 index
int [] id = {0,1,1,8,8,0,0,1,8,8};
// find(5) == 0
Q: How to implement find(p)?
A: Easy, just return id[p]
quick-find (eager approach)
Data Structure
Integer array id[] of length N
Interpretation: id[p] identifies the set containing element p
\[ \{0,5,6\}\ \{1,2,7\}\ \{3,4,8,9\} \underset{\textrm{union}(6,1)}{\Rightarrow} \{0,1,2,5,6,7\}\ \{3,4,8,9\} \]
// 0 1 2 3 4 5 6 7 8 9 index
int [] id = {0,1,1,8,8,0,0,1,8,8};
union(6,1);
// id = ??
Q: How to implement union(p,q)?
quick-find (eager approach)
Data Structure
Integer array id[] of length N
Interpretation: id[p] identifies the set containing element p
\[ \{0,5,6\}\ \{1,2,7\}\ \{3,4,8,9\} \underset{\textrm{union}(6,1)}{\Rightarrow} \{0,1,2,5,6,7\}\ \{3,4,8,9\} \]
// 0 1 2 3 4 5 6 7 8 9 index
int [] id = {0,1,1,8,8,0,0,1,8,8};
union(6,1);
// id = ??
Q: How to implement union(p,q)?
A: Change all entries whose identifier equals id[p] to id[q]. id = {1,1,1,8,8,1,1,1,8,8}
quick-find java implementation
public class QuickFindUF {
private int[] id;
public QuickFindUF(int N) {
// set id of each element to itself. N array accesses
id = new int[N];
for(int i = 0; i < N; i++)
id[i] = i;
}
public int find(int p) {
// return the id of p. 1 array access
return id[p];
}
public void union(int p, int q) {
// change all entries with id[p] to id[q]
// N+2 to 2N+2 array accesses
int pid = id[p];
int qid = id[q];
for(int i = 0; i < id.length; i++) {
if(id[i] == pid) id[i] = qid;
}
}
}
quick-find is too slow
Cost model
Number of array accesses (for read or write)
algorithm
initialize
union
find
quick-find
\(N\)
\(N\)
\(1\)
Note
Ignoring leading constant (for now)
Union is too expensive!
Processing a sequence of \(N\) union operations on \(N\) elements takes more than \(N^2\) (quadratic) array accesses.
quadratic algorithms do not scale
Rough standard (for now)
\(10^{10}\) operations per second
\(10^{10}\) words of main memory
touch all words in approximately 1 second
a truism (roughly) since 1950!
Ex. Huge problem for quick-find
\(10^{10}\) union commands on \(10^{10}\) elements
quick-find takes more than \(10^{20}\) operations
300+ years of computer time!
you will be long gone before your program finishes
quadratic algorithms do not scale
Quadratic algorithms don't scale with technology
new computer may be 10× as fast with 10× as much memory \(\Rightarrow\) want to solve a problem that is 10× as big
with quadratic algorithm, takes 10× as long!
Union-Find
quick union implementation
quick-union (lazy approach)
Data Structure
Integer array parent[] of length N, where parent[i] is parent of i in tree
Interpretation: elements in a tree corresponding to a set
// 0 1 2 3 4 5 6 7 8 9 index
int [] parent = {0,1,9,4,9,6,6,7,8,9};
// parent of 3 is 4, parent of 4 is 9, parent of 9 is 9
// root of 3 is 9
// parent and root of 5 is 6
// 0 1 2 3 4 5 6 7 8 9 index
int [] parent = {0,1,9,4,9,6,6,7,8,9};
// parent of 3 is 4, parent of 4 is 9, parent of 9 is 9
// root of 3 is 9
// parent and root of 5 is 6
Q: How to implement find(p)?
A: Return root of tree containing p
public class QuickUnionUF {
private int[] parent;
public QuickUnionUF(int N) {
// set parent of each element to itself. N array accesses
parent = new int[N];
for(int i = 0; i < N; i++)
parent[i] = i;
}
public int find(int p) {
// chase parent pointers until root. depth of p array accesses
while(p != parent[p])
p = parent[p];
return p;
}
public void union(int p, int q) {
// change root of p to point to root of q
// depth of p and q array accesses
int i = find(p);
int j = find(q);
if(i == j) return; // already unioned
parent[i] = j;
}
}
quick-union is also too slow
Cost model
Number of array accesses (for read or write)
algorithm
initialize
union
find
quick-find
\(N\)
\(N\)
\(1\)
quick-union
\(N\)
\(N^\dagger\)
\(N\)
\(\dagger\) includes cost of finding two roots
Note
Analyzed quick-union for worst case
quick-union is also too slow
Quick-find defect
Union too expensive (\(N\) array accesses)
Trees are flat, but too expensive to keep them flat
Proposition: depth of any node \(\textsf{x}\) is at most \(\lg N\)
\[N = 10\]
\[\text{depth}(\textsf{x}) \leq \lg N \approx 3.32\]
Note
In computer science, \(\lg\) means base-2 logarithm
weighted quick-union analysis
Proposition: depth of any node \(\textsf{x}\) is at most \(\lg N\)
Proof: What causes the depth of element \(\textsf{x}\) to increase? Increase by 1 when root of tree \(T_1\) containing \(\textsf{X}\) is linked to root of tree \(T_2\).
Since \(|T_2| \geq |T_1|\), the size of the tree containing \(\textsf{X}\) at least doubles .
Size of tree containing \(\textsf{x}\) can double at most \(\lg N\) times. Why?
weighted quick-union analysis
algorithm
initialize
union
find
quick-find
\(N\)
\(N\)
\(1\)
quick-union
\(N\)
\(N^\dagger\)
\(N\)
weighted QU
\(N\)
\(\lg N^\dagger\)
\(\lg N\)
\(\dagger\) includes cost of finding two roots
Note
Analyzed quick-union for worst case
summary
Key point: weighted quick-union makes it possible to solve problems that could not otherwise be addressed.
algorithm
worst-case time
quick-find
\(M N\)
quick-union
\(M N\)
weighted QU
\(N + M \log N\)
QU + path compression*
\(N + M \log N\)
weighted QU + path compression*
\(N + M \invackermann(N) \approx N+M\)
Order of growth for \(M\) union-find ops on a set of \(N\) elements
Example: \(10^{10}\) unions and finds with \(10^{10}\) elements
WQUPC reduces time from CENTURIES to SECONDS
Supercomputer won't help much; good algorithm enables solution
\(\invackermann\): inverse Ackermann function, link *path compression analysis is amortized
The game of Hex is played on a diamond-shaped board of hexagons. Two players alternate turns by placing their colored stones (yellow/cyan, black/white, red/blue, etc.) on the board, attempting to make a connection between their respective opposite sides.