文章目录
题目
标题和出处
标题:甲板上的战舰
出处:419. 甲板上的战舰
难度
4 级
题目描述
要求
给定一个 m × n \texttt{m} \times \texttt{n} m×n 的矩阵 board \texttt{board} board 表示甲板,其中每个单元格可以是战舰 'X' \texttt{`X'} 'X' 或者是空位 '.' \texttt{`.'} '.',返回在 board \texttt{board} board 上放置的战舰的数量。
战舰 只能水平或者竖直放置在 board \texttt{board} board 上。换句话说,战舰只能按 1 × k \texttt{1} \times \texttt{k} 1×k( 1 \texttt{1} 1 行 k \texttt{k} k 列)或 k × 1 \texttt{k} \times \texttt{1} k×1( k \texttt{k} k 行 1 \texttt{1} 1 列)的形状建造,其中 k \texttt{k} k 可以是任意大小。两艘战舰之间至少有一个水平或竖直的空位分隔(即没有相邻的战舰)。
示例
示例 1:

输入: board = [["X",".",".","X"],[".",".",".","X"],[".",".",".","X"]] \texttt{board = [["X",".",".","X"],[".",".",".","X"],[".",".",".","X"]]} board = [["X",".",".","X"],[".",".",".","X"],[".",".",".","X"]]
输出: 2 \texttt{2} 2
示例 2:
输入: board = [["."]] \texttt{board = [["."]]} board = [["."]]
输出: 0 \texttt{0} 0
数据范围
- m = board.length \texttt{m} = \texttt{board.length} m=board.length
- n = board[i].length \texttt{n} = \texttt{board[i].length} n=board[i].length
- 1 ≤ m, n ≤ 200 \texttt{1} \le \texttt{m, n} \le \texttt{200} 1≤m, n≤200
- board[i][j] \texttt{board[i][j]} board[i][j] 是 '.' \texttt{`.'} '.' 或 'X' \texttt{`X'} 'X'
进阶
你是否可以遍历一次,只使用 O(1) \texttt{O(1)} O(1) 额外空间且不修改 board \texttt{board} board 的值来解决这个问题?
解法一
思路和算法
由于同一个战舰在矩阵中占据的元素相邻,且不同战舰之间不相邻,因此可以使用广度优先搜索计算战舰数量。以下将 'X' \text{`X'} 'X' 称为「战舰元素」。
广度优先搜索需要使用与矩阵相同大小的二维数组记录每个元素是否被访问过,初始时所有元素的状态都是未访问。依次遍历矩阵中的每个元素,如果遇到一个元素是战舰元素且状态是未访问,则遇到一个新的战舰,将战舰数量加 1 1 1,并访问与当前战舰元素连接的所有战舰元素,即访问当前战舰的所有元素。
遍历结束之后,即可得到战舰数量。
实现方面有以下两点说明。
-
从一个战舰元素开始遍历整个战舰时,对于每个战舰元素,考虑与当前战舰元素在四个方向上相邻且未访问的战舰元素,可以创建方向数组实现四个方向的遍历。
-
此处的解法为新建与矩阵相同大小的二维数组记录每个元素是否被访问过,也可以不新建二维数组,而是在矩阵上原地修改访问过的元素。虽然原地修改可以省略新建二维数组的空间,但是不能省略队列空间,因此空间复杂度相同,而且原地修改会改变矩阵的元素,使得计算战舰数量之后无法再次使用矩阵信息。
代码
java
class Solution {
static int[][] dirs = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
public int countBattleships(char[][] board) {
int battleships = 0;
int m = board.length, n = board[0].length;
boolean[][] visited = new boolean[m][n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (board[i][j] == '.' || visited[i][j]) {
continue;
}
battleships++;
visited[i][j] = true;
Queue<int[]> queue = new ArrayDeque<int[]>();
queue.offer(new int[]{i, j});
while (!queue.isEmpty()) {
int[] cell = queue.poll();
int row = cell[0], col = cell[1];
for (int[] dir : dirs) {
int newRow = row + dir[0], newCol = col + dir[1];
if (newRow >= 0 && newRow < m && newCol >= 0 && newCol < n && board[newRow][newCol] == 'X' && !visited[newRow][newCol]) {
visited[newRow][newCol] = true;
queue.offer(new int[]{newRow, newCol});
}
}
}
}
}
return battleships;
}
}
复杂度分析
-
时间复杂度: O ( m n ) O(mn) O(mn),其中 m m m 和 n n n 分别是矩阵 board \textit{board} board 的行数和列数。广度优先搜索最多需要访问每个元素一次。
-
空间复杂度: O ( m n ) O(mn) O(mn),其中 m m m 和 n n n 分别是矩阵 board \textit{board} board 的行数和列数。记录每个元素是否被访问过的二维数组和队列需要 O ( m n ) O(mn) O(mn) 的空间。
解法二
思路和算法
也可以使用深度优先搜索计算战舰数量。
深度优先搜索需要使用与矩阵相同大小的二维数组记录每个元素是否被访问过,初始时所有元素的状态都是未访问。依次遍历矩阵中的每个元素,如果遇到一个元素是战舰元素且状态是未访问,则遇到一个新的战舰,将战舰数量加 1 1 1,并访问与当前战舰元素连接的所有战舰元素,即访问当前战舰的所有元素。
遍历结束之后,即可得到战舰数量。
实现方面有以下两点说明。
-
从一个战舰元素开始遍历整个战舰时,对于每个战舰元素,考虑与当前战舰元素在四个方向上相邻且未访问的战舰元素,可以创建方向数组实现四个方向的遍历。
-
此处的解法为新建与矩阵相同大小的二维数组记录每个元素是否被访问过,也可以不新建二维数组,而是在矩阵上原地修改访问过的元素。虽然原地修改可以省略新建二维数组的空间,但是不能省略递归调用栈空间,因此空间复杂度相同,而且原地修改会改变矩阵的元素,使得计算战舰数量之后无法再次使用矩阵信息。
代码
java
class Solution {
static int[][] dirs = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
int m, n;
char[][] board;
boolean[][] visited;
public int countBattleships(char[][] board) {
int battleships = 0;
this.m = board.length;
this.n = board[0].length;
this.board = board;
this.visited = new boolean[m][n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (board[i][j] == '.' || visited[i][j]) {
continue;
}
battleships++;
dfs(i, j);
}
}
return battleships;
}
public void dfs(int row, int col) {
visited[row][col] = true;
for (int[] dir : dirs) {
int newRow = row + dir[0], newCol = col + dir[1];
if (newRow >= 0 && newRow < m && newCol >= 0 && newCol < n && board[newRow][newCol] == 'X' && !visited[newRow][newCol]) {
dfs(newRow, newCol);
}
}
}
}
复杂度分析
-
时间复杂度: O ( m n ) O(mn) O(mn),其中 m m m 和 n n n 分别是矩阵 board \textit{board} board 的行数和列数。深度优先搜索最多需要访问每个元素一次。
-
空间复杂度: O ( m n ) O(mn) O(mn),其中 m m m 和 n n n 分别是矩阵 board \textit{board} board 的行数和列数。记录每个元素是否被访问过的二维数组和递归调用栈需要 O ( m n ) O(mn) O(mn) 的空间。
解法三
预备知识
该解法涉及到并查集。
并查集是一种树型的数据结构,用于处理不相交集合的合并与查询问题。
思路和算法
由于战舰由相邻的战舰元素连接形成,因此战舰数量为战舰元素组成的连通分量数,连通性问题可以使用并查集解决。
并查集初始化时,每个战舰元素元素分别属于不同的集合,每个集合只包含一个战舰元素元素,集合个数等于战舰元素元素个数。
初始化之后,遍历每个元素,如果一个元素是战舰元素元素且其上边或左边的相邻元素是战舰元素元素,则将两个相邻战舰元素元素所在的集合做合并,每次合并之后将集合个数减 1 1 1。
遍历结束之后,并查集的集合个数即为战舰数量。
代码
java
class Solution {
public int countBattleships(char[][] board) {
int m = board.length, n = board[0].length;
int xCounts = 0;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (board[i][j] == 'X') {
xCounts++;
}
}
}
UnionFind uf = new UnionFind(m * n, xCounts);
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (board[i][j] == 'X') {
if (i > 0 && board[i - 1][j] == 'X') {
uf.union(i * n + j, (i - 1) * n + j);
}
if (j > 0 && board[i][j - 1] == 'X') {
uf.union(i * n + j, i * n + j - 1);
}
}
}
}
return uf.getCount();
}
}
class UnionFind {
private int[] parent;
private int[] rank;
private int count;
public UnionFind(int n, int count) {
parent = new int[n];
for (int i = 0; i < n; i++) {
parent[i] = i;
}
rank = new int[n];
this.count = count;
}
public void union(int x, int y) {
int rootx = find(x);
int rooty = find(y);
if (rootx != rooty) {
if (rank[rootx] > rank[rooty]) {
parent[rooty] = rootx;
} else if (rank[rootx] < rank[rooty]) {
parent[rootx] = rooty;
} else {
parent[rooty] = rootx;
rank[rootx]++;
}
count--;
}
}
public int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]);
}
return parent[x];
}
public int getCount() {
return count;
}
}
复杂度分析
-
时间复杂度: O ( m n × α ( m n ) ) O(mn \times \alpha(mn)) O(mn×α(mn)),其中 m m m 和 n n n 分别是矩阵 board \textit{board} board 的行数和列数, α \alpha α 是反阿克曼函数。并查集的初始化需要 O ( m n ) O(mn) O(mn) 的时间,然后遍历 m n mn mn 个元素,执行 O ( m n ) O(mn) O(mn) 次合并操作,这里的并查集使用了路径压缩和按秩合并,单次操作的时间复杂度是 O ( α ( m n ) ) O(\alpha(mn)) O(α(mn)),因此并查集初始化之后的操作的时间复杂度是 O ( m n × α ( m n ) ) O(mn \times \alpha(mn)) O(mn×α(mn)),总时间复杂度是 O ( m n + m n × α ( m n ) ) = O ( m n × α ( m n ) ) O(mn + mn \times \alpha(mn)) = O(mn \times \alpha(mn)) O(mn+mn×α(mn))=O(mn×α(mn))。
-
空间复杂度: O ( m n ) O(mn) O(mn),其中 m m m 和 n n n 分别是矩阵 board \textit{board} board 的行数和列数。并查集需要 O ( m n ) O(mn) O(mn) 的空间。
解法四
思路和算法
由于每个战舰都是 1 1 1 行 k k k 列或 k k k 行 1 1 1 列,且不同战舰之间不相邻,因此每个战舰都有一个左上角元素,左上角元素满足该元素是战舰元素且该元素的上边和左边的相邻元素都是空位(这里假设 board \textit{board} board 的四条边均被空位包围)。可以遍历矩阵一次,通过寻找每个战舰的左上角元素的做法计算战舰数量,该做法的空间复杂度是 O ( 1 ) O(1) O(1) 且不修改 board \textit{board} board 的值。
遍历矩阵 board \textit{board} board,对于每个战舰元素 board [ i ] [ j ] \textit{board}[i][j] board[i][j],判断该元素是否为左上角元素,如果 i = 0 i = 0 i=0 或 board [ i − 1 ] [ j ] \textit{board}[i - 1][j] board[i−1][j] 不是战舰元素,且 j = 0 j = 0 j=0 或 board [ i ] [ j − 1 ] \textit{board}[i][j - 1] board[i][j−1] 不是战舰元素,则 board [ i ] [ j ] \textit{board}[i][j] board[i][j] 是左上角元素。每次遇到左上角元素则将战舰数量加 1 1 1,遍历结束之后即可得到战舰数量。
代码
java
class Solution {
public int countBattleships(char[][] board) {
int battleships = 0;
int m = board.length, n = board[0].length;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (isTopLeftCorner(board, i, j)) {
battleships++;
}
}
}
return battleships;
}
public boolean isTopLeftCorner(char[][] board, int row, int col) {
return board[row][col] == 'X' && (row == 0 || board[row - 1][col] != 'X') && (col == 0 || board[row][col - 1] != 'X');
}
}
复杂度分析
-
时间复杂度: O ( m n ) O(mn) O(mn),其中 m m m 和 n n n 分别是矩阵 board \textit{board} board 的行数和列数。需要访问每个元素一次。
-
空间复杂度: O ( 1 ) O(1) O(1)。