Problem Overview — Dynamic Island Creation
LeetCode 305 Number of Islands II starts with an m x n grid of all water. You are given a list of positions — each position represents a cell to turn from water to land. After each addition, you must return the total number of distinct islands currently in the grid. An island is a group of horizontally or vertically connected land cells surrounded by water.
This is fundamentally different from the classic Number of Islands (LeetCode 200), where the grid is static and you run one BFS or DFS pass. Here the grid changes with each operation, and you need a running count after every single addition. That dynamic nature is what makes this problem interesting and demands a smarter approach.
The result is an array of integers, one per position in the input list, where each integer is the island count after that land cell is added. Positions can be added out of order, and the same position may appear more than once — duplicate additions must be handled gracefully.
- m x n grid initially all water — no land cells at the start
- positions list gives cells to flip from water to land one at a time
- After each flip, output the current total number of islands
- Islands: horizontally or vertically adjacent land cells (4-directional)
- k positions up to 10^4; grid up to 10^4 cells — brute force too slow
- Duplicate positions must be skipped — a cell already land stays land
Why BFS Each Time Is Too Slow
The naive approach is to re-run BFS or DFS from scratch after each land addition to count all islands. Each BFS traversal visits every cell in the grid — O(m * n) per call. With k additions, the total time is O(k * m * n). When k approaches 10^4 and the grid approaches 10^4 cells, this yields up to 10^8 operations per test case, which reliably times out.
Even a smarter incremental BFS — only re-exploring neighbors of the newly added cell — still degrades to O(m * n) per step in the worst case when large islands merge. The fundamental issue is that BFS and DFS cannot efficiently answer "are these two cells in the same island?" without re-exploring. That question is exactly what Union-Find is built for.
The structure we need supports two operations efficiently: union two cells into the same island, and find whether two cells share an island. Union-Find (also called Disjoint Set Union or DSU) does both in near-constant amortized time, turning the overall algorithm from O(k * m * n) into O(k * alpha(m * n)) — practically linear.
Union-Find vs BFS for Dynamic Connectivity
Union-Find processes each land addition in near O(1) amortized time using path compression and union by rank. This replaces O(m*n) BFS per addition with O(alpha(m*n)) — effectively constant — making the overall algorithm practical for large grids.
Union-Find Data Structure
The Union-Find structure tracks which cells belong to the same connected component (island). Each cell (i, j) is mapped to a flat index using i * n + j, turning the 2D grid into a 1D array of size m * n. Two arrays power the structure: parent[] and rank[]. Initially all cells are water, so parent[cell] = -1 to signal "not yet land".
The find operation returns the root representative of a cell's component. With path compression, every node on the path to the root is pointed directly at the root on the way back up. This flattens the tree so future find calls on any node in that path cost O(1). The union operation merges two components by attaching the root of the smaller-ranked tree under the root of the larger-ranked tree — union by rank.
When two land cells become adjacent (share an edge), union is called on them. If they are already in the same component, the island count does not change. If they are in different components, the two islands merge into one and the count decreases by one. This is the core logic that runs for each of the four neighbors after every land addition.
- 1Allocate parent[m*n] = -1 (all water) and rank[m*n] = 0
- 2Cell (i, j) maps to index i*n + j
- 3find(x): follow parent pointers to root, compress path on the way back
- 4union(x, y): find roots of x and y; if same, do nothing; else attach smaller rank under larger
- 5isLand(x): check parent[x] != -1
Processing Each Land Addition
When position (r, c) is added, first check if it is already land (parent[r*n+c] != -1). If so, skip it and append the current island count unchanged — handling duplicates in O(1). Otherwise, mark it as land by setting parent[r*n+c] = r*n+c (the cell is its own root), set rank to 0, and increment the island count by one — a brand new island just appeared.
Next, check all four neighbors: up, down, left, right. For each neighbor that is within bounds and is land (parent != -1), call union(cell, neighbor). The union operation checks whether both cells share a root. If not, it merges the two components — attaching one root under the other based on rank — and decrements the island count by one, because two separate islands just became one.
After processing all four neighbors, append the current island count to the result array. That's the entire per-step logic: one potential new-island increment, up to four potential merges. Each union and find call runs in near O(1) amortized time, so the total per-step cost is O(alpha(m*n)), effectively constant.
O(alpha(n)) per Operation — Effectively O(1)
Path compression combined with union by rank gives O(alpha(n)) amortized time per find or union operation, where alpha is the inverse Ackermann function. For any practical input size, alpha(n) <= 4. This means each land addition costs effectively constant time regardless of how many cells or islands exist.
Handling Duplicate Positions
The problem allows the same grid cell to appear multiple times in the positions list. A naive implementation that sets parent[cell] = cell unconditionally on every visit would incorrectly reset a cell that is already part of a large island, corrupting the Union-Find structure and producing wrong island counts.
The fix is a single guard at the start of each step: if parent[cell] != -1, the cell is already land. Skip all processing — do not reset parent, do not increment the count, do not union with neighbors — and simply record the current island count as-is. This check is O(1) and correctly handles any number of duplicate entries.
In interviews, duplicates are a common edge case that catches candidates off guard. Examiners may specifically ask how you handle them, so it is worth calling out explicitly when explaining your approach. The parent[] array doubles as a visited/land marker, which makes the duplicate check free with no extra memory.
- 1Before processing position (r, c), compute cell = r*n + c
- 2If parent[cell] != -1, cell is already land — append current count and continue
- 3Otherwise: set parent[cell] = cell, rank[cell] = 0, increment islandCount
- 4Check 4 neighbors: if in bounds and parent[neighbor] != -1, call union(cell, neighbor)
- 5Append updated islandCount to results
Code Walkthrough — Python and Java
In Python, implement a UnionFind class with an __init__ that allocates parent and rank arrays of size m*n initialized to -1 and 0 respectively, plus an islands counter. The find method uses recursive path compression: return x if parent[x] == x else the result of setting parent[x] = find(parent[x]). The union method finds both roots and, if different, attaches the lower-rank root under the higher-rank root, increments rank if equal, and decrements self.islands. The main numIslands2 function creates a UnionFind, iterates positions, skips if already land, marks land, unions with valid land neighbors, and collects self.islands after each step.
In Java, use two int arrays parent[] and rank[] of size m*n. Initialize all to -1. The find method uses iterative path compression with a while loop. The union method is identical in logic. The main method collects results in a List<Integer>. Total time complexity is O(k * alpha(m*n)) where k is the number of positions — effectively O(k). Space complexity is O(m*n) for the parent and rank arrays.
Both implementations share the same structure: UnionFind encapsulates state, the main loop drives the process, and results are accumulated per step. The key insight is that Union-Find is not just an optimization — it is the right abstraction for dynamic connectivity, and this problem is a textbook application of the pattern.
Do Not Forget Duplicate Position Handling
Failing to check for duplicate positions is the most common bug in interviews. If you set parent[cell] = cell without checking first, you will break the Union-Find structure for cells that are already part of an island. Always guard with if parent[cell] != -1: skip before doing any work.