Island Perimeter Problem

This problem is officially posted on LeetCode 463, but I’m bringing it back here for a fresh spin… and will walk you through three different ways to crack it:

  • Exploring with Breadth-First Search
  • Diving in with Depth-First Search
  • Counting Those Borders

Problem

You are given row x col grid representing a map where grid[i][j] = 1 represents land and grid[i][j] = 0 represents water.

Grid cells are connected horizontally/vertically (not diagonally). The grid is completely surrounded by water, and there is exactly one island (i.e., one or more connected land cells).

The island doesn’t have “lakes”, meaning the water inside isn’t connected to the water around the island. One cell is a square with side length 1. The grid is rectangular, width and height don’t exceed 100. Determine the perimeter of the island.

Example 1:

Input: grid = [[0,1,0,0],[1,1,1,0],[0,1,0,0],[1,1,0,0]]
Output: 16
Explanation: The perimeter is the 16 yellow stripes in the image above.

Example 2:

Input: grid = [[1]]
Output: 4

Example 3:

Input: grid = [[1,0]]
Output: 4

Constraints:

  • row == grid.length
  • col == grid[i].length
  • 1 <= row, col <= 100
  • grid[i][j] is 0 or 1.
  • There is exactly one island in grid.

Exploring with Breadth-First Search

The idea is:

  • Step one: Find a land cell to start with—any land cell will do, no need to be picky.
  • Step two: Start exploring in four directions—up, down, left, right—like an overly excited tourist.
  • Step three: Every time you bump into water, stop wandering in that direction and count it toward the perimeter.
  • Step four: Keep the exploration going until there’s no land left to check

It’s like trying to find the edges of a pizza slice 🍕… but instead of cheese and crust, it’s land and water

Algorithm

  1. Initialization:
      • Initialize perimeter = 0.
      • Create a queue to store [row, col] coordinates of cells to visit.

    Create a visited set to keep track of land cells already visited, preventing infinite loops and redundant calculations.

  2. Find Entry Land Cell: Iterate through the grid to find the first land cell. This will be the starting point for our BFS.
  3. Start BFS:
    • Add the starting cell’s coordinates [startRow, startCol] to the queue.
    • Add the starting cell’s coordinate string “${startRow},${startCol}” to the visited set.
  4. BFS Loop: While the queue is not empty:
    • Dequeue the current cell [row, col].
    • Examine its four neighbors (up, down, left, right): [nr, nc].
    • For each neighbor [nextRow, nextCol]:
      • Check Perimeter Contribution:
        • If the neighbor is out of bounds OR the neighbor is water, increment perimeter.
      • Check for Exploration:
        • If the neighbor is land AND has not been visited yet then:
          • Mark the neighbor as visited
          • Enqueue the neighbor: queue.push([nextRow, nextCol]).
  5. Return Result: Once the queue is empty, return the final perimeter count.


function islandPerimeter(grid) {
  const rows = grid.length
  const cols = grid[0].length
  const directions = [
    [ 0,  1],
    [ 0, -1],
    [ 1,  0],
    [-1,  0]
  ] // Right, Left, Down, Up
  const visitedCells = new Set() // To store "row,col" strings of visited land cells
  const cellsQueue = [] // Queue for BFS: stores [row, col] pairs
  let perimeter = 0

  // 1. Find the first land cell to start BFS
  const [startRow, startCol] = findEntryCell(grid)

  // 2. Initialize BFS
  cellsQueue.push([startRow, startCol])
  visitedCells.add(`${startRow},${startCol}`)

  // 3. BFS Loop
  while (cellsQueue.length > 0) {
    const [row, col] = cellsQueue.shift() // Dequeue the current cell

    // 4. Check neighbors
    for (const [dr, dc] of directions) {
      const nRow = row + dr
      const nCol = col + dc

      // Check for out of bounds OR water neighbor
      if (nRow < 0 || nRow >= rows || nCol < 0 || nCol >= cols || grid[nRow][nCol] === 0) {
        perimeter++
			} else { // Neighbor is within bounds and is land. Check if we have visited it.
        const neighborKey = `${nRow},${nCol}`
        if (!visitedCells.has(neighborKey)) {
          visitedCells.add(neighborKey)
          cellsQueue.push([nRow, nCol])
        }
      }
    }
  }

  return perimeter
}

function findEntryCell(grid) {
  for (let r = 0; r < grid.length; r++) {
    for (let c = 0; c < grid[0].length; c++) {
      if (grid[r][c] === 1) {
        return [r, c] // Return the first land cell found
      }
    }
  }
  return [-1, -1] // No land found. This will never happen based on constraints.
}

// Example Usage:
const grid1 = [
  [0, 1, 0, 0],
  [1, 1, 1, 0],
  [0, 1, 0, 0],
  [1, 1, 0, 0]
]
console.log('Example 1 Output:', islandPerimeter(grid1)) // Expected: 16

const grid2 = [[1]]
console.log('Example 2 Output:', islandPerimeter(grid2)) // Expected: 4

const grid3 = [[1, 0]]
console.log('Example 3 Output:', islandPerimeter(grid3)) // Expected: 4

Diving in with Depth-First Search

The idea here is the same as island exploring! However, we set off in one direction, exploring as far as we can until we inevitably run into water. Once we hit water, we backtrack to where we started and try another path.

