Topics › Graphs › MinimumSpanningTree/Disjoint Set and Problems
Making a large island
LeetCode ↗ Java source ↗ Edit this article ↗
Jump to code:
Problem
You are given an n x n binary grid, where 1 is land and 0 is water. You may change at most one 0 into a 1. Return the size of the largest island possible afterwards. An island is a group of 1s connected horizontally or vertically.
Constraints: 1 ≤ n ≤ 500, every cell is 0 or 1.
Example 1
grid = [[1,0],
[0,1]]
answer = 3 // flip either 0; it touches both single-cell islands: 1 + 1 + 1
Example 2
grid = [[1,1],
[1,1]]
answer = 4 // there is no 0 to flip; the whole grid is already one island
Example 3
grid = [[1,1,0],
[0,0,0],
[0,1,1]]
answer = 5 // flip the centre: it joins the top island (2) and the bottom island (2)
Approach 1: Brute force, try every flip
Intuition. There are only as many choices as there are 0s. For each one, flip it, measure the island that now contains it with a BFS, and flip it back. Only the island containing the flipped cell can have grown, and any island not touching it is no larger than the island we would get by flipping a cell next to it, so the maximum over all flips is the answer. The one case this misses is a grid with no 0 at all; then the whole grid is one island of size n^2.
Algorithm.
- For every cell
(r, c)with value0: set it to1, computecomponentSize(r, c)with BFS, restore it to0, and keep the maximum. - If no
0was found, returnn * n.
static int bruteForce(int[][] grid) {
int n = grid.length, best = 0;
boolean hasZero = false;
for (int r = 0; r < n; r++) {
for (int c = 0; c < n; c++) {
if (grid[r][c] == 1) continue;
hasZero = true;
grid[r][c] = 1; // flip, measure, restore
best = Math.max(best, componentSize(grid, r, c));
grid[r][c] = 0;
}
}
return hasZero ? best : n * n; // no 0 to flip: the whole grid is one island
}
static int componentSize(int[][] grid, int sr, int sc) {
int n = grid.length, size = 0;
boolean[][] seen = new boolean[n][n];
Deque<int[]> queue = new ArrayDeque<>();
seen[sr][sc] = true;
queue.add(new int[]{sr, sc});
while (!queue.isEmpty()) {
int[] cell = queue.poll();
size++;
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 < n && grid[nr][nc] == 1 && !seen[nr][nc]) {
seen[nr][nc] = true;
queue.add(new int[]{nr, nc});
}
}
}
return size;
}
Complexity. Time O(n^4): up to n^2 zeros, and each BFS can visit all n^2 cells. Space O(n^2) for the seen array and the queue. For n = 500 that is around 6 × 10^10 steps, hopeless.
The waste is that every BFS rediscovers the same islands again and again. The islands never change; only the cell we flip does.
Approach 2: Optimal, Disjoint Set with island sizes
Intuition. Precompute the islands once. Then, for a 0 cell, flipping it creates an island made of the cell itself plus every island it touches. If we know which island each neighbour belongs to and how big each island is, the result is
1 + sum of sizes of the distinct islands among its four neighbours
and that takes constant time per cell.
A Disjoint Set with union by size gives both pieces of information for free: find(cell) names the island, and size[root] is how many cells it has. The important word in the formula is distinct. A 0 cell surrounded on several sides by the same island must count that island once, which is why the roots are collected in a Set.
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.
- Number the cells
r * n + cand create a Disjoint Set over alln^2of them. - Pass 1: for every land cell, union it with its land neighbour below and to the right. Looking in only these two directions is enough because every adjacent pair is then examined exactly once.
- Pass 2: for every cell:
- If it is land, update the answer with the size of its island. This handles a grid with no
0. - If it is water, collect the roots of its land neighbours in a set, add their sizes to
1, and update the answer.
- If it is land, update the answer with the size of its island. This handles a grid with no
- Return the best value.
static int optimal(int[][] grid) {
int n = grid.length;
DisjointSet ds = new DisjointSet(n * n);
for (int r = 0; r < n; r++) { // step 1: union every pair of adjacent 1s
for (int c = 0; c < n; c++) {
if (grid[r][c] == 0) continue;
if (r + 1 < n && grid[r + 1][c] == 1) ds.union(r * n + c, (r + 1) * n + c);
if (c + 1 < n && grid[r][c + 1] == 1) ds.union(r * n + c, r * n + c + 1);
}
}
int best = 0;
for (int r = 0; r < n; r++) { // step 2: evaluate every cell
for (int c = 0; c < n; c++) {
if (grid[r][c] == 1) {
best = Math.max(best, ds.size[ds.find(r * n + c)]); // covers the all-ones grid
continue;
}
Set<Integer> roots = new HashSet<>(); // distinct islands around this 0
for (int k = 0; k < 4; k++) {
int nr = r + DR[k], nc = c + DC[k];
if (nr >= 0 && nr < n && nc >= 0 && nc < n && grid[nr][nc] == 1) roots.add(ds.find(nr * n + nc));
}
int total = 1; // the flipped cell itself
for (int root : roots) total += ds.size[root];
best = Math.max(best, total);
}
}
return best;
}
Dry run on example 3. Pass 1 builds island A = {(0,0), (0,1)} with size 2 and island B = {(2,1), (2,2)} with size 2. Pass 2 on the water cells:
| cell | land neighbours | distinct roots | total |
|---|---|---|---|
| (0,2) | (0,1) | A | 1 + 2 = 3 |
| (1,0) | (0,0) | A | 3 |
| (1,1) | (0,1), (2,1) | A, B | 1 + 2 + 2 = 5 |
| (1,2) | (2,2) | B | 3 |
| (2,0) | (2,1) | B | 3 |
The best is 5, from flipping the centre.
Complexity. Time O(n^2 · α(n^2)): each pass touches every cell once and does at most four find calls per cell. Space O(n^2) for the Disjoint Set arrays; the per-cell root set has at most four entries.
The same plan works without a Disjoint Set: run a DFS that paints each island with a distinct id (say starting from 2) and records its size in a map, then do pass 2 with those ids. Both versions are O(n^2); the Disjoint Set version avoids recursion and reuses the structure from the other problems in this section.
Why de-duplication matters
Consider this grid:
1 1 1
1 0 1
1 1 1
The ring is a single island of size 8 and the centre touches it on all four sides. Without the Set, the formula would compute 1 + 8 + 8 + 8 + 8 = 33, which is larger than the grid. With the set, there is one distinct root and the answer is 1 + 8 = 9, the whole grid. The Java tests include this case.
Edge cases and pitfalls
- No zeros. Pass 2 only sees land cells, so the answer must come from island sizes; that is why land cells also update
best. The brute force handles it with thehasZeroflag. Returning0or1here is a common bug. - All zeros. Every flip creates an island of size
1. The answer is1, including the1 x 1grid[[0]]. - Same island on several sides. Collect roots in a set before summing, as shown above.
- Calling
sizeon a non-root.size[x]is only meaningful whenxis a root. Always readsize[find(x)]. - Mutating the input. The brute force flips cells in place and restores them. Forgetting to restore makes later flips, and any later caller of the method, see a corrupted grid. The tests check the grid is unchanged afterwards.
- Overflow. At most
500 × 500 = 250000cells, sointis enough everywhere.
Related problems
- Number of islands counts islands; approach 1’s BFS comes from there.
- Number of islands II maintains islands with a Disjoint Set while land is being added.
- Disjoint Set explains union by size, whose
sizearray this problem reads directly. - Most stones removed with same row or column is another component-counting problem on a grid of coordinates.