Files
codetutor/backend/data/questions/battleships-in-a-board.yaml

192 lines
8.8 KiB
YAML

title: Battleships in a Board
slug: battleships-in-a-board
difficulty: medium
leetcode_id: 419
leetcode_url: https://leetcode.com/problems/battleships-in-a-board/
categories:
- arrays
- graphs
patterns:
- matrix-traversal
description: |
Given an `m x n` matrix `board` where each cell is a battleship `'X'` or empty `'.'`, return *the number of **battleships** on the board*.
**Battleships** can only be placed horizontally or vertically on `board`. In other words, they can only be made of the shape `1 x k` (1 row, `k` columns) or `k x 1` (`k` rows, 1 column), where `k` can be of any size. At least one horizontal or vertical cell separates between two battleships (i.e., there are no adjacent battleships).
constraints: |
- `m == board.length`
- `n == board[i].length`
- `1 <= m, n <= 200`
- `board[i][j]` is either `'.'` or `'X'`
examples:
- input: 'board = [["X",".",".","X"],[".",".",".","X"],[".",".",".","X"]]'
output: "2"
explanation: "There are two battleships: one horizontal at position (0,0) and one vertical spanning positions (0,3), (1,3), (2,3)."
- input: 'board = [["."]]'
output: "0"
explanation: "The board contains no battleships, only empty cells."
explanation:
intuition: |
Imagine you're scanning a radar display looking for ships. Each ship appears as a contiguous line of `'X'` marks, either horizontal or vertical. Your task is to count how many distinct ships are present.
The **key insight** is that you don't need to trace the entire shape of each battleship. Instead, you only need to identify the **"head"** of each ship — the top-left cell where a battleship begins.
Think of it like this: as you scan the grid from left to right, top to bottom (like reading a book), the first `'X'` you encounter for any battleship is always its top-left corner. Every other `'X'` in that ship will either be below or to the right of this starting point.
So instead of asking "is this an `'X'`?", ask "is this the **start** of a new battleship?" A cell is the start of a new battleship if:
- It contains `'X'`
- There's no `'X'` directly above it (otherwise it's part of a vertical ship that started earlier)
- There's no `'X'` directly to its left (otherwise it's part of a horizontal ship that started earlier)
This elegant observation lets us count battleships in a single pass without any extra data structures or recursion.
approach: |
We solve this using a **Single Pass (Count Heads)** approach:
**Step 1: Initialise the counter**
- `count`: Set to `0` to track the number of battleships found
&nbsp;
**Step 2: Iterate through each cell**
- Traverse the grid row by row, column by column (standard left-to-right, top-to-bottom order)
- For each cell at position `(i, j)`, check if it's the "head" of a battleship
&nbsp;
**Step 3: Identify battleship heads**
- Skip if `board[i][j] == '.'` (empty cell)
- Skip if `i > 0` and `board[i-1][j] == 'X'` (this cell is part of a vertical ship that started above)
- Skip if `j > 0` and `board[i][j-1] == 'X'` (this cell is part of a horizontal ship that started to the left)
- If none of the above, this is a new battleship head — increment `count`
&nbsp;
**Step 4: Return the result**
- Return `count` after processing all cells
&nbsp;
This approach works because each battleship has exactly one "head" cell (its top-left corner), and we count each head exactly once.
common_pitfalls:
- title: Using DFS/BFS for Each Ship
description: |
A natural instinct is to use flood-fill (DFS or BFS) to explore each battleship when you encounter an `'X'`:
- Mark all connected `'X'` cells as visited
- Increment counter after exploring the entire ship
While this works correctly, it's **overkill** for this problem. The problem statement guarantees that battleships are always straight lines with no adjacency, so you don't need to trace their full extent.
More importantly, DFS/BFS approaches typically require either:
- O(m*n) extra space for a visited array, or
- Modifying the board to mark visited cells
The follow-up explicitly asks for O(1) space without modification.
wrong_approach: "DFS/BFS flood-fill with visited tracking"
correct_approach: "Count only the top-left head of each ship"
- title: Counting Every X Cell
description: |
Simply counting all `'X'` cells gives the wrong answer because a single battleship can span multiple cells.
For example, with a 3-cell vertical ship, counting all `'X'` cells would report 3 instead of 1.
wrong_approach: "Increment counter for every 'X' cell"
correct_approach: "Only count cells that are battleship heads"
- title: Forgetting Boundary Checks
description: |
When checking the cell above (`board[i-1][j]`) or to the left (`board[i][j-1]`), you must ensure you're not accessing invalid indices.
- Only check `board[i-1][j]` when `i > 0`
- Only check `board[i][j-1]` when `j > 0`
Failing to add these guards causes an index-out-of-bounds error on the first row or column.
wrong_approach: "Check neighbors without boundary validation"
correct_approach: "Guard neighbor checks with i > 0 and j > 0"
key_takeaways:
- "**Counting heads pattern**: When objects span multiple cells, identify a unique 'anchor' point (like the top-left corner) to count each object exactly once"
- "**Leverage problem constraints**: The guarantee of no adjacent ships and only horizontal/vertical placement enables the O(1) space solution"
- "**Single-pass matrix traversal**: Scanning in reading order (left-to-right, top-to-bottom) naturally processes heads before their continuations"
- "**Question the obvious approach**: DFS/BFS is often the go-to for connected components, but simpler patterns exist for constrained inputs"
time_complexity: "O(m * n). We visit each cell exactly once, performing O(1) work per cell."
space_complexity: "O(1). We only use a single counter variable, regardless of the board size."
solutions:
- approach_name: Single Pass (Count Heads)
is_optimal: true
code: |
def count_battleships(board: list[list[str]]) -> int:
# Counter for battleship heads
count = 0
m, n = len(board), len(board[0])
for i in range(m):
for j in range(n):
# Skip empty cells
if board[i][j] == '.':
continue
# Skip if this is a continuation of a vertical ship
if i > 0 and board[i - 1][j] == 'X':
continue
# Skip if this is a continuation of a horizontal ship
if j > 0 and board[i][j - 1] == 'X':
continue
# This is a battleship head — count it
count += 1
return count
explanation: |
**Time Complexity:** O(m * n) — Single pass through all cells.
**Space Complexity:** O(1) — Only a counter variable is used.
We scan each cell once and only count `'X'` cells that have no `'X'` neighbor above or to the left. Since we traverse top-to-bottom and left-to-right, such cells are exactly the top-left starting points of each battleship.
- approach_name: DFS Flood Fill
is_optimal: false
code: |
def count_battleships(board: list[list[str]]) -> int:
m, n = len(board), len(board[0])
visited = [[False] * n for _ in range(m)]
count = 0
def dfs(i: int, j: int) -> None:
# Mark current cell as visited
visited[i][j] = True
# Explore all 4 directions
for di, dj in [(0, 1), (0, -1), (1, 0), (-1, 0)]:
ni, nj = i + di, j + dj
# Check bounds, if unvisited, and if part of ship
if 0 <= ni < m and 0 <= nj < n:
if not visited[ni][nj] and board[ni][nj] == 'X':
dfs(ni, nj)
for i in range(m):
for j in range(n):
# Found an unvisited battleship cell
if board[i][j] == 'X' and not visited[i][j]:
dfs(i, j) # Mark entire ship as visited
count += 1 # Count this ship
return count
explanation: |
**Time Complexity:** O(m * n) — Each cell is visited at most once by DFS.
**Space Complexity:** O(m * n) — The visited array plus recursion stack.
This approach uses standard flood-fill DFS to explore each battleship completely when first encountered. While correct, it uses extra space for the visited array and is more complex than necessary given the problem's constraints. Included to show the classic connected-components approach.