419. 棋盘上的战舰
问题描述
给你一个 m x n
的棋盘 board
,它包含两种字符:
'X'
:表示战舰的一部分'.'
:表示空位
战舰用 'X'
表示,且满足以下规则:
- 舰队只能水平或垂直放置
- 舰队之间至少有一个水平或垂直的空位分隔
- 棋盘上没有相邻的舰队(即舰队不会对角相邻)
给你一个有效的战舰放置图,计算棋盘上有多少舰队。
示例:
输入: board = [["X",".",".","X"],
[".",".",".","X"],
[".",".",".","X"]]
输出: 2
解释: 有两舰队,一个水平放置,一个垂直放置。
算法思路
核心
- 舰队特性:舰队是连续的
'X'
,只能水平或垂直 - 关键:每个舰队有且只有一个"头部"
- 头部定义:舰队中最靠上、最靠左的
'X'
方法:计数舰队头部
- 遍历棋盘每个位置
- 当遇到
'X'
时,检查它是否是舰队的头部 - 头部判断条件:
- 左边没有
'X'
(或在最左边) - 上边没有
'X'
(或在最上边)
- 左边没有
- 满足条件的
'X'
就是一舰队的头部,计数加1
为什么这个方法正确?
- 每舰队有且只有一个头部(最左最上的X)
- 每个头部对应一完整的舰队
- 不会重复计数,也不会遗漏
代码实现
class Solution {
/**
* 计算棋盘上舰队的数量
*
* @param board m x n 的字符数组,'X'表示战舰,'.'表示空位
* @return 舰队的数量
*/
public int countBattleships(char[][] board) {
int m = board.length; // 棋盘行数
int n = board[0].length; // 棋盘列数
int count = 0; // 舰队计数
// 遍历棋盘的每个位置
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
// 如果当前位置是舰队的一部分
if (board[i][j] == 'X') {
// 检查是否是舰队的头部(最左最上的X)
boolean isHead = true;
// 检查左边是否有X(同一行)
if (j > 0 && board[i][j-1] == 'X') {
isHead = false; // 不是头部,因为左边有X
}
// 检查上边是否有X(同一列)
if (i > 0 && board[i-1][j] == 'X') {
isHead = false; // 不是头部,因为上边有X
}
// 如果是头部,计数加1
if (isHead) {
count++;
}
}
}
}
return count;
}
}
优化(更简洁)
class Solution {
/**
* 优化:代码更简洁
*
* @param board 棋盘
* @return 舰队数量
*/
public int countBattleships(char[][] board) {
int count = 0;
int m = board.length;
int n = board[0].length;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (board[i][j] == 'X' &&
(i == 0 || board[i-1][j] != 'X') && // 上边没有X或在边界
(j == 0 || board[i][j-1] != 'X')) { // 左边没有X或在边界
count++;
}
}
}
return count;
}
}
算法分析
-
时间复杂度:O(m × n)
- 需要遍历棋盘的每个位置一次
- 每个位置的检查是 O(1)
-
空间复杂度:O(1)
- 只使用常数额外空间
- 没有使用额外的数据结构
-
关键:
- 避免了DFS/BFS的复杂实现
- 一次遍历解决问题
- 空间效率最高
算法过程
输入:
board = [["X",".",".","X"],
[".",".",".","X"],
[".",".",".","X"]]
遍历过程:
位置 | 值 | 左边有X | 上边有X | 是否头部 | 说明 |
---|---|---|---|---|---|
(0,0) | X | 否(边界) | 否(边界) | 是 | 左上角X,是头部 |
(0,1) | . | - | - | - | 空位,跳过 |
(0,2) | . | - | - | - | 空位,跳过 |
(0,3) | X | 否 | 否(边界) | 是 | 右上角X,左边和上边都没有X |
(1,0) | . | - | - | - | 空位,跳过 |
(1,1) | . | - | - | - | 空位,跳过 |
(1,2) | . | - | - | - | 空位,跳过 |
(1,3) | X | 否 | 是(board[0][3]==‘X’) | 否 | 上边有X,不是头部 |
(2,0) | . | - | - | - | 空位,跳过 |
(2,1) | . | - | - | - | 空位,跳过 |
(2,2) | . | - | - | - | 空位,跳过 |
(2,3) | X | 否 | 是(board[1][3]==‘X’) | 否 | 上边有X,不是头部 |
结果:count = 2
测试用例
public static void main(String[] args) {
Solution solution = new Solution();
// 测试用例1:标准示例
char[][] board1 = {
{'X',' ','.','X'},
{'.','.','.','X'},
{'.','.','.','X'}
};
System.out.println("Test 1: " + solution.countBattleships(board1)); // 2
// 测试用例2:单艘战舰
char[][] board2 = {
{'X','X','X'}
};
System.out.println("Test 2: " + solution.countBattleships(board2)); // 1
// 测试用例3:没有战舰
char[][] board3 = {
{'.','.','.'},
{'.','.','.'}
};
System.out.println("Test 3: " + solution.countBattleships(board3)); // 0
// 测试用例4:多艘战舰
char[][] board4 = {
{'X','.','X'},
{'X','.','X'},
{'X','.','X'}
};
System.out.println("Test 4: " + solution.countBattleships(board4)); // 2
// 测试用例5:1x1战舰
char[][] board5 = {
{'X'}
};
System.out.println("Test 5: " + solution.countBattleships(board5)); // 1
// 测试用例6:复杂布局
char[][] board6 = {
{'X','X','.','X'},
{'.','.','.','.'},
{'X','.','X','X'},
{'X','.','.','.'}
};
System.out.println("Test 6: " + solution.countBattleships(board6)); // 3
// 水平战舰(0,0-1),垂直战舰(2-3,0),水平战舰(2,2-3)
}
关键点
-
头部定义的正确性:
- 水平舰队:最左边的
'X'
- 垂直舰队:最上边的
'X'
- L形舰队:最左最上的
'X'
- 水平舰队:最左边的
-
边界条件处理:
- 最左边的列:左边没有
'X'
- 最上边的行:上边没有
'X'
- 使用
i == 0
和j == 0
判断边界
- 最左边的列:左边没有
-
问题约束的利用:
- 舰队不相邻,确保不会误判
- 只能水平或垂直,简化了头部判断
-
为什么不需要标记已访问?
- 因为只通过位置关系判断头部
- 不需要防止重复访问
常见问题
-
为什么不用DFS/BFS?
- 可以用,但需要额外空间标记已访问
- 时间复杂度相同,但空间复杂度O(mn)
- 本方法更优
-
如果舰队可以对角相邻怎么办?
- 本题保证不会对角相邻
- 如果允许,需要修改判断逻辑
-
算法的直观理解:
- 想象从左到右、从上到下扫描棋盘
- 每当看到一个
'X'
,就问:“这是不是一新舰队的开始?” - 如果左边和上边都没有
'X'
,那就是新舰队的开始
-
与岛屿数量问题的区别:
- 岛屿问题需要DFS/BFS找连通分量
- 本题利用舰队的特殊形状,可以用更简单的方法
-
扩展:
- 如果舰队可以L形、T形等复杂形状
- 还能用头部计数法吗?
- 可以,只要定义好"头部"概念