Algorithm

  1. islandPerimeter(grid) function:
    • Handles edge cases (empty grid).
    • Finds a starting land cell [startRow, startCol].
    • Initializes a visited set (using Set to store “row,col” strings).
    • Calls the dfs helper function starting from [startRow, startCol].
    • Returns the result of the dfs call.
  2. dfs(grid, r, c, visitedCells) function:
    • Base Case 1 (Perimeter Edge): If the current cell is out of the grid boundaries OR is a water cell. Return 1.
    • Base Case 2 (Already Visited): If the current cell is already in the visited set. Return 0.
    • Recursive Step:
      • Mark the current land cell.
      • Initialize perimeterCount = 0.
      • Recursively call dfs for all four neighbors (up, down, left, right).
      • Add the result returned by each recursive call to perimeterCount.
      • Return the total perimeterCount found by exploring from this cell.

// Find entry cell
function findEntryCell(grid) {
  for (let r = 0; r < grid.length; r++) {
    for (let c = 0; c < grid[0].length; c++) { 
      if (grid[r][c] === 1) { 
        return [r, c] // Return the first land cell found 
      } 
    } 
  } 
  return [-1, -1] // No land found. This will never happen based on constraints. 
} 

// DFS 
function dfs(grid, r, c, visitedCells) { 
  const rows = grid.length;
  const cols = grid[0].length;

  // Base Case 1: Out of bounds or water cell -> contributes 1 to perimeter
  if (r < 0 || r >= rows || c < 0 || c >= cols || grid[r][c] === 0) {
    return 1;
  }

  // Base Case 2: Already visitedCells land cell -> contributes 0 (already processed)
  const key = `${r},${c}`;
  if (visitedCells.has(key)) {
    return 0;
  }

  // Mark current land cell as visited cell
  visitedCells.add(key);

  let perimeterCount = 0;

  // Explore neighbors recursively
  perimeterCount += dfs(grid, r + 1, c, visitedCells); // Down
  perimeterCount += dfs(grid, r - 1, c, visitedCells); // Up
  perimeterCount += dfs(grid, r, c + 1, visitedCells); // Right
  perimeterCount += dfs(grid, r, c - 1, visitedCells); // Left

  return perimeterCount;
}


/**
* Main function to calculate the island perimeter using DFS.
* @param {number[][]} grid
* @return {number}
*/
function islandPerimeter(grid) {
  const [startRow, startCol] = findEntryCell(grid);

  const visitedCells = new Set(); // Keep track of visitedCells LAND cells

  // Start DFS from the first land cell found
  return dfs(grid, startRow, startCol, visitedCells);
};

// Example Usage:
const grid1 = [
  [0, 1, 0, 0],
  [1, 1, 1, 0],
  [0, 1, 0, 0],
  [1, 1, 0, 0]
]
console.log("Example 1 Output:", islandPerimeter(grid1)); // Expected: 16

const grid2 = [[1]];
console.log("Example 2 Output:", islandPerimeter(grid2)); // Expected: 4

const grid3 = [[1,0]];
console.log("Example 3 Output:", islandPerimeter(grid3)); // Expected: 4

Counting Those Borders

This approach is straightforward:

  • We march through every single cell, scanning the land and adding 4 to the perimeter count.
  • But what about its neighbors? If a neighboring cell is also land, we knock off 1 per shared edge.

Essentially, any cozy pair of land cells sharing space will subtract 2 in total, keeping the perimeter count realistic. It’s like a block party where each house claims all four sides until they realize they’ve got friendly neighbors, leading to some fair boundary negotiations! 🎉

Algorithm

  1. Initialize perimeter to 0.
  2. Iterate through every cell of the grid.
  3. If the current cell grid is land:
    • Add 4 to the perimeter
    • Check its four neighbors (up, down, left, right) for sharing boundary
    • Substract 1 for any sharing boundary

/**
 * Calculates the perimeter of the single island in the grid using iteration.
 * @param {number[][]} grid
 * @return {number}
 */
function islandPerimeter(grid) {
  const rows = grid.length
  const cols = grid[0].length
  let perimeter = 0

  for (let r = 0; r < rows; r++) {
    for (let c = 0; c < cols; c++) { 
      // Check if the current cell is land 
      if (grid[r][c] === 1) { 
        perimeter += 4 // Start with 4 sides for a land cell 
        // Check neighbor above 
        if (r > 0 && grid[r - 1][c] === 1) {
          perimeter -= 1 // Subtract shared edge
        }
        // Check neighbor below
        if (r < rows - 1 && grid[r + 1][c] === 1) { 
          perimeter -= 1 // Subtract shared edge 
        } 
        // Check neighbor left 
        if (c > 0 && grid[r][c - 1] === 1) {
          perimeter -= 1 // Subtract shared edge
        }
        // Check neighbor right
        if (c < cols - 1 && grid[r][c + 1] === 1) {
          perimeter -= 1 // Subtract shared edge
        }
      }
    }
  }

  return perimeter
}

// Example Usage:
const grid1 = [
  [0, 1, 0, 0],
  [1, 1, 1, 0],
  [0, 1, 0, 0],
  [1, 1, 0, 0]
]
console.log('Example 1 Output:', islandPerimeter(grid1)) // Expected: 16

const grid2 = [[1]]
console.log('Example 2 Output:', islandPerimeter(grid2)) // Expected: 4

const grid3 = [[1, 0]]
console.log('Example 3 Output:', islandPerimeter(grid3)) // Expected: 4

Talking About Complexity

Time Complexity

No matter which strategy we pick—Couting, DFS, or BFS. We scan through the grid, meeting each land and water cell just once before moving along. So our time complexity is a solid O(R × C)—where R is the number of rows, and C is the number of columns. One-time meet-and-greet with each cell! 🤝

Space Complexity

Counting Solution wins the award of minimalist—it travels light, carries no baggage, and doesn’t need extra storage space. That’s why its space complexity is a sweet O(1) 🏕️

BFS & DFS, these two like to keep track of where they’ve been, so in the worst case, they end up storing R × C visited cells—meaning their space complexity hits O(R × C) 🎒