419. Battleships in a Board
Problem Description
You are given an m x n
matrix called board
that represents a game board. Each cell in this matrix contains either:
'X'
which represents part of a battleship'.'
which represents an empty cell
Your task is to count how many battleships are on the board.
The battleships follow these rules:
-
Shape: Each battleship can only be placed either horizontally or vertically. This means a battleship has the shape:
1 x k
(1 row and k columns) - horizontal battleshipk x 1
(k rows and 1 column) - vertical battleship- where
k
can be any positive size
-
Separation: Battleships cannot be adjacent to each other. There must be at least one empty cell (horizontally or vertically) separating any two battleships.
The solution leverages a key insight: instead of tracking entire battleships, we can count only the "starting points" of each battleship. For any battleship, we define its starting point as its top-left cell. A cell at position (i, j)
containing 'X'
is a starting point if:
- It's an
'X'
cell - There's no
'X'
directly above it (at position(i-1, j)
) - There's no
'X'
directly to its left (at position(i, j-1)
)
By counting these starting points, we effectively count the number of battleships since each battleship has exactly one starting point.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: We can model this problem as a graph where each cell containing 'X' is a node, and adjacent 'X' cells (horizontally or vertically) form edges between nodes. Each battleship forms a connected component in this graph.
Is it a tree?
- No: The battleships are not structured as trees. They form linear connected components (either horizontal or vertical lines), but multiple battleships exist independently on the board, and there's no hierarchical parent-child relationship.
Is the problem related to directed acyclic graphs?
- No: The problem doesn't involve directed edges or acyclic properties. The battleships are undirected connected components.
Is the problem related to shortest paths?
- No: We're not finding shortest paths between cells. We need to count distinct connected components (battleships).
Does the problem involve connectivity?
- Yes: The core of the problem is identifying connected components of 'X' cells. Each battleship is essentially a connected component of cells.
Does the problem have small constraints?
- Yes: The matrix size is typically manageable (m x n board), and we can explore each cell and its connections efficiently.
DFS/backtracking?
- Yes: DFS is perfect for exploring connected components. We can use DFS to traverse each battleship (connected component) and mark visited cells to avoid counting the same battleship multiple times.
Conclusion: The flowchart suggests using DFS to explore connected components. For each unvisited 'X' cell, we initiate a DFS to explore the entire battleship, mark all its cells as visited, and increment our battleship count. This ensures each battleship is counted exactly once.
Intuition
When we first look at this problem, the natural instinct is to use DFS to explore each battleship as a connected component. We'd start from an unvisited 'X'
cell, traverse all connected 'X'
cells to mark the entire battleship as visited, then increment our count. This is the standard connected components approach.
However, there's a clever observation that makes this problem much simpler: we don't actually need to traverse the entire battleship.
Think about it - if battleships can only be straight lines (horizontal or vertical) and they can't touch each other, then each battleship has a unique "starting point". For consistency, let's define this starting point as the top-left corner of each battleship:
- For a horizontal battleship, it's the leftmost cell
- For a vertical battleship, it's the topmost cell
- For a single cell battleship, it's that cell itself
The key insight is: a cell is a starting point if and only if it contains 'X'
AND has no 'X'
above it AND has no 'X'
to its left.
Why does this work? Because:
- Every battleship has exactly one starting point (its top-left cell)
- No non-starting cell can satisfy our condition (it would have an
'X'
either above or to its left) - The separation rule guarantees battleships don't touch, so we don't need to worry about distinguishing between different battleships
This transforms our problem from "explore all connected components using DFS" to "count all starting points with a simple scan". We iterate through the matrix once, and for each 'X'
cell at position (i, j)
, we check:
- Is
board[i-1][j]
(cell above) an'X'
? If yes, skip - this is not a starting point - Is
board[i][j-1]
(cell to the left) an'X'
? If yes, skip - this is not a starting point - Otherwise, we found a starting point - increment the counter
This approach is elegant because it leverages the problem's constraints (battleships are straight lines and don't touch) to avoid the complexity of DFS traversal entirely. We're essentially counting "heads" of battleships rather than exploring their entire "bodies".
Solution Approach
Following our intuition of counting battleship "starting points", let's implement the solution step by step:
1. Initialize variables:
m, n = len(board), len(board[0]) # Get board dimensions
ans = 0 # Counter for battleships
2. Iterate through each cell: We scan the entire board from top to bottom, left to right:
for i in range(m):
for j in range(n):
3. Apply the starting point detection logic:
For each cell at position (i, j)
, we check three conditions:
Condition 1: Skip empty cells
if board[i][j] == '.': continue
If the current cell is empty, it can't be part of a battleship, so we move to the next cell.
Condition 2: Check if there's an 'X' above
if i > 0 and board[i - 1][j] == 'X': continue
If we're not in the first row (i > 0
) and the cell directly above is 'X'
, then the current cell is part of a vertical battleship but not its starting point. The starting point would be somewhere above.
Condition 3: Check if there's an 'X' to the left
if j > 0 and board[i][j - 1] == 'X': continue
If we're not in the first column (j > 0
) and the cell directly to the left is 'X'
, then the current cell is part of a horizontal battleship but not its starting point. The starting point would be somewhere to the left.
4. Count the starting point:
If the current cell passes all checks (it's an 'X'
with no 'X'
above or to the left), it must be a battleship's starting point:
ans += 1
Time Complexity: O(m Γ n)
where m and n are the dimensions of the board. We visit each cell exactly once.
Space Complexity: O(1)
as we only use a constant amount of extra space for our counter and loop variables. We don't need any additional data structures like visited arrays or recursion stacks.
The beauty of this solution lies in its simplicity - by recognizing that we only need to count starting points rather than explore entire battleships, we avoid the overhead of DFS traversal while still accurately counting all battleships on the board.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example to illustrate how the solution counts battleships by identifying their starting points.
Consider this 4x5 board:
. X . . X X X X . . . . . X . X . X X X
We'll scan through each cell from top-left to bottom-right, checking if each 'X' cell is a battleship's starting point.
Row 0:
- (0,0): '.' - skip empty cell
- (0,1): 'X' - No 'X' above (first row), no 'X' to left. This is a starting point! Count = 1
- (0,2): '.' - skip empty cell
- (0,3): '.' - skip empty cell
- (0,4): 'X' - No 'X' above (first row), no 'X' to left. This is a starting point! Count = 2
Row 1:
- (1,0): 'X' - No 'X' above, no 'X' to left (first column). This is a starting point! Count = 3
- (1,1): 'X' - Has 'X' above at (0,1), so not a starting point - skip
- (1,2): 'X' - No 'X' above, but has 'X' to left at (1,1), so not a starting point - skip
- (1,3): '.' - skip empty cell
- (1,4): '.' - skip empty cell
Row 2:
- (2,0): '.' - skip empty cell
- (2,1): '.' - skip empty cell
- (2,2): '.' - skip empty cell
- (2,3): 'X' - No 'X' above, no 'X' to left. This is a starting point! Count = 4
- (2,4): '.' - skip empty cell
Row 3:
- (3,0): 'X' - No 'X' above, no 'X' to left (first column). This is a starting point! Count = 5
- (3,1): '.' - skip empty cell
- (3,2): 'X' - No 'X' above, no 'X' to left. This is a starting point! Count = 6
- (3,3): 'X' - Has 'X' above at (2,3), so not a starting point - skip
- (3,4): 'X' - No 'X' above, but has 'X' to left at (3,3), so not a starting point - skip
Final count: 6 battleships
Let's verify by identifying each battleship:
- Vertical battleship at column 1, rows 0-1: Starting point (0,1) β
- Single cell battleship at (0,4): Starting point (0,4) β
- Horizontal battleship at row 1, columns 0-2: Starting point (1,0) β
- Vertical battleship at column 3, rows 2-3: Starting point (2,3) β
- Single cell battleship at (3,0): Starting point (3,0) β
- Horizontal battleship at row 3, columns 2-4: Starting point (3,2) β
Each battleship has exactly one starting point (its top-left cell), and our algorithm correctly identifies all of them by checking for the absence of 'X' cells above and to the left.
Solution Implementation
1class Solution:
2 def countBattleships(self, board: List[List[str]]) -> int:
3 """
4 Count the number of battleships on the board.
5
6 Strategy: Count only the top-left corner of each battleship.
7 A cell is considered a battleship's top-left corner if:
8 1. It contains 'X'
9 2. No 'X' exists directly above it (or it's in the first row)
10 3. No 'X' exists directly to its left (or it's in the first column)
11
12 Args:
13 board: 2D grid where 'X' represents a battleship part and '.' represents empty water
14
15 Returns:
16 Number of battleships on the board
17 """
18 # Get board dimensions
19 rows, cols = len(board), len(board[0])
20
21 # Initialize battleship counter
22 battleship_count = 0
23
24 # Iterate through each cell in the board
25 for row in range(rows):
26 for col in range(cols):
27 # Skip empty water cells
28 if board[row][col] == '.':
29 continue
30
31 # Skip if this 'X' is part of a battleship that starts above
32 # (i.e., not the topmost cell of a vertical battleship)
33 if row > 0 and board[row - 1][col] == 'X':
34 continue
35
36 # Skip if this 'X' is part of a battleship that starts to the left
37 # (i.e., not the leftmost cell of a horizontal battleship)
38 if col > 0 and board[row][col - 1] == 'X':
39 continue
40
41 # This cell is the top-left corner of a battleship
42 battleship_count += 1
43
44 return battleship_count
45
1class Solution {
2 public int countBattleships(char[][] board) {
3 // Get board dimensions
4 int rows = board.length;
5 int cols = board[0].length;
6
7 // Counter for number of battleships
8 int battleshipCount = 0;
9
10 // Traverse through each cell in the board
11 for (int row = 0; row < rows; row++) {
12 for (int col = 0; col < cols; col++) {
13 // Skip empty water cells
14 if (board[row][col] == '.') {
15 continue;
16 }
17
18 // Skip if current 'X' is part of a battleship that starts above
19 // (i.e., this is not the topmost cell of a vertical battleship)
20 if (row > 0 && board[row - 1][col] == 'X') {
21 continue;
22 }
23
24 // Skip if current 'X' is part of a battleship that starts to the left
25 // (i.e., this is not the leftmost cell of a horizontal battleship)
26 if (col > 0 && board[row][col - 1] == 'X') {
27 continue;
28 }
29
30 // If we reach here, we've found the top-left corner of a new battleship
31 battleshipCount++;
32 }
33 }
34
35 return battleshipCount;
36 }
37}
38
1class Solution {
2public:
3 int countBattleships(vector<vector<char>>& board) {
4 // Get dimensions of the board
5 int rows = board.size();
6 int cols = board[0].size();
7
8 // Counter for number of battleships
9 int battleshipCount = 0;
10
11 // Iterate through each cell in the board
12 for (int row = 0; row < rows; ++row) {
13 for (int col = 0; col < cols; ++col) {
14 // Skip empty water cells
15 if (board[row][col] == '.') {
16 continue;
17 }
18
19 // Skip if current cell is part of a battleship that starts above
20 // (not the topmost cell of a vertical battleship)
21 if (row > 0 && board[row - 1][col] == 'X') {
22 continue;
23 }
24
25 // Skip if current cell is part of a battleship that starts to the left
26 // (not the leftmost cell of a horizontal battleship)
27 if (col > 0 && board[row][col - 1] == 'X') {
28 continue;
29 }
30
31 // Current cell is the top-left corner of a battleship
32 // Increment the counter
33 ++battleshipCount;
34 }
35 }
36
37 return battleshipCount;
38 }
39};
40
1/**
2 * Counts the number of battleships on the board.
3 * A battleship is represented by consecutive 'X' cells either horizontally or vertically.
4 * This solution counts only the top-left corner of each battleship to avoid duplicates.
5 *
6 * @param board - 2D array where 'X' represents a battleship cell and '.' represents empty water
7 * @returns The total number of battleships on the board
8 */
9function countBattleships(board: string[][]): number {
10 // Get board dimensions
11 const rows: number = board.length;
12 const cols: number = board[0].length;
13
14 // Initialize battleship counter
15 let battleshipCount: number = 0;
16
17 // Iterate through each cell in the board
18 for (let row = 0; row < rows; row++) {
19 for (let col = 0; col < cols; col++) {
20 // Skip empty water cells
21 if (board[row][col] === '.') {
22 continue;
23 }
24
25 // Skip if current 'X' is part of a battleship that starts above
26 // (i.e., there's an 'X' in the cell directly above)
27 if (row > 0 && board[row - 1][col] === 'X') {
28 continue;
29 }
30
31 // Skip if current 'X' is part of a battleship that starts to the left
32 // (i.e., there's an 'X' in the cell directly to the left)
33 if (col > 0 && board[row][col - 1] === 'X') {
34 continue;
35 }
36
37 // This 'X' is the top-left corner of a new battleship
38 battleshipCount++;
39 }
40 }
41
42 return battleshipCount;
43}
44
Time and Space Complexity
Time Complexity: O(m Γ n)
, where m
is the number of rows and n
is the number of columns in the board. The algorithm uses two nested loops to iterate through every cell in the board exactly once. For each cell, it performs constant-time operations (checking the cell value and potentially checking two adjacent cells), resulting in a total time complexity of O(m Γ n)
.
Space Complexity: O(1)
. The algorithm only uses a constant amount of extra space for variables m
, n
, ans
, i
, and j
, regardless of the input size. No additional data structures that scale with the input are created, making the space complexity constant.
Common Pitfalls
Pitfall 1: Attempting to Mark or Modify the Board
The Mistake: Many developers instinctively try to mark visited cells or modify the board to track which cells have been processed:
# WRONG APPROACH - Modifying the board
def countBattleships(self, board: List[List[str]]) -> int:
count = 0
for i in range(len(board)):
for j in range(len(board[0])):
if board[i][j] == 'X':
count += 1
# Trying to mark the entire battleship as visited
# This destroys the board and makes subsequent checks fail
board[i][j] = '.'
# Mark rest of battleship...
Why It Fails:
- The problem likely expects the board to remain unchanged (follow-up constraint)
- Modifying the board breaks the logic for checking adjacent cells
- Creates unnecessary complexity when the simple "starting point" approach suffices
The Fix: Stick to the read-only approach - just check if a cell is a starting point without modifying anything.
Pitfall 2: Over-Complicating with DFS/BFS
The Mistake: Implementing unnecessary traversal algorithms when the problem has a simpler solution:
# OVERLY COMPLEX - Using DFS
def countBattleships(self, board: List[List[str]]) -> int:
visited = set()
count = 0
def dfs(i, j):
if (i, j) in visited or i < 0 or i >= len(board) or j < 0 or j >= len(board[0]):
return
if board[i][j] == '.':
return
visited.add((i, j))
dfs(i+1, j)
dfs(i, j+1)
# ... more DFS logic
for i in range(len(board)):
for j in range(len(board[0])):
if board[i][j] == 'X' and (i, j) not in visited:
dfs(i, j)
count += 1
Why It's Problematic:
- Uses O(mΓn) extra space for the visited set
- Much more complex code that's harder to debug
- Slower due to function call overhead
- Overkill for a problem that can be solved with simple iteration
The Fix: Recognize that counting starting points is sufficient - no need to explore the entire battleship.
Pitfall 3: Incorrect Boundary Checking Order
The Mistake: Checking board values before verifying boundaries:
# WRONG - Can cause IndexError if board[i-1][j] == 'X' and i > 0: # Checks board BEFORE boundary continue # WRONG - Another variation if i >= 1 and board[i-1][j] == 'X': # Using >= 1 instead of > 0 is fine, continue # but be consistent throughout
Why It Fails:
- When
i = 0
, accessingboard[i-1][j]
attempts to accessboard[-1][j]
, which in Python gives the last row (unexpected behavior) - In other languages, this could cause an array out-of-bounds exception
The Fix: Always check boundaries first using short-circuit evaluation:
# CORRECT - Boundary check comes first if i > 0 and board[i-1][j] == 'X': continue
Pitfall 4: Misunderstanding the Starting Point Logic
The Mistake: Counting end points or middle points instead of consistently using top-left corners:
# WRONG - Inconsistent counting logic
if board[i][j] == 'X':
# Checking if it's an end point (bottom-right)
if (i == len(board)-1 or board[i+1][j] == '.') and \
(j == len(board[0])-1 or board[i][j+1] == '.'):
count += 1
Why It Fails:
- Makes the logic unnecessarily complex
- Easy to miss edge cases
- Harder to verify correctness
The Fix: Consistently use the top-left corner as the starting point. This ensures each battleship is counted exactly once and the logic remains simple and verifiable.
What is the best way of checking if an element exists in a sorted array once in terms of time complexity? Select the best that applies.
Recommended Readings
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Donβt Miss This!