Topics › Graphs › MinimumSpanningTree/Disjoint Set and Problems
Number of islands II
LeetCode ↗ Java source ↗ Edit this article ↗
Jump to code:
Problem
You are given an n x m grid in which every cell starts as water, and a list of k operations. Operation i is a cell [row, col] that turns into land. After every operation, report the number of islands, where an island is a maximal group of land cells connected horizontally or vertically (not diagonally).
An operation may name a cell that is already land. Then nothing changes and the count is reported again.
Constraints (LeetCode version): 1 ≤ n, m ≤ 10^4, n · m ≤ 10^4, 1 ≤ k ≤ 10^4. Other versions allow larger grids; the approaches do not change.
Example 1
n = 4, m = 5, operators = [[1,1],[0,1],[3,3],[3,4]]
answer = [1, 1, 2, 2]
// (1,1) new island; (0,1) joins it; (3,3) new island; (3,4) joins (3,3)
Example 2
n = 3, m = 3, operators = [[0,1],[1,0],[1,2],[2,1],[1,1]]
answer = [1, 2, 3, 4, 1]
// four separate cells around the centre, then the centre joins all four
Example 2 shows why this problem is interesting: a single new cell can reduce the island count by three at once.
Approach 1: Brute force, recount after every operation
Intuition. We already know how to count islands in a static grid: scan every cell, and each time we find an unvisited land cell, flood its whole island with BFS and add one. Apply that after every operation. It is obviously correct, because each answer is computed from scratch on the current grid.
The waste is just as obvious: an operation changes one cell, yet we re-examine all n · m cells.
Algorithm.
- Keep a boolean grid
land. - For each operation, mark the cell as land and call
countIslands. countIslandsscans the grid; every unvisited land cell starts a BFS that marks its whole island, and increments the count.
static List<Integer> bruteForce(int n, int m, int[][] operators) {
boolean[][] land = new boolean[n][m];
List<Integer> answer = new ArrayList<>();
for (int[] op : operators) {
land[op[0]][op[1]] = true;
answer.add(countIslands(land));
}
return answer;
}
static int countIslands(boolean[][] land) {
int n = land.length, m = land[0].length, count = 0;
boolean[][] seen = new boolean[n][m];
Deque<int[]> queue = new ArrayDeque<>();
for (int r = 0; r < n; r++) {
for (int c = 0; c < m; c++) {
if (!land[r][c] || seen[r][c]) continue;
count++; // new island found, flood it
seen[r][c] = true;
queue.add(new int[]{r, c});
while (!queue.isEmpty()) {
int[] cell = queue.poll();
for (int k = 0; k < 4; k++) {
int nr = cell[0] + DR[k], nc = cell[1] + DC[k];
if (nr >= 0 && nr < n && nc >= 0 && nc < m && land[nr][nc] && !seen[nr][nc]) {
seen[nr][nc] = true;
queue.add(new int[]{nr, nc});
}
}
}
}
}
return count;
}
Complexity. Time O(k · n · m): each of the k recounts visits every cell a constant number of times. Space O(n · m) for the grid, the seen array and the queue. With k = 10^4 and a 10^4-cell grid that is already 10^8 cell visits, and it grows with both inputs.
Approach 2: Disjoint Set, update the count incrementally
Intuition. Think about what one operation does to the island count:
- The new land cell is, on its own, a new island:
+1. - For each of its up to four land neighbours, if that neighbour belongs to a different island from the one the new cell is now part of, the two islands become one:
-1. - If two neighbours already belong to the same island (they are connected around some loop), joining the second one changes nothing.
Distinguishing “different island” from “same island” is exactly what a Disjoint Set answers. Number the cells r * m + c, start each one in its own set, and union the new cell with each land neighbour. union returns true only when it merged two different sets, so every true means one fewer island.
The sets of water cells are never touched, so they never affect the count. Only cells that have been switched to land participate in unions.
Why not simply store an island label in every cell and relabel on a merge? That works if you always relabel the smaller island (each cell then changes label at most log2(n · m) times), but it needs a member list per island and a single merge can still touch thousands of cells. The Disjoint Set gets the same effect with two arrays and does the relabelling lazily: a merge rewires one root pointer, and path compression fixes the other pointers the next time they are used.
static final class DisjointSet {
final int[] parent, size;
DisjointSet(int n) {
parent = new int[n];
size = new int[n];
for (int i = 0; i < n; i++) {
parent[i] = i;
size[i] = 1;
}
}
int find(int x) {
if (parent[x] != x) parent[x] = find(parent[x]); // path compression
return parent[x];
}
boolean union(int a, int b) {
int ra = find(a), rb = find(b);
if (ra == rb) return false; // already in the same set
if (size[ra] < size[rb]) {
int t = ra;
ra = rb;
rb = t;
}
parent[rb] = ra; // hang the smaller tree under the larger
size[ra] += size[rb];
return true;
}
}
Algorithm.
- Create a Disjoint Set over
n · mcell ids, an all-waterlandgrid, andislands = 0. - For each operation
(r, c):- If
land[r][c]is already true, appendislandsunchanged and continue. - Mark it land and do
islands++. - For each of the four neighbours that is inside the grid and is land,
unionit with(r, c); on success doislands--. - Append
islands.
- If
static List<Integer> disjointSet(int n, int m, int[][] operators) {
DisjointSet ds = new DisjointSet(n * m);
boolean[][] land = new boolean[n][m];
List<Integer> answer = new ArrayList<>();
int islands = 0;
for (int[] op : operators) {
int r = op[0], c = op[1];
if (land[r][c]) { // repeated operation: nothing changes
answer.add(islands);
continue;
}
land[r][c] = true;
islands++;
for (int k = 0; k < 4; k++) {
int nr = r + DR[k], nc = c + DC[k];
if (nr >= 0 && nr < n && nc >= 0 && nc < m && land[nr][nc]) {
if (ds.union(r * m + c, nr * m + nc)) islands--;
}
}
answer.add(islands);
}
return answer;
}
Dry run on example 2 (3 x 3, cell id = 3r + c):
| op | new id | land neighbours | successful unions | islands |
|---|---|---|---|---|
| (0,1) | 1 | none | 0 | 1 |
| (1,0) | 3 | none | 0 | 2 |
| (1,2) | 5 | none | 0 | 3 |
| (2,1) | 7 | none | 0 | 4 |
| (1,1) | 4 | 1, 7, 3, 5 | 4 | 4 + 1 - 4 = 1 |
The last row is the important one: four unions succeed because the four neighbours were four different islands.
Complexity. Time O(n · m + k · α(n · m)): creating the Disjoint Set is linear in the grid size, and each operation performs at most four unions of near-constant cost. Space O(n · m) for the Disjoint Set arrays and the land grid.
Approach 2 is an example of a general principle. When a structure only ever grows (edges or vertices are added, never removed), a Disjoint Set can maintain connectivity online at almost no cost. Deletions are what it cannot handle; for those, the usual trick is to process operations in reverse so that deletions become additions.
Edge cases and pitfalls
- Repeated operations. Turning an existing land cell into land again must not add
+1. Without theland[r][c]check,[(0,0), (0,0)]would report[1, 2]instead of[1, 1]. - Neighbours that are water. Only union with neighbours that are already land. Unioning with a water neighbour would put that water cell inside an island’s set; two islands that both touch it would then look connected through water, and later unions that should succeed would fail.
- Index mapping. The id is
r * m + c, wheremis the number of columns. Usingr * n + con a non-square grid maps two cells to one id. - Bounds. Check
0 ≤ nr < nand0 ≤ nc < mbefore readingland[nr][nc]. - Single cell grid.
1 x 1with[(0,0)]gives[1]. - No operations. Return an empty list.
Related problems
- Number of islands is the static version, solved by the BFS used in approach 1.
- Disjoint Set builds the union-find structure from scratch.
- Making a large island also uses a Disjoint Set over grid cells, with island sizes.
- Number of operations to make network connected counts components with the same “successful union means one fewer group” rule.