Lessons
1 · Foundations (Think Like a Programmer)
2 · Data Structures and Recursion
- 05Arrays and Array-Based Problem Solving
- 06Strings: Characters, Immutability, and Efficient Manipulation
- 07Linked Lists: Nodes, Pointers, and Pointer Reasoning
- 08Stacks and Queues
- 09Hashing: HashMap, HashSet, and Frequency Patterns
- 10Recursion and Backtracking
- 11Trees and Binary Search Trees
- 12Heaps and Priority Queues
3 · Core Algorithms and Reusable Patterns
4 · Advanced Interview Topics
Lesson 24 · Advanced Interview Topics
Range Queries and Disjoint Set Union
What to do when prefix sums are not enough: Fenwick and segment trees for range queries with updates (optional depth), and disjoint set union with path compression and union by size for connectivity, cycles and Kruskal's algorithm (the part worth knowing).
For
Learners who have covered graphs and prefix sums and want to know which advanced structures are worth learning for interviews, and how they work.
Level
Before you start
Articles 16 and 19: prefix sums and graph basics. The bit trick n & -n from article 18 is used by the Fenwick tree.
On this page
Key takeaways
- Prefix sums answer range queries on static data. When values change, a Fenwick tree or a segment tree gives O(log n) queries and updates. These are advanced topics, and less common for fresher interviews.
- A Fenwick tree stores partial sums in an array and walks with i & -i. A segment tree stores the answer for a range at each node and works for sum, minimum, maximum and more.
- Disjoint set union (union-find) keeps track of groups. find returns a group's representative, and union merges two groups.
- Path compression plus union by size make each operation nearly constant, O(alpha(n)). Without them, a chain can make find O(n).
- DSU is the right tool for connectivity when edges arrive over time, for detecting a cycle in an undirected graph, and for Kruskal's minimum spanning tree.
Imagine you maintain a leaderboard of scores, and you need to answer "what is the total score of players ranked 10 through 500?" many times, while individual scores keep changing. Prefix sums answer the range question beautifully, but if a single score changes, the whole prefix array must be rebuilt: O(n) per update. That is too slow if updates are frequent.
This article covers two families of tools that sit one step beyond the basics. The first handles range queries with updates (Fenwick trees and segment trees). The second, disjoint set union, tracks which items belong to the same group as groups merge over time.
How much of this do you need?
#Be honest with yourself about priorities. Here is a rough guide for fresher and early-career interviews. Companies differ, so check your target roles.
Topic | Priority | Why |
|---|---|---|
Prefix sums for static range queries | Essential | Covered earlier. Common. |
Disjoint set union (union-find) | Worth knowing well | Short code, appears in graph and "groups" problems, and it is a classic follow-up to DFS for components |
Kruskal's minimum spanning tree | Good to know | Uses DSU. Appears in graph rounds |
Fenwick tree (BIT) | Optional | Short code, and a neat idea, but rarely required |
Segment tree | Optional, advanced | Powerful, longer code. More common in contests and some product-company rounds |
Lazy propagation, advanced variants | Beyond this course | Specialised |
If time is short, read the DSU section carefully and skim the Fenwick and segment tree parts to understand what they offer.
What you will learn
#- When prefix sums stop being enough, and the options that replace them.
- How a Fenwick tree and a segment tree work, at the level of ideas and code.
- What disjoint set union is, and how path compression and union by size make it fast.
- How to use DSU for connected components, cycle detection and Kruskal's algorithm.
Range queries with updates
#The setting: an array of numbers. Two operations, repeated many times:
- Query: the sum (or minimum, or maximum) of a range
[left, right]. - Update: change one element.
Approach | Query | Update | Notes |
|---|---|---|---|
Plain loop | O(n) | O(1) | Fine for few queries |
Prefix sums | O(1) | O(n), must rebuild | Best when nothing changes |
Fenwick tree | O(log n) | O(log n) | Sums (and anything invertible). Short code |
Segment tree | O(log n) | O(log n) | Sum, min, max, gcd and more |
The trade is clear: both trees give up the instant O(1) queries of prefix sums, to get fast updates too.
Fenwick tree (binary indexed tree), optional
#A Fenwick tree stores partial sums in a plain array so that both a prefix sum and a point update touch only about log n entries.
The trick is in which range each slot covers. With 1-based positions, slot i stores the sum of the elements in the range (i - lowbit(i), i], where lowbit(i) = i & -i is the lowest set bit of i (the trick from the bit article). For example, the slots for n = 8:
Slot | Binary |
| Covers positions |
|---|---|---|---|
1 |
| 1 | 1 |
2 |
| 2 | 1 to 2 |
3 |
| 1 | 3 |
4 |
| 4 | 1 to 4 |
5 |
| 1 | 5 |
6 |
| 2 | 5 to 6 |
7 |
| 1 | 7 |
8 |
| 8 | 1 to 8 |
Prefix sum of the first i elements. Add tree[i], then remove the lowest set bit (i -= i & -i) and repeat until i is 0. For i = 7 (111): add tree[7] (position 7), then i = 6: add tree[6] (positions 5 and 6), then i = 4: add tree[4] (positions 1 to 4). Total: positions 1 to 7. Each step removes one set bit, so there are at most log n steps.
Point update at position i. Add the change to tree[i], then move to the next slot that covers i (i += i & -i) and repeat while i <= n. For position 5: update slots 5, 6 and 8, which are exactly the slots whose ranges contain position 5.
A range sum is a difference of two prefix sums: sum(left..right) = prefix(right) - prefix(left - 1).
Our class uses a friendly 0-based public interface and shifts to 1-based inside.
// File: Main.javaimport java.util.Random;public class Main { static class FenwickTree { private final int n; private final long[] tree; // 1-based internally FenwickTree(int[] values) { n = values.length; tree = new long[n + 1]; for (int i = 0; i < n; i++) { add(i, values[i]); } } // Adds delta to the element at position index (0-based). void add(int index, long delta) { for (int i = index + 1; i <= n; i += i & -i) { tree[i] += delta; } } // Sum of the elements at positions 0..index. A negative index gives 0. long prefix(int index) { long sum = 0; for (int i = index + 1; i > 0; i -= i & -i) { sum += tree[i]; } return sum; } long range(int left, int right) { return prefix(right) - prefix(left - 1); } } public static void main(String[] args) { int[] values = {3, 2, -1, 6, 5, 4, -3, 3, 7, 2, 3}; FenwickTree fenwick = new FenwickTree(values); System.out.println(fenwick.range(0, 4) + " " + fenwick.range(2, 5) + " " + fenwick.range(10, 10) + " " + fenwick.prefix(-1)); fenwick.add(3, 10); // the element at position 3 grows by 10 System.out.println(fenwick.range(0, 4) + " " + fenwick.range(2, 5)); // Compare with a plain array on 2000 random operations. Random random = new Random(7); int[] plain = new int[50]; FenwickTree check = new FenwickTree(plain); boolean same = true; for (int step = 0; step < 2000; step++) { if (random.nextBoolean()) { int index = random.nextInt(50); int delta = random.nextInt(21) - 10; plain[index] += delta; check.add(index, delta); } else { int a = random.nextInt(50); int b = random.nextInt(50); int left = Math.min(a, b); int right = Math.max(a, b); long expected = 0; for (int i = left; i <= right; i++) { expected += plain[i]; } same &= expected == check.range(left, right); } } System.out.println("matches the brute force on 2000 random operations: " + same); }}15 14 3 025 24matches the brute force on 2000 random operations: trueComplexity: each add and prefix does at most about log2(n) + 1 steps. Building by repeated add takes O(n log n), and there are linear-time builds too. Space is O(n). The code is remarkably short, which is the Fenwick tree's appeal. It handles sums (and counts, XORs, and other "invertible" combinations), but not minimum or maximum directly, because you cannot subtract those.
Segment tree, optional and advanced
#A segment tree is a binary tree over the positions of the array. The root represents the whole range [0, n - 1]. Each node represents a range and stores the answer for that range (its sum, or minimum, or whatever). A node covering [lo, hi] has two children covering [lo, mid] and [mid + 1, hi]. A leaf covers one element.
Array: [5, 3, 8, 1] Sum tree: [0..3] 17 / \ [0..1] 8 [2..3] 9 / \ / \ [0] 5 [1] 3 [2] 8 [3] 1Query a range. Descend from the root. If a node's range lies entirely inside the query, return its stored value. If it lies entirely outside, return the identity value (0 for sums, a huge number for minimum). Otherwise, recurse into both children and combine their answers. At each level, at most a constant number of nodes are partially overlapping, so a query touches O(log n) nodes.
Update one element. Walk down to the leaf, change it, and recompute each node on the way back up from its two children: O(log n).
The same code works for different questions if you pass in the combine function and its identity: addition and 0 for sums, Math::min and Long.MAX_VALUE for minimums. A common array layout stores the tree in an array where the children of node i are 2i and 2i + 1, with size about 4n.
// File: Main.javaimport java.util.Random;import java.util.function.LongBinaryOperator;public class Main { static class SegmentTree { private final int n; private final long[] tree; private final LongBinaryOperator combine; private final long identity; SegmentTree(int[] values, LongBinaryOperator combine, long identity) { this.n = values.length; this.tree = new long[Math.max(1, 4 * n)]; this.combine = combine; this.identity = identity; if (n > 0) { build(1, 0, n - 1, values); } } private void build(int node, int lo, int hi, int[] values) { if (lo == hi) { tree[node] = values[lo]; return; } int mid = lo + (hi - lo) / 2; build(2 * node, lo, mid, values); build(2 * node + 1, mid + 1, hi, values); tree[node] = combine.applyAsLong(tree[2 * node], tree[2 * node + 1]); } // Answer for positions left..right, both included. long query(int left, int right) { return query(1, 0, n - 1, left, right); } private long query(int node, int lo, int hi, int left, int right) { if (right < lo || hi < left) { return identity; // no overlap } if (left <= lo && hi <= right) { return tree[node]; // full overlap } int mid = lo + (hi - lo) / 2; // partial overlap: ask both children return combine.applyAsLong(query(2 * node, lo, mid, left, right), query(2 * node + 1, mid + 1, hi, left, right)); } void update(int index, long value) { update(1, 0, n - 1, index, value); } private void update(int node, int lo, int hi, int index, long value) { if (lo == hi) { tree[node] = value; return; } int mid = lo + (hi - lo) / 2; if (index <= mid) { update(2 * node, lo, mid, index, value); } else { update(2 * node + 1, mid + 1, hi, index, value); } tree[node] = combine.applyAsLong(tree[2 * node], tree[2 * node + 1]); } } public static void main(String[] args) { int[] values = {5, 3, 8, 1, 9, 2, 7}; SegmentTree sums = new SegmentTree(values, Long::sum, 0); SegmentTree mins = new SegmentTree(values, Math::min, Long.MAX_VALUE); System.out.println(sums.query(1, 4) + " " + mins.query(2, 5) + " " + sums.query(0, 6) + " " + mins.query(0, 6)); sums.update(3, 10); mins.update(3, 10); System.out.println(sums.query(1, 4) + " " + mins.query(2, 5)); Random random = new Random(11); int[] plain = new int[40]; for (int i = 0; i < plain.length; i++) { plain[i] = random.nextInt(100); } SegmentTree sumTree = new SegmentTree(plain, Long::sum, 0); SegmentTree minTree = new SegmentTree(plain, Math::min, Long.MAX_VALUE); boolean same = true; for (int step = 0; step < 2000; step++) { if (random.nextBoolean()) { int index = random.nextInt(40); int value = random.nextInt(100); plain[index] = value; sumTree.update(index, value); minTree.update(index, value); } else { int a = random.nextInt(40); int b = random.nextInt(40); int left = Math.min(a, b); int right = Math.max(a, b); long expectedSum = 0; long expectedMin = Long.MAX_VALUE; for (int i = left; i <= right; i++) { expectedSum += plain[i]; expectedMin = Math.min(expectedMin, plain[i]); } same &= expectedSum == sumTree.query(left, right) && expectedMin == minTree.query(left, right); } } System.out.println("matches the brute force on 2000 random operations: " + same); }}21 1 35 130 2matches the brute force on 2000 random operations: trueThe first numbers: sum(1..4) = 3 + 8 + 1 + 9 = 21, min(2..5) = 1, the whole-array sum is 35, and its minimum is 1. After changing position 3 from 1 to 10, the sum over 1..4 becomes 30 and the minimum over 2..5 becomes 2 (from the 2 at position 5).
Complexity: build O(n), query and update O(log n), space O(n) (about 4n slots). Segment trees can also support range updates with a technique called lazy propagation, which is beyond this course.
Disjoint set union (union-find), worth knowing
#Now a different problem: groups that merge. You have n items, each starting in a group of its own. Over time you are told "these two are in the same group", and you must be able to answer "are these two in the same group?" The structure for this is the disjoint set union (DSU), also called union-find. It supports two operations:
find(x): returns the representative (the "root") of the group containingx. Two items are in the same group exactly when their representatives are equal.union(a, b): merges the groups ofaandb.
The idea
#Store the groups as a forest of trees. Each item has a parent. A root is its own parent. find(x) follows parents up to the root. union(a, b) finds both roots, and makes one root the parent of the other (if they are different).
A naive version can degrade. Joining items in a chain (union(0, 1), union(1, 2), ...) can produce a tree that is just a long path, so find takes O(n). Two small improvements fix this:
- Path compression. While walking up in
find, make the nodes you pass point closer to (or directly at) the root. Next time, the same lookups are short. The code below uses path halving, a one-pass version:parent[x] = parent[parent[x]]as it climbs. - Union by size. When merging, attach the smaller tree under the larger one's root. This keeps trees shallow, because a node's depth only increases when its tree is merged under a tree at least as big, which can happen at most
log ntimes.
With both, the cost per operation is O(α(n)) amortised, where α is the inverse Ackermann function. It grows so slowly that it is at most 4 for any input you could fit in the universe, so in practice we say "almost constant".
What DSU is used for
#- Connected components with edges arriving over time. Each edge is a
union. The number of components isnminus the number of successful unions. - Cycle detection in an undirected graph. For each edge
(a, b), iffind(a) == find(b), they are already connected, so this edge closes a cycle. - Kruskal's algorithm for a minimum spanning tree (below).
- Grouping problems, such as merging accounts that share an email.
DSU cannot split groups. It only merges. If the problem needs deletions, you need a different approach.
// File: Main.javaimport java.util.Arrays;public class Main { static class DisjointSet { private final int[] parent; private final int[] size; private int components; DisjointSet(int n) { parent = new int[n]; size = new int[n]; components = n; for (int i = 0; i < n; i++) { parent[i] = i; // each item starts alone size[i] = 1; } } int find(int x) { while (parent[x] != x) { parent[x] = parent[parent[x]]; // path halving: point at the grandparent x = parent[x]; } return x; } // Returns true if two different groups were merged, false if they were already together. boolean union(int a, int b) { int rootA = find(a); int rootB = find(b); if (rootA == rootB) { return false; } if (size[rootA] < size[rootB]) { // attach the smaller tree under the larger int temp = rootA; rootA = rootB; rootB = temp; } parent[rootB] = rootA; size[rootA] += size[rootB]; components--; return true; } boolean connected(int a, int b) { return find(a) == find(b); } int sizeOf(int x) { return size[find(x)]; } int components() { return components; } } // Kruskal: sort the edges by weight, and take each edge that joins two different groups. static long minimumSpanningTreeWeight(int n, int[][] edges) { // each edge: {u, v, weight} int[][] sorted = edges.clone(); Arrays.sort(sorted, (a, b) -> Integer.compare(a[2], b[2])); DisjointSet groups = new DisjointSet(n); long total = 0; int taken = 0; for (int[] edge : sorted) { if (groups.union(edge[0], edge[1])) { total += edge[2]; taken++; } } return taken == n - 1 ? total : -1; // -1: the graph is not connected } static int depthOf(int[] parent, int x) { int depth = 0; while (parent[x] != x) { x = parent[x]; depth++; } return depth; } public static void main(String[] args) { DisjointSet groups = new DisjointSet(6); groups.union(0, 1); groups.union(1, 2); groups.union(3, 4); System.out.println(groups.components() + " " + groups.connected(0, 2) + " " + groups.connected(2, 3) + " " + groups.sizeOf(0) + " " + groups.sizeOf(5)); DisjointSet cycle = new DisjointSet(3); System.out.println(cycle.union(0, 1) + " " + cycle.union(1, 2) + " " + cycle.union(2, 0) + " (false means this edge closes a cycle)"); int[][] edges = {{0, 1, 10}, {0, 2, 6}, {0, 3, 5}, {1, 3, 15}, {2, 3, 4}}; System.out.println(minimumSpanningTreeWeight(4, edges) + " " + minimumSpanningTreeWeight(3, new int[][] {{0, 1, 1}})); // Why union by size matters: join 0-1, 1-2, 2-3, ... in a chain. int n = 10000; int[] naiveParent = new int[n]; for (int i = 0; i < n; i++) { naiveParent[i] = i; } for (int i = 0; i + 1 < n; i++) { naiveParent[i] = i + 1; // naive union: always hang the old root under the new item } DisjointSet sized = new DisjointSet(n); for (int i = 0; i + 1 < n; i++) { sized.union(i, i + 1); } System.out.println("naive chain: item 0 is " + depthOf(naiveParent, 0) + " links below its root"); System.out.println("union by size: " + sized.components() + " group holding " + sized.sizeOf(0) + " items"); }}3 true false 3 1true true false (false means this edge closes a cycle)19 -1naive chain: item 0 is 9999 links below its rootunion by size: 1 group holding 10000 itemsThe chain experiment shows the danger: always hanging the old root under the new item builds a path of 9,999 links, so find(0) would have to walk it all. With union by size, the tree stays shallow and the example ends with a single group of 10,000 items.
The cycle line shows the cycle test in action: the first two unions merge groups (true), but union(2, 0) finds that 0 and 2 are already together (through 1), so it returns false.
Dry run of Kruskal on the edges 0-1 (10), 0-2 (6), 0-3 (5), 1-3 (15), 2-3 (4). Sorted by weight: 2-3 (4), 0-3 (5), 0-2 (6), 0-1 (10), 1-3 (15).
Edge | Same group already? | Action | Total so far |
|---|---|---|---|
| no | take it, merge | 4 |
| no | take it, merge into | 9 |
| yes ( | skip: it would form a cycle | 9 |
| no | take it, merge into | 19 |
| yes | skip | 19 |
The minimum spanning tree has weight 19 with 3 edges (n - 1). If fewer than n - 1 edges could be taken, the graph is disconnected and has no spanning tree, which the method reports as -1. Kruskal is O(E log E), dominated by the sort.
Complexity summary
#Structure or task | Build | Query or | Update or | Space |
|---|---|---|---|---|
Prefix sums (static) | O(n) | O(1) | O(n) rebuild | O(n) |
Fenwick tree | O(n log n) | O(log n) | O(log n) | O(n) |
Segment tree | O(n) | O(log n) | O(log n) | O(n) |
DSU with compression and union by size | O(n) | O(α(n)) | O(α(n)) | O(n) |
Kruskal's MST | O(E log E) | O(V + E) |
Edge cases and common mistakes
#- Fenwick tree indexing. The tree is 1-based. Using index
0makesi & -iequal to0and the loop never advances. Convert from 0-based input at the boundary. - Updating when you meant setting. A Fenwick
addadds a delta. To set a value, add the difference between the new and the old value. - Overflow. Use
longfor sums. - Segment tree size. Allocate about
4nslots. - Inclusive versus exclusive ranges. Decide and be consistent: this article uses inclusive
[left, right]. - DSU
unionon non-roots. Alwaysfindboth roots first. Settingparent[a] = bdirectly can detach nodes from their own groups (see the exercise). - Forgetting path compression or union by size. Correct, but a chain can make operations
O(n). - A recursive
findwithout compression on a long chain can overflow the stack. The iterative version avoids that. - Counting components. Start at
n, and decrease only when a union actually merges two different groups. - Using DSU when edges are removed. DSU cannot undo a merge.
- Using a heavy structure when a simple one works. If there are no updates, prefix sums are better. If the graph is static and you only need components once, a DFS is simpler.
Exercises
#Exercise 1
Trace
#For a Fenwick tree with n = 8, which slots are modified by add at 1-based position 5? Which slots are read by prefix of the first 6 elements?
Exercise 2: Mutable range sum
#Implement a class with update(index, value) (set the element at a position to a new value) and sumRange(left, right), using a Fenwick tree. For the array [1, 3, 5], sumRange(0, 2) is 9. After update(1, 2), it is 8.
Exercise 3: Redundant connection
#A graph that was a tree with nodes 1..n had one extra edge added. Given the edge list, return the edge that, if removed, leaves a tree. If several edges could be removed, return the one that appears last in the input. For [[1, 2], [1, 3], [2, 3]] the answer is [2, 3].
Exercise 4: Debug
#This union method looks plausible but breaks connectivity. Find an input that shows the problem, and fix it.
void union(int a, int b) { parent[a] = b;}Exercise 5: Interview-style, Kruskal's algorithm
#Given n vertices and weighted undirected edges, return the total weight of a minimum spanning tree, or -1 if the graph is not connected. For n = 4 and edges (0,1,10), (0,2,6), (0,3,5), (1,3,15), (2,3,4) the answer is 19. Explain why the greedy edge choice is valid.
Show solutionHide solution
add at position 5. Start at i = 5 (101), then i += i & -i: 5 + 1 = 6, then 6 + 2 = 8, then 8 + 8 = 16 > 8, stop. The modified slots are 5, 6 and 8. These are exactly the slots whose ranges contain position 5: slot 5 covers position 5, slot 6 covers positions 5 and 6, slot 8 covers positions 1 to 8.
prefix of 6. Start at i = 6 (110): read tree[6] (positions 5 to 6). Then i -= i & -i: 6 - 2 = 4, read tree[4] (positions 1 to 4). Then 4 - 4 = 0, stop. The read slots are 6 and 4, and together they cover positions 1 to 6.
// File: Main.javaimport java.util.ArrayList;import java.util.List;public class Main { public static void main(String[] args) { int n = 8; List<Integer> modified = new ArrayList<>(); for (int i = 5; i <= n; i += i & -i) { modified.add(i); } List<Integer> read = new ArrayList<>(); for (int i = 6; i > 0; i -= i & -i) { read.add(i); } System.out.println("add at 5 modifies " + modified + ", prefix of 6 reads " + read); }}add at 5 modifies [5, 6, 8], prefix of 6 reads [6, 4]Exercise 2
Show solutionHide solution
Keep a copy of the current values in a plain array so we know the old value, and apply the difference to the Fenwick tree: add(index, newValue - oldValue). A range sum is then prefix(right) - prefix(left - 1). Both operations are O(log n).
// File: Main.javapublic class Main { static class NumArray { private final int n; private final long[] tree; private final int[] values; NumArray(int[] initial) { n = initial.length; tree = new long[n + 1]; values = initial.clone(); for (int i = 0; i < n; i++) { add(i, initial[i]); } } private void add(int index, long delta) { for (int i = index + 1; i <= n; i += i & -i) { tree[i] += delta; } } private long prefix(int index) { long sum = 0; for (int i = index + 1; i > 0; i -= i & -i) { sum += tree[i]; } return sum; } void update(int index, int value) { add(index, (long) value - values[index]); // add only the change values[index] = value; } long sumRange(int left, int right) { return prefix(right) - prefix(left - 1); } } public static void main(String[] args) { NumArray array = new NumArray(new int[] {1, 3, 5}); System.out.println(array.sumRange(0, 2)); array.update(1, 2); System.out.println(array.sumRange(0, 2) + " " + array.sumRange(1, 1) + " " + array.sumRange(2, 2)); array.update(0, 100); array.update(0, 100); // setting the same value twice must not double count System.out.println(array.sumRange(0, 0) + " " + array.sumRange(0, 2)); }}98 2 5100 107Storing the old values is what makes update a set, so repeating the same update does not change the total twice.
Exercise 3
Show solutionHide solution
Go through the edges in order, using a DSU over the nodes. For each edge (a, b): if a and b are already in the same group, this edge closes a cycle. Because the input had exactly one extra edge, this is the redundant edge. Otherwise merge their groups. Since the graph is a tree plus one edge, there is exactly one cycle, so exactly one edge fails to merge, and we can return it directly. (It is the last edge of the cycle in input order, which matches "return the last one that could be removed".)
Time O(n α(n)), space O(n).
// File: Main.javaimport java.util.Arrays;public class Main { static int find(int[] parent, int x) { while (parent[x] != x) { parent[x] = parent[parent[x]]; x = parent[x]; } return x; } static int[] findRedundantConnection(int[][] edges) { int n = edges.length; // nodes are 1..n for a tree plus one extra edge int[] parent = new int[n + 1]; int[] size = new int[n + 1]; for (int i = 0; i <= n; i++) { parent[i] = i; size[i] = 1; } for (int[] edge : edges) { int a = find(parent, edge[0]); int b = find(parent, edge[1]); if (a == b) { return edge; // already connected: this edge makes a cycle } if (size[a] < size[b]) { int temp = a; a = b; b = temp; } parent[b] = a; size[a] += size[b]; } return new int[0]; } public static void main(String[] args) { System.out.println(Arrays.toString(findRedundantConnection(new int[][] {{1, 2}, {1, 3}, {2, 3}}))); System.out.println(Arrays.toString(findRedundantConnection(new int[][] {{1, 2}, {2, 3}, {3, 4}, {1, 4}, {1, 5}}))); System.out.println(Arrays.toString(findRedundantConnection(new int[][] {{1, 2}, {2, 1}}))); }}[2, 3][1, 4][2, 1]In the second example, the edges 1-2, 2-3, 3-4 build a path, and 1-4 closes a cycle 1-2-3-4-1. The edge 1-5 is fine. The last test uses a repeated edge (1-2 twice), which is also a cycle of length 2, and DSU catches it, where the DFS "parent" trick would not.
Exercise 4
Show solutionHide solution
parent[a] = b assumes a and b are roots. If a is not a root (it is already below some other node), this overwrites its link and detaches it from the rest of its group.
Example: union(0, 1) sets parent[0] = 1, so 0 and 1 are together. Then union(0, 3) sets parent[0] = 3, which cuts 0 off from 1. Now find(0) is 3 and find(1) is 1, so connected(0, 1) is false, although they were merged.
The fix is to link roots: parent[find(a)] = find(b), after checking that the roots differ (and preferably by size).
// File: Main.javapublic class Main { static int find(int[] parent, int x) { while (parent[x] != x) { x = parent[x]; } return x; } public static void main(String[] args) { int[] buggy = {0, 1, 2, 3}; buggy[0] = 1; // union(0, 1) buggy[0] = 3; // union(0, 3), done on a non-root System.out.println("buggy: connected(0, 1) = " + (find(buggy, 0) == find(buggy, 1))); int[] fixed = {0, 1, 2, 3}; fixed[find(fixed, 0)] = find(fixed, 1); // union(0, 1) int rootOfZero = find(fixed, 0); int rootOfThree = find(fixed, 3); if (rootOfZero != rootOfThree) { fixed[rootOfZero] = rootOfThree; // union(0, 3), done on roots } System.out.println("fixed: connected(0, 1) = " + (find(fixed, 0) == find(fixed, 1)) + ", connected(0, 3) = " + (find(fixed, 0) == find(fixed, 3))); }}buggy: connected(0, 1) = falsefixed: connected(0, 1) = true, connected(0, 3) = trueExercise 5
Show solutionHide solution
Sort the edges by weight. Go through them from lightest to heaviest. Take an edge if it connects two different groups (use DSU), otherwise skip it, since it would create a cycle. Stop when n - 1 edges are taken. If you run out of edges first, the graph is disconnected.
Why is the greedy choice valid? (the cut property). Take the lightest edge among all edges that connect two different groups. Suppose some minimum spanning tree does not contain it. Add the edge to that tree: this creates a cycle, and that cycle must cross between the two groups in at least one other edge, which is at least as heavy (it is not lighter, because our edge is the lightest crossing edge). Swap that heavier edge for ours: we still have a spanning tree, and its weight is not larger. So there is a minimum spanning tree that includes our edge. Repeating this at each step shows the greedy result is optimal. Sorting dominates the cost: O(E log E).
// File: Main.javaimport java.util.Arrays;public class Main { static int find(int[] parent, int x) { while (parent[x] != x) { parent[x] = parent[parent[x]]; x = parent[x]; } return x; } static long kruskal(int n, int[][] edges) { int[][] sorted = edges.clone(); Arrays.sort(sorted, (a, b) -> Integer.compare(a[2], b[2])); int[] parent = new int[n]; int[] size = new int[n]; for (int i = 0; i < n; i++) { parent[i] = i; size[i] = 1; } long total = 0; int taken = 0; for (int[] edge : sorted) { int a = find(parent, edge[0]); int b = find(parent, edge[1]); if (a == b) { continue; // would form a cycle } if (size[a] < size[b]) { int temp = a; a = b; b = temp; } parent[b] = a; size[a] += size[b]; total += edge[2]; taken++; } return taken == n - 1 ? total : -1; } public static void main(String[] args) { int[][] edges = {{0, 1, 10}, {0, 2, 6}, {0, 3, 5}, {1, 3, 15}, {2, 3, 4}}; System.out.println(kruskal(4, edges)); System.out.println(kruskal(3, new int[][] {{0, 1, 1}})); System.out.println(kruskal(1, new int[][] {})); System.out.println(kruskal(3, new int[][] {{0, 1, 2}, {1, 2, 2}, {0, 2, 2}})); }}19-104A single vertex needs no edges, so its minimum spanning tree has weight 0 (and taken == n - 1 == 0). For the triangle with equal weights, any two of the three edges form a minimum spanning tree, total 4.
Where to go next
#- Next: The Interview Problem-Solving Workflow starts the final module, which turns everything you have learned into interview performance.
- Related: Graphs: Representations, BFS, and DFS solved component counting with a search. DSU solves the same problem incrementally, and the two are good to compare.
- Practice habit: when a problem says "merge groups", "are these connected?" or "edges arrive one at a time", think DSU. When it says "range query with updates" and the constraints are large, think Fenwick or segment tree, and confirm that this level of depth is appropriate for your target interviews.
Connected lessons
Linked from 4 guides
Other pages in the library that point here.
Keep reading
More in Technology →Guide · Technology
How to Learn Data Structures and Algorithms Without Burning Out
Guide · Career
The OA Landscape: Platforms, Formats, Proctoring and Rules
Guide · Career
A LinkedIn Profile That Gets Recruiter Messages

