Connected components
Connected components split an undirected graph into maximal groups of mutually reachable vertices; one search per unvisited vertex finds them, and grids turn this into islands and flood fill.
Learning goals
- Count and label components with repeated searches
- Count islands and fill regions on a grid
- Collect the size of every region
- Pick between search and union-find for connectivity
Prerequisites
Are all the computers on a network able to talk to each other? How many separate islands are on a map? Both questions ask for the connected components of a graph.
Undirected components
A **connected component** is a maximal set of vertices in which every two are joined by a path. Every vertex is in exactly one component, and no edge joins two components. To count them, loop over all vertices; whenever you meet one that is unvisited, run a search from it (which marks its entire component) and add 1 to the count.
The loop plus the searches visit each vertex and edge once, so the total is O(n + m). The animation's *Label connected components* option, with the preset of three groups, shows the count increase once per new search start.
1static int countComponentsBfs(List<List<Integer>> g) {2boolean[] seen = new boolean[g.size()];3int count = 0;4for (int start = 0; start < g.size(); start++) {5if (seen[start]) continue;6count++; // a new, unvisited vertex begins a new component7Deque<Integer> queue = new ArrayDeque<>();8seen[start] = true;9queue.add(start);10while (!queue.isEmpty()) {11int u = queue.poll();12for (int v : g.get(u))13if (!seen[v]) { seen[v] = true; queue.add(v); }14}15}16return count;17}
Components, two-colouring and cycle detection in an undirected graph
Interactive · edit the inputundirected graph · 8 vertices · 6 edges
component of each vertex
Search from an unlabelled vertex: everything the search reaches is one component. Repeat from the next unlabelled vertex until every vertex has a label.
1int[] components(List<List<Integer>> g) { // component id per vertex2int n = g.size(); int[] comp = new int[n]; Arrays.fill(comp, -1);3int id = 0;4for (int s = 0; s < n; s++) {5if (comp[s] != -1) continue;6Deque<Integer> q = new ArrayDeque<>(); q.add(s); comp[s] = id;7while (!q.isEmpty()) {8int u = q.poll();9for (int v : g.get(u)) if (comp[v] == -1) { comp[v] = id; q.add(v); }10}11id++; // the search ended: one whole component labelled12}13return comp;14}15boolean isBipartite(List<List<Integer>> g) {16int n = g.size(); int[] color = new int[n]; Arrays.fill(color, -1);17for (int s = 0; s < n; s++) { // check every component18if (color[s] != -1) continue;19color[s] = 0; Deque<Integer> q = new ArrayDeque<>(); q.add(s);20while (!q.isEmpty()) {21int u = q.poll();22for (int v : g.get(u)) {23if (color[v] == -1) { color[v] = color[u] ^ 1; q.add(v); }24else if (color[v] == color[u]) return false; // an odd cycle25}26}27}28return true;29}30boolean hasCycle(int n, List<int[]>[] g) { // undirected; g[u] holds {v, edgeId}31boolean[] seen = new boolean[n];32for (int s = 0; s < n; s++) if (!seen[s] && dfsCycle(g, s, -1, seen)) return true;33return false;34}35boolean dfsCycle(List<int[]>[] g, int u, int viaEdge, boolean[] seen) {36seen[u] = true;37for (int[] e : g[u]) {38if (e[1] == viaEdge) continue; // do not walk back along the edge we came by39if (seen[e[0]] || dfsCycle(g, e[0], e[1], seen)) return true; // a seen vertex reached by another edge: a cycle40}41return false;42}
Live state
vertices
8
edges
6
Execution trace (dry run table)Show 13 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | vertices=8 edges=6 | Search from an unlabelled vertex: everything the search reaches is one component. Repeat from the next unlabelled vertex until every vertex has a label. |
| 2 | L6 | start=0 component=0 | Vertex 0 is unlabelled: start component C1 here. |
| 3 | L9 | u=0 v=1 component=0 | 0 - 1: 1 is unlabelled, so it belongs to C1 too. |
| 4 | L9 | u=1 v=2 component=0 | 1 - 2: 2 is unlabelled, so it belongs to C1 too. |
| 5 | L11 | components=1 | The queue is empty: component C1 is complete. 1 so far. |
| 6 | L6 | start=3 component=1 | Vertex 3 is unlabelled: start component C2 here. |
| 7 | L9 | u=3 v=4 component=1 | 3 - 4: 4 is unlabelled, so it belongs to C2 too. |
| 8 | L11 | components=2 | The queue is empty: component C2 is complete. 2 so far. |
| 9 | L6 | start=5 component=2 | Vertex 5 is unlabelled: start component C3 here. |
| 10 | L9 | u=5 v=6 component=2 | 5 - 6: 6 is unlabelled, so it belongs to C3 too. |
| 11 | L9 | u=5 v=7 component=2 | 5 - 7: 7 is unlabelled, so it belongs to C3 too. |
| 12 | L11 | components=3 | The queue is empty: component C3 is complete. 3 so far. |
| 13 | L13 | return=3 sizes=[3, 2, 3] | 3 components with sizes [3, 2, 3]. A vertex with no edge is a component of size 1. Cost O(V + E). |
Component labeling
Counting is rarely enough: you usually want to know *which* component each vertex belongs to. Keep an array comp[v] and write the current label while searching. Afterwards comp[a] == comp[b] answers "are a and b connected?" in O(1), and grouping by label lists each component.
This is a good place to use an iterative stack, because a component can be as big as the whole graph and a recursive search might overflow on a long path.
1static int[] labelComponents(List<List<Integer>> g) { // comp[v] = 0, 1, 2, ... in order of the lowest vertex2int n = g.size();3int[] comp = new int[n];4Arrays.fill(comp, -1);5int label = 0;6for (int s = 0; s < n; s++) {7if (comp[s] != -1) continue;8Deque<Integer> stack = new ArrayDeque<>();9comp[s] = label;10stack.push(s);11while (!stack.isEmpty()) {12int u = stack.pop();13for (int v : g.get(u))14if (comp[v] == -1) { comp[v] = label; stack.push(v); }15}16label++;17}18return comp;19}
Components, two-colouring and cycle detection in an undirected graph
Interactive · edit the inputundirected graph · 8 vertices · 6 edges
component of each vertex
Search from an unlabelled vertex: everything the search reaches is one component. Repeat from the next unlabelled vertex until every vertex has a label.
1int[] components(List<List<Integer>> g) { // component id per vertex2int n = g.size(); int[] comp = new int[n]; Arrays.fill(comp, -1);3int id = 0;4for (int s = 0; s < n; s++) {5if (comp[s] != -1) continue;6Deque<Integer> q = new ArrayDeque<>(); q.add(s); comp[s] = id;7while (!q.isEmpty()) {8int u = q.poll();9for (int v : g.get(u)) if (comp[v] == -1) { comp[v] = id; q.add(v); }10}11id++; // the search ended: one whole component labelled12}13return comp;14}15boolean isBipartite(List<List<Integer>> g) {16int n = g.size(); int[] color = new int[n]; Arrays.fill(color, -1);17for (int s = 0; s < n; s++) { // check every component18if (color[s] != -1) continue;19color[s] = 0; Deque<Integer> q = new ArrayDeque<>(); q.add(s);20while (!q.isEmpty()) {21int u = q.poll();22for (int v : g.get(u)) {23if (color[v] == -1) { color[v] = color[u] ^ 1; q.add(v); }24else if (color[v] == color[u]) return false; // an odd cycle25}26}27}28return true;29}30boolean hasCycle(int n, List<int[]>[] g) { // undirected; g[u] holds {v, edgeId}31boolean[] seen = new boolean[n];32for (int s = 0; s < n; s++) if (!seen[s] && dfsCycle(g, s, -1, seen)) return true;33return false;34}35boolean dfsCycle(List<int[]>[] g, int u, int viaEdge, boolean[] seen) {36seen[u] = true;37for (int[] e : g[u]) {38if (e[1] == viaEdge) continue; // do not walk back along the edge we came by39if (seen[e[0]] || dfsCycle(g, e[0], e[1], seen)) return true; // a seen vertex reached by another edge: a cycle40}41return false;42}
Live state
vertices
8
edges
6
Execution trace (dry run table)Show 13 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | vertices=8 edges=6 | Search from an unlabelled vertex: everything the search reaches is one component. Repeat from the next unlabelled vertex until every vertex has a label. |
| 2 | L6 | start=0 component=0 | Vertex 0 is unlabelled: start component C1 here. |
| 3 | L9 | u=0 v=1 component=0 | 0 - 1: 1 is unlabelled, so it belongs to C1 too. |
| 4 | L9 | u=1 v=2 component=0 | 1 - 2: 2 is unlabelled, so it belongs to C1 too. |
| 5 | L11 | components=1 | The queue is empty: component C1 is complete. 1 so far. |
| 6 | L6 | start=3 component=1 | Vertex 3 is unlabelled: start component C2 here. |
| 7 | L9 | u=3 v=4 component=1 | 3 - 4: 4 is unlabelled, so it belongs to C2 too. |
| 8 | L11 | components=2 | The queue is empty: component C2 is complete. 2 so far. |
| 9 | L6 | start=5 component=2 | Vertex 5 is unlabelled: start component C3 here. |
| 10 | L9 | u=5 v=6 component=2 | 5 - 6: 6 is unlabelled, so it belongs to C3 too. |
| 11 | L9 | u=5 v=7 component=2 | 5 - 7: 7 is unlabelled, so it belongs to C3 too. |
| 12 | L11 | components=3 | The queue is empty: component C3 is complete. 3 so far. |
| 13 | L13 | return=3 sizes=[3, 2, 3] | 3 components with sizes [3, 2, 3]. A vertex with no edge is a component of size 1. Cost O(V + E). |
Number of islands
A grid is an implicit graph: each cell is a vertex and two land cells are joined if they touch up, down, left or right. The **number of islands** is the number of connected components of land cells, so scan the grid and start a search at every land cell not seen before.
Each cell is visited once and has four neighbours, so the time is O(rows x cols). Switch the animation to *Count islands* to watch each search colour one island.
1static int numIslands(int[][] grid) { // 1 = land, 0 = water; 4-direction connectivity2int rows = grid.length, cols = grid[0].length, islands = 0;3boolean[][] seen = new boolean[rows][cols];4int[][] dirs = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};5for (int r = 0; r < rows; r++)6for (int c = 0; c < cols; c++) {7if (grid[r][c] == 0 || seen[r][c]) continue;8islands++;9Deque<int[]> stack = new ArrayDeque<>();10seen[r][c] = true;11stack.push(new int[]{r, c});12while (!stack.isEmpty()) {13int[] cell = stack.pop();14for (int[] d : dirs) {15int nr = cell[0] + d[0], nc = cell[1] + d[1];16if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && grid[nr][nc] == 1 && !seen[nr][nc]) {17seen[nr][nc] = true;18stack.push(new int[]{nr, nc});19}20}21}22}23return islands;24}
Flood fill and counting islands (DFS with a stack)
Interactive · edit the inputgrid · 3 × 4
Scan every cell; each unvisited land cell starts a new island, and a DFS marks the whole island so it is never counted twice.
1static int countIslands(int[][] g) {2int count = 0;3for (int r = 0; r < g.length; r++)4for (int c = 0; c < g[0].length; c++)5if (g[r][c] == 1) { count++; fill(g, r, c); } // each unvisited land cell starts an island6return count;7}8static int fill(int[][] g, int sr, int sc) { // iterative DFS, marks land as 29Deque<int[]> stack = new ArrayDeque<>(); stack.push(new int[]{sr, sc}); g[sr][sc] = 2; int size = 0;10while (!stack.isEmpty()) {11int[] cell = stack.pop(); size++;12for (int[] d : new int[][]{{-1, 0}, {0, 1}, {1, 0}, {0, -1}}) {13int r = cell[0] + d[0], c = cell[1] + d[1];14if (r >= 0 && r < g.length && c >= 0 && c < g[0].length && g[r][c] == 1) { g[r][c] = 2; stack.push(new int[]{r, c}); }15}16}17return size;18}
Live state
R
3
C
4
Execution trace (dry run table)Show 10 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | R=3 C=4 | Scan every cell; each unvisited land cell starts a new island, and a DFS marks the whole island so it is never counted twice. |
| 2 | L5 | r=0 c=0 islands=1 | Unvisited land at (0,0): island number 1 begins. |
| 3 | L11 | cell=(0,0) islands=1 size=1 | Pop (0,0). Unvisited land neighbours pushed: (0,1), (1,0). |
| 4 | L11 | cell=(1,0) islands=1 size=2 | Pop (1,0). No unvisited land neighbours. |
| 5 | L11 | cell=(0,1) islands=1 size=3 | Pop (0,1). No unvisited land neighbours. |
| 6 | L5 | r=1 c=3 islands=2 | Unvisited land at (1,3): island number 2 begins. |
| 7 | L11 | cell=(1,3) islands=2 size=1 | Pop (1,3). Unvisited land neighbours pushed: (2,3). |
| 8 | L11 | cell=(2,3) islands=2 size=2 | Pop (2,3). Unvisited land neighbours pushed: (2,2). |
| 9 | L11 | cell=(2,2) islands=2 size=3 | Pop (2,2). No unvisited land neighbours. |
| 10 | L6 | return=2 | 2 islands. Every cell is visited at most once: O(R x C) time, O(R x C) stack in the worst case. |
Flood fill
**Flood fill** is the paint-bucket tool: starting at a cell, change the colour of every cell reachable through cells of the *same* original colour. It is one search from the start cell, not a scan of the whole grid.
One trap: if the new colour equals the old colour, nothing should change, and a search that marks cells by their colour would never stop. Return early in that case. The animation's *Flood fill from the start cell* option shows the stack and the filled region.
1static int[][] floodFill(int[][] image, int sr, int sc, int colour) {2int old = image[sr][sc];3if (old == colour) return image; // already that colour: nothing to do (and no endless loop)4int rows = image.length, cols = image[0].length;5int[][] dirs = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};6Deque<int[]> stack = new ArrayDeque<>();7image[sr][sc] = colour;8stack.push(new int[]{sr, sc});9while (!stack.isEmpty()) {10int[] cell = stack.pop();11for (int[] d : dirs) {12int nr = cell[0] + d[0], nc = cell[1] + d[1];13if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && image[nr][nc] == old) {14image[nr][nc] = colour; // repainting is the visited mark15stack.push(new int[]{nr, nc});16}17}18}19return image;20}
Flood fill and counting islands (DFS with a stack)
Interactive · edit the inputgrid · 3 × 4
Scan every cell; each unvisited land cell starts a new island, and a DFS marks the whole island so it is never counted twice.
1static int countIslands(int[][] g) {2int count = 0;3for (int r = 0; r < g.length; r++)4for (int c = 0; c < g[0].length; c++)5if (g[r][c] == 1) { count++; fill(g, r, c); } // each unvisited land cell starts an island6return count;7}8static int fill(int[][] g, int sr, int sc) { // iterative DFS, marks land as 29Deque<int[]> stack = new ArrayDeque<>(); stack.push(new int[]{sr, sc}); g[sr][sc] = 2; int size = 0;10while (!stack.isEmpty()) {11int[] cell = stack.pop(); size++;12for (int[] d : new int[][]{{-1, 0}, {0, 1}, {1, 0}, {0, -1}}) {13int r = cell[0] + d[0], c = cell[1] + d[1];14if (r >= 0 && r < g.length && c >= 0 && c < g[0].length && g[r][c] == 1) { g[r][c] = 2; stack.push(new int[]{r, c}); }15}16}17return size;18}
Live state
R
3
C
4
Execution trace (dry run table)Show 10 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | R=3 C=4 | Scan every cell; each unvisited land cell starts a new island, and a DFS marks the whole island so it is never counted twice. |
| 2 | L5 | r=0 c=0 islands=1 | Unvisited land at (0,0): island number 1 begins. |
| 3 | L11 | cell=(0,0) islands=1 size=1 | Pop (0,0). Unvisited land neighbours pushed: (0,1), (1,0). |
| 4 | L11 | cell=(1,0) islands=1 size=2 | Pop (1,0). No unvisited land neighbours. |
| 5 | L11 | cell=(0,1) islands=1 size=3 | Pop (0,1). No unvisited land neighbours. |
| 6 | L5 | r=1 c=3 islands=2 | Unvisited land at (1,3): island number 2 begins. |
| 7 | L11 | cell=(1,3) islands=2 size=1 | Pop (1,3). Unvisited land neighbours pushed: (2,3). |
| 8 | L11 | cell=(2,3) islands=2 size=2 | Pop (2,3). Unvisited land neighbours pushed: (2,2). |
| 9 | L11 | cell=(2,2) islands=2 size=3 | Pop (2,2). No unvisited land neighbours. |
| 10 | L6 | return=2 | 2 islands. Every cell is visited at most once: O(R x C) time, O(R x C) stack in the worst case. |
Region sizes
Counting the cells found by each search gives the **size** of each region. Collect the sizes in a list to get the largest island, the smallest, or a size distribution. The only change from counting islands is a counter that restarts at each new search and is saved when the search ends.
Sizes are also how you answer "is this region big enough?" problems, such as the area of the biggest island or the number of cells that can never reach the border.
1static List<Integer> regionSizes(int[][] grid) { // size of every 4-connected group of 1s, in scan order2int rows = grid.length, cols = grid[0].length;3boolean[][] seen = new boolean[rows][cols];4int[][] dirs = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};5List<Integer> sizes = new ArrayList<>();6for (int r = 0; r < rows; r++)7for (int c = 0; c < cols; c++) {8if (grid[r][c] == 0 || seen[r][c]) continue;9int size = 0;10Deque<int[]> stack = new ArrayDeque<>();11seen[r][c] = true;12stack.push(new int[]{r, c});13while (!stack.isEmpty()) {14int[] cell = stack.pop();15size++;16for (int[] d : dirs) {17int nr = cell[0] + d[0], nc = cell[1] + d[1];18if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && grid[nr][nc] == 1 && !seen[nr][nc]) {19seen[nr][nc] = true;20stack.push(new int[]{nr, nc});21}22}23}24sizes.add(size);25}26return sizes;27}
Flood fill and counting islands (DFS with a stack)
Interactive · edit the inputgrid · 3 × 4
Scan every cell; each unvisited land cell starts a new island, and a DFS marks the whole island so it is never counted twice.
1static int countIslands(int[][] g) {2int count = 0;3for (int r = 0; r < g.length; r++)4for (int c = 0; c < g[0].length; c++)5if (g[r][c] == 1) { count++; fill(g, r, c); } // each unvisited land cell starts an island6return count;7}8static int fill(int[][] g, int sr, int sc) { // iterative DFS, marks land as 29Deque<int[]> stack = new ArrayDeque<>(); stack.push(new int[]{sr, sc}); g[sr][sc] = 2; int size = 0;10while (!stack.isEmpty()) {11int[] cell = stack.pop(); size++;12for (int[] d : new int[][]{{-1, 0}, {0, 1}, {1, 0}, {0, -1}}) {13int r = cell[0] + d[0], c = cell[1] + d[1];14if (r >= 0 && r < g.length && c >= 0 && c < g[0].length && g[r][c] == 1) { g[r][c] = 2; stack.push(new int[]{r, c}); }15}16}17return size;18}
Live state
R
3
C
4
Execution trace (dry run table)Show 10 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | R=3 C=4 | Scan every cell; each unvisited land cell starts a new island, and a DFS marks the whole island so it is never counted twice. |
| 2 | L5 | r=0 c=0 islands=1 | Unvisited land at (0,0): island number 1 begins. |
| 3 | L11 | cell=(0,0) islands=1 size=1 | Pop (0,0). Unvisited land neighbours pushed: (0,1), (1,0). |
| 4 | L11 | cell=(1,0) islands=1 size=2 | Pop (1,0). No unvisited land neighbours. |
| 5 | L11 | cell=(0,1) islands=1 size=3 | Pop (0,1). No unvisited land neighbours. |
| 6 | L5 | r=1 c=3 islands=2 | Unvisited land at (1,3): island number 2 begins. |
| 7 | L11 | cell=(1,3) islands=2 size=1 | Pop (1,3). Unvisited land neighbours pushed: (2,3). |
| 8 | L11 | cell=(2,3) islands=2 size=2 | Pop (2,3). Unvisited land neighbours pushed: (2,2). |
| 9 | L11 | cell=(2,2) islands=2 size=3 | Pop (2,2). No unvisited land neighbours. |
| 10 | L6 | return=2 | 2 islands. Every cell is visited at most once: O(R x C) time, O(R x C) stack in the worst case. |
Practice ladder
Now try it yourself
Easy
- Flood Fill
- Island Perimeter
Medium
- Number of Islands
- Max Area of Island
- Number of Provinces
- Number of Connected Components in an Undirected Graph
Advanced
- Surrounded Regions
- Number of Enclaves
- Making A Large Island
Key takeaways
- Loop over vertices and start a search whenever one is unvisited; the number of starts is the number of components.
- Store a label per vertex to answer connectivity queries in O(1).
- A grid is an implicit graph; islands are its components and flood fill is one search.
- Count cells in each search to get region sizes.