棋盘上的战舰 图解题解
这道题到底在问什么
- 输入
- board = [[X,.,.,X],[.,.,.,X],[.,.,.,X]]
- 输出
- 2 (一艘单格 + 一艘竖排三格)
- 输入
- board = [[.]]
- 输出
- 0 (没有 X)
最优解:为什么这么做
一句话答案:LeetCode 419 棋盘上的战舰用一次网格扫描只数舰头:X 且上、左都不是 X 就是船头、计一次数,靠战舰互不相邻保证船头唯一。时间 O(m·n)、空间 O(1)。
棋盘上摆着 X 和空位,这道题要数出几艘船
给一个 m×n 的棋盘 board,每格要么是战舰的一段 X、要么是空位点。战舰只能横排成 1×k 或竖排成 k×1 的一条直线,且任意两艘之间至少隔一个空格、绝不挨着。返回一共有几艘战舰。题面那张棋盘的答案是 2:左上角 (0,0) 一艘单格船,最右一列 (0,3)、(1,3)、(2,3) 连成一艘竖排三格船。
把整艘船找出来再数,多花的是哪一笔
一个直觉做法是对每个还没数过的 X 做一次 DFS,顺着连通的格子一路铺开、把整艘船都标记掉,铺开几次就是几艘船。答案没错,但每铺一次都要开访问标记数组、或往递归栈里压一串格子,额外空间涨到 O(m·n)。而这题的进阶要求偏偏是只用 O(1) 额外空间、还不许改动原棋盘,染色法两条都踩线。
既然每艘船都是直线,何必把它整条走完
换个数法:不找整艘船,只找每艘船左上角那一格。横排船的最左格、竖排船的最上格就是它的「船头」,每艘船有且只有一个船头,数船头就等于数船。怎么认船头?看这个 X 的正上方和正左方:上、左都不是 X,说明前面没有同船的格子接着,它就是船头,计一次数;只要上或左有一个是 X,它就是后半截,跳过。之所以不重不漏,全靠「任意两艘至少隔一格」:船与船不相邻,一个 X 的上、左邻居只可能是同船的格子、绝不会牵到隔壁船,「上左都不是 X」就精准卡住了每艘船唯一的那个头。
一行一行扫过去,每个 X 只做一道判断题
扫描顺序是从上到下、每行从左到右。碰到空位点直接跳过;碰到 X,就做那道判断题:它上边一格(同列上一行)、左边一格(同行左一列)是不是 X。两个都不是 X,船头计数加一;只要有一个是 X,跳过。第 0 行的 X 上方是棋盘外、第 0 列的 X 左方是棋盘外,都视作那侧不是 X。全程只养一个计数器,扫完它就是答案。
回到题面这张 3×4 棋盘,逐格数船头
board=[[X,.,.,X],[.,.,.,X],[.,.,.,X]],三行四列。(0,0) 是 X,上左都在界外,判成船头,计数到 1。(0,1)、(0,2) 空位跳过。(0,3) 是 X,上方界外、左方 (0,2) 是空位,又一个船头,计数到 2。(1,0) 到 (1,2) 空位跳过;(1,3) 是 X,上方 (0,3) 也是 X,是后半截,跳过。(2,0) 到 (2,2) 空位跳过;(2,3) 是 X,上方 (1,3) 也是 X,跳过。扫完全盘,计数器停在 2,也就是那艘单格船加一艘竖排三格船。
漏看左方那一列,横排战舰会被数成好几艘
每个格子只访问一次、外加常数次上左判断,时间 O(m·n);只用一个计数变量、不开数组也不写回棋盘,空间 O(1),正好压在进阶要求上。两个最容易栽的地方:只查上方、忘了左方,横排船的第二格、第三格因为上方不是 X,会被逐个误判成新船头,一艘横排船数成好几艘,横排的非头格正是靠「左方是 X」才被跳过的。还有第 0 行、第 0 列的边界,直接读 board[i-1][j] 或 board[i][j-1] 会越界,得先判 i>0、j>0 再看邻居。空棋盘或整盘没一个 X,计数器一直是 0,返回 0。
▶ 动画逐步走查(共 25 步)——想跟着动画一帧帧对照就展开
- 3记住这把尺子:X 且「上不是 X、左不是 X」就计数。下面每一帧都在套它。
- 4count = 0;扫描指针从 (0,0) 起逐行往右走蓝色是空位,灰色是还没扫到的战舰格 X。套路:一行一行、从左到右扫,只在 X 的「左上角」处计数。
- 5看 (0,0) = X,检查它的上方与左方 | 战舰数 = 0扫到 (0,0) 是 X(紫色)。判定它是不是船头:看它正上方和正左方有没有 X。
- 6(0,0) 是船头,战舰数 = 1上方是棋盘外边界,左方是棋盘外边界,所以 (0,0) 是一艘新战舰的左上角(变绿),战舰数加到 1。
- 7看 (0,1) = 空位 | 战舰数 = 1扫到 (0,1),这里是空位,什么都不用做,继续往右扫。当前战舰数 1。
- 8看 (0,2) = X,检查它的上方与左方 | 战舰数 = 1扫到 (0,2) 是 X(紫色)。判定它是不是船头:看它正上方和正左方有没有 X。
- 9(0,2) 是船头,战舰数 = 2上方是棋盘外边界,左方是空位,所以 (0,2) 是一艘新战舰的左上角(变绿),战舰数加到 2。
- 10看 (0,3) = X,检查它的上方与左方 | 战舰数 = 2扫到 (0,3) 是 X(紫色)。判定它是不是船头:看它正上方和正左方有没有 X。
- 11(0,3) 是船身,跳过 | 战舰数 = 2它正左方 (0,2) 也是 X,说明 (0,3) 只是同一艘船的后半截(变灰),不是船头,跳过不计。战舰数仍是 2。
- 12看 (0,4) = 空位 | 战舰数 = 2扫到 (0,4),这里是空位,什么都不用做,继续往右扫。当前战舰数 2。
- 13看 (1,0) = X,检查它的上方与左方 | 战舰数 = 2扫到 (1,0) 是 X(紫色)。判定它是不是船头:看它正上方和正左方有没有 X。
- 14(1,0) 是船身,跳过 | 战舰数 = 2它正上方 (0,0) 也是 X,说明 (1,0) 只是同一艘船的后半截(变灰),不是船头,跳过不计。战舰数仍是 2。
- 15看 (1,1) = 空位 | 战舰数 = 2扫到 (1,1),这里是空位,什么都不用做,继续往右扫。当前战舰数 2。
- 16看 (1,2) = 空位 | 战舰数 = 2扫到 (1,2),这里是空位,什么都不用做,继续往右扫。当前战舰数 2。
- 17看 (1,3) = 空位 | 战舰数 = 2扫到 (1,3),这里是空位,什么都不用做,继续往右扫。当前战舰数 2。
- 18看 (1,4) = X,检查它的上方与左方 | 战舰数 = 2扫到 (1,4) 是 X(紫色)。判定它是不是船头:看它正上方和正左方有没有 X。
- 19(1,4) 是船头,战舰数 = 3上方是空位,左方是空位,所以 (1,4) 是一艘新战舰的左上角(变绿),战舰数加到 3。
- 20看 (2,0) = 空位 | 战舰数 = 3扫到 (2,0),这里是空位,什么都不用做,继续往右扫。当前战舰数 3。
- 21看 (2,1) = 空位 | 战舰数 = 3扫到 (2,1),这里是空位,什么都不用做,继续往右扫。当前战舰数 3。
- 22看 (2,2) = X,检查它的上方与左方 | 战舰数 = 3扫到 (2,2) 是 X(紫色)。判定它是不是船头:看它正上方和正左方有没有 X。
- 23(2,2) 是船头,战舰数 = 4上方是空位,左方是空位,所以 (2,2) 是一艘新战舰的左上角(变绿),战舰数加到 4。
- 24看 (2,3) = 空位 | 战舰数 = 4扫到 (2,3),这里是空位,什么都不用做,继续往右扫。当前战舰数 4。
- 25看 (2,4) = X,检查它的上方与左方 | 战舰数 = 4扫到 (2,4) 是 X(紫色)。判定它是不是船头:看它正上方和正左方有没有 X。
- 26(2,4) 是船身,跳过 | 战舰数 = 4它正上方 (1,4) 也是 X,说明 (2,4) 只是同一艘船的后半截(变灰),不是船头,跳过不计。战舰数仍是 4。
- 27扫描结束,共数到 4 个船头 | 战舰数 = 4整张棋盘扫完。绿色的四格就是四艘战舰各自的左上角,数到 4 个,答案就是 4。灰色的都是被跳过的船身。
⚠️ 容易写错的地方
✗ 错:对每个 X 做 DFS/BFS 整艘船染色
✓ 对:只判「上左是否为 X」数船头
题目保证战舰是直线且互不相邻,船头唯一,不必遍历整艘船,O(1) 空间即可
✗ 错:忘了第 0 行、第 0 列的边界
✓ 对:i 等于 0 时视作上方无 X,j 等于 0 时视作左方无 X
越界访问会出错,边界处天然就是船头候选
✗ 错:只检查上方漏了左方
✓ 对:上方和左方都要为非 X 才算船头
横排战舰的非头格,是靠「左方是 X」被正确跳过的
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class Solution:
def countBattleships(self, board: List[List[str]]) -> int:
m, n = len(board), len(board[0])
ans = 0
for i in range(m):
for j in range(n):
if board[i][j] == '.':
continue
if i > 0 and board[i - 1][j] == 'X':
continue
if j > 0 and board[i][j - 1] == 'X':
continue
ans += 1
return ansC++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
class Solution {
public:
int countBattleships(vector<vector<char>>& board) {
int m = board.size(), n = board[0].size();
int ans = 0;
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if (board[i][j] == '.') {
continue;
}
if (i > 0 && board[i - 1][j] == 'X') {
continue;
}
if (j > 0 && board[i][j - 1] == 'X') {
continue;
}
++ans;
}
}
return ans;
}
};Java
import java.util.*;
class Solution {
public int countBattleships(char[][] board) {
int m = board.length, n = board[0].length;
int ans = 0;
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if (board[i][j] == '.') {
continue;
}
if (i > 0 && board[i - 1][j] == 'X') {
continue;
}
if (j > 0 && board[i][j - 1] == 'X') {
continue;
}
++ans;
}
}
return ans;
}
}复杂度
时间
O(m·n)
每个格子只看一次,外加常数次的上方、左方判断
空间
O(1)
只用一个计数变量,不开额外数组、也不改棋盘
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 棋盘上的战舰 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么只数船头就不会重复、也不会漏数?+
靠的是题目那条隐藏前提:战舰是直线、且任意两艘至少隔一个空格。因为每艘船是一条横线或竖线,它的最左上角那一格独一无二,把「数船」换成「数左上角」是一一对应,不会重复。又因为船与船不相邻,任何一个 X 的上邻居、左邻居要么是空位、要么是同一艘船的格子,绝不会是另一艘船;所以「上、左都不是 X」这个判据只会在真正的船头上成立,既不会把后半截误当船头,也不会把两艘船黏成一艘。前提一旦去掉,这套推理就塌了。
如果战舰允许相邻或者拐弯,这个 O(1) 解还成立吗?+
不成立。整个解法的地基就是「互不相邻且是直线」,船头才唯一、才能靠看上左两格认出来。一旦允许两艘船挨着,或者一艘船拐了弯,「上左都不是 X」就不再等于船头,甚至分不清一块 X 到底属于几艘船。这时问题退化成一般的连通块计数,也就是「岛屿数量」那类题,得用并查集或 DFS/BFS 把每个连通块整体染色来数,额外空间不再是 O(1)。
面试官要求全程不修改 board,这个解满足吗?+
满足,而且这正是进阶版的考点。整个过程只读 board、只维护一个整型计数器,从不往棋盘里写回任何标记,原图始终保持不变。对比之下,很多染色法会把走过的 X 改成别的字符来避免重复访问,那就动了原数据;本解因为只看每个格子上、左两个邻居、不需要标记已访问,天然做到了只读,空间也压到了 O(1)。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 棋盘上的战舰 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。