最大加号标志 图解题解
这道题到底在问什么
- 输入
- n = 5, mines = [[4,2]]
- 输出
- 2
- 输入
- n = 4, mines = [[0,1],[2,3]]
- 输出
- 2
最优解:为什么这么做
一句话答案:LeetCode 764 最大加号标志:对每格数上下左右四方向连续 1 的长度、取四者最小当臂长,四遍方向 DP 压完再取全表最大值,因为加号被最短的一条臂卡死。时间 O(n²)、空间 O(n²)。
什么样的十字才算一个 k 阶加号
给一个 n × n 的方格,除 mines 列出的地雷是 0,其余都是 1。一个 k 阶加号有一个中心格,从中心往上下左右各伸一条长 k-1 的臂,中心和四条臂必须全踩在 1 上,求盘面能摆的最大阶数 k,没有 1 就返回 0。坐标写成(行号,列号),都从 0 数起。题面例子 n=4、地雷 (0,1) 和 (2,3),答案 2:走一格全是 1,再走就撞雷或出界。
为什么挨个中心去扩加号会白扫太多次
让每个格子轮流当中心、四个方向一格格往外探,n² 个中心每个最坏花 O(n) 探臂,整套就是 O(n³)。慢就慢在探臂:一个中心要确认自己 k 阶,四条臂各查 k-1 个格子,臂最坏伸到 n,相邻中心的臂还大段重叠、被反复重数。
一个格子能撑多大加号,被四条臂里最短的卡死
先别管加号,只问每个格子朝某方向连着能数出几个 1(遇雷或边界就断)。它当中心时,臂长超不过任何一个方向的连续 1 长度——四条臂要同时成立,只要一条方向 1 不够长,加号就伸不出去。四个方向里最短的那条卡住它,一条臂短,加号就缩到那条的长度。
四个方向各扫一遍,为什么能共用同一张表
开一张和盘面同样大的 dp 表(dp 即动态规划:把每格算过的结果存表里、后面直接取用不重算),dp[i][j] 记以 (i,j) 为中心能撑的臂长。陆地格先填 n(臂长上限,后面往下压——每遍都跟表里的值取较小的、把臂长往小里改,叫压),地雷格填 0。分四遍:逐行从左往右数向左、从右往左数向右(从左扫过去,是在数每格往左能连几个 1),逐列从上往下数向上、从下往上数向下;每数出一个方向的长度就和 dp 当前值取 min,遇 1 加一、遇地雷清零,保证数的是不含雷的连续 1。四个方向互相独立,共用一张表、四遍压完剩的就是四方向最小。
拿 4×4 两颗地雷,把四遍扫描亲手压一遍
用题面 n=4、地雷 (0,1) 和 (2,3),dp 先填陆地 4、地雷 0。第一遍数向左:第 1 行整行 1,压成 1、2、3、4;第 0 行到 (0,1) 撞雷清零,成 1、0、1、2。第二遍从右往左取 min:(1,3) 向左本是 4,可在最右边、向右只有 1,min(4,1)=1 压回。左右叠完,中间两列留 2、贴边成 1。
第三遍数向上取 min:(1,1) 头顶就是地雷、向上只有 1,横向的 2 被压成 1;(1,2) 上方畅通、向上有 2,min(2,2) 保住 2。第四遍数向下取 min:最底一行向下都只有 1、整行压成 1,而 (2,1) 下方还有一格、向下 2 保住 2。四遍压完,全表只剩 (1,2) 和 (2,1) 是 2,取全表最大得 2。
取 min 写成 max,加号为什么会被夸大
四遍每遍走完整张 n×n 的表,时间是它的常数倍 O(n²);只用一张 dp 表反复取 min,空间 O(n²)。地雷再多也不改复杂度,规模只看 n。取 min 写成 max,等于一条臂够长就行、加号被夸大成最长那条;数连续 1 遇地雷忘清零,跨地雷的两段会被接成一段;陆地格初值填 0 或 1 而非够大的 n,第一遍 min 就把臂长误压低。
▶ 动画逐步走查(共 21 步)——想跟着动画一帧帧对照就展开
- 3记住这句口诀:加号的臂长被最短的那条方向卡住,所以是四个方向连续 1 的长度取最小值。下面每一帧都在套它。
- 4绿色 1 = 可用格;红色 0 = 地雷这就是 4 × 4 的盘面。红色的 0 是地雷,在 (0,1) 和 (2,3);其余绿色的格子都是 1。加号的每一格都必须落在绿色的 1 上。
- 5紫色 = 中心;橙色 = 四个方向各延伸一格先用 (2,1) 试手。把中心点紫,再往上下左右各探一格:上是 (1,1)、下是 (3,1)、左是 (2,0)、右是 (2,2),四个邻居都是 1。四个方向都能走出 1 格,所以这里能撑起 2 阶加号。
- 6另开一张同样大小的 dp 表,每格记「以它为中心的臂长」。臂长再长也超不过 n,所以陆地格先填 4 这个上限,两颗地雷格直接填 0。接下来四遍扫描会把这些 4 一点点压小。
- 7第一遍,逐行从左往右扫,数每个格子向左方向(含自己)连续有几个 1。遇到 1 就加一,遇到地雷就清零重来。先拿第 1 行找感觉。
- 8第 1 行整行都是 1,没有地雷打断,所以向左长度一路累加成 1、2、3、4。这一行越往右,左臂越长。
- 9再看第 0 行。(0,0) 是 1,记 1;到 (0,1) 撞上地雷,长度清零记 0;过了雷,(0,2) 重新从 1 开始,(0,3) 接着是 2。地雷会把连续段切断,这点很关键。
- 10四行都这么扫完,整张 dp 表现在装的是每个格子的向左臂长。注意这只是四个方向里的一个,接下来三遍会用更短的方向把它继续压小。
- 11第二遍反过来,逐行从右往左数向右臂长,数出来后和表里的值取最小。盯住 (1,3):它向左臂长是 4,可它在最右边,向右一步都迈不出去。
- 12果然,(1,3) 向右只有 1,min(4, 1) 等于 1,它被压回 1。最左边的 (1,0) 正相反,向左只有 1。取 min 的意义就在这:一个方向再长,只要另一个方向短,加号就撑不大。
- 13两遍叠完,每格装的是左右两个方向的较小值,也就是这一行里横着能伸多远。靠中间的格子(像第 1、2 列)还保留着 2,边上的都被压成了 1。
- 14第三遍换方向,逐列从上往下数向上臂长,继续取 min。看第 1 列:顶上 (0,1) 是地雷,所以 (1,1) 向上只有 1,(2,1) 是 2,(3,1) 是 3。地雷同样会把竖向也切断。
- 15(1,1) 头顶就是地雷,向上只有 1,横向的 2 被压成了 1。而 (1,2) 上方一路畅通,向上有 2,和横向的 2 取 min 还是 2,稳稳保住。能不能当大加号的中心,这一步开始分化。
- 16三遍叠完,每格已经是左、右、上三个方向的最小值。只剩最后一个方向,向下。
- 17最后一遍,逐列从下往上数向下臂长,再取一次 min。最底下那一行 (第 3 行) 每个格子向下都迈不出去,向下臂长全是 1。
- 18所以第 3 行整行被向下臂长 1 压成了 1,贴着边界的格子当不了大加号的中心。而 (2,1) 下方还有一格,向下臂长是 2,min(2, 2) 保持 2,它扛住了四个方向。
- 19四个方向都扫完了,这张表就是最终答案表。每格的值,正是以它为中心能撑起的最大加号阶数。能看到只有 (1,2) 和 (2,1) 两个格子是 2,其余都是 1 或 0。
- 20最后一步,把整张表扫一遍取最大值。最大就是 2,出现在 (1,2) 和 (2,1)。这就是题目要的答案,和开头说的 2 对上了。
- 21紫色中心 + 橙色四臂,组成 2 阶加号回到盘面,把 (2,1) 这个 2 阶加号画出来:中心加上下左右各一格,五个格子全是 1,稳稳成立。再大一阶就要伸到第二格,可那样会撞到边界或地雷,所以到 2 为止。
- 22紫色中心;橙色为想伸出的第二格, 已越界或撞雷想把 (2,1) 升到 3 阶吗?那上臂要伸到 (0,1),可那里是地雷;左臂要伸到 (2,-1),已经出了边界。任何一条臂走不通,加号就升不上去,所以 (2,1) 封顶在 2 阶。
- 23(1,2) 同样能撑起 2 阶(1,2) 也是一样,上下左右各一格都是 1,同样是 2 阶。两个并列第一,答案取它们的阶数 2,具体在哪个中心并不重要。
⚠️ 容易写错的地方
✗ 错:四个方向取最大值
✓ 对:取最小值
加号要四条臂同时成立,被最短的一条卡死,所以是 min 不是 max
✗ 错:地雷处不清零,继续累加
✓ 对:遇到 0 必须把连续计数清零
加号的格子必须全是 1,地雷会把连续段彻底切断
✗ 错:dp 初值填 0 或 1
✓ 对:陆地格初值填 n(臂长上限)
初值要足够大,后面四遍 min 才压得动;填小了会把真实臂长误压低
完整代码(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 *
from string import *
from operator import *
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
class Solution:
def orderOfLargestPlusSign(self, n: int, mines: List[List[int]]) -> int:
dp = [[n] * n for _ in range(n)]
for x, y in mines:
dp[x][y] = 0
for i in range(n):
left = right = up = down = 0
for j, k in zip(range(n), reversed(range(n))):
left = left + 1 if dp[i][j] else 0
right = right + 1 if dp[i][k] else 0
up = up + 1 if dp[j][i] else 0
down = down + 1 if dp[k][i] else 0
dp[i][j] = min(dp[i][j], left)
dp[i][k] = min(dp[i][k], right)
dp[j][i] = min(dp[j][i], up)
dp[k][i] = min(dp[k][i], down)
return max(max(v) for v in dp)C++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <optional>
#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;
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};
struct ListNode {
int val;
ListNode *next;
ListNode() : val(0), next(nullptr) {}
ListNode(int x) : val(x), next(nullptr) {}
ListNode(int x, ListNode *next) : val(x), next(next) {}
};
class Solution {
public:
int orderOfLargestPlusSign(int n, vector<vector<int>>& mines) {
vector<vector<int>> dp(n, vector<int>(n, n));
for (auto& e : mines) dp[e[0]][e[1]] = 0;
for (int i = 0; i < n; ++i) {
int left = 0, right = 0, up = 0, down = 0;
for (int j = 0, k = n - 1; j < n; ++j, --k) {
left = dp[i][j] ? left + 1 : 0;
right = dp[i][k] ? right + 1 : 0;
up = dp[j][i] ? up + 1 : 0;
down = dp[k][i] ? down + 1 : 0;
dp[i][j] = min(dp[i][j], left);
dp[i][k] = min(dp[i][k], right);
dp[j][i] = min(dp[j][i], up);
dp[k][i] = min(dp[k][i], down);
}
}
int ans = 0;
for (auto& e : dp) ans = max(ans, *max_element(e.begin(), e.end()));
return ans;
}
};Java
import java.util.*;
class TreeNode { int val; TreeNode left; TreeNode right; TreeNode(){} TreeNode(int val){this.val=val;} TreeNode(int val, TreeNode left, TreeNode right){this.val=val;this.left=left;this.right=right;} }
class ListNode { int val; ListNode next; ListNode(){} ListNode(int val){this.val=val;} ListNode(int val, ListNode next){this.val=val;this.next=next;} }
class Solution {
public int orderOfLargestPlusSign(int n, int[][] mines) {
int[][] dp = new int[n][n];
for (int[] e : dp) {
Arrays.fill(e, n);
}
for (int[] e : mines) {
dp[e[0]][e[1]] = 0;
}
for (int i = 0; i < n; ++i) {
int left = 0, right = 0, up = 0, down = 0;
for (int j = 0, k = n - 1; j < n; ++j, --k) {
left = dp[i][j] > 0 ? left + 1 : 0;
right = dp[i][k] > 0 ? right + 1 : 0;
up = dp[j][i] > 0 ? up + 1 : 0;
down = dp[k][i] > 0 ? down + 1 : 0;
dp[i][j] = Math.min(dp[i][j], left);
dp[i][k] = Math.min(dp[i][k], right);
dp[j][i] = Math.min(dp[j][i], up);
dp[k][i] = Math.min(dp[k][i], down);
}
}
return Arrays.stream(dp).flatMapToInt(Arrays::stream).max().getAsInt();
}
}复杂度
时间
O(n²)
四个方向各扫一遍 n × n 的表,常数倍的网格遍历
空间
O(n²)
一张和网格同样大的 dp 表,复用它存四方向的 min
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 最大加号标志 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
能不能不用四张方向表,只用一张 dp 表?+
可以,参考代码就是这么做的。四个方向的扫描互相独立,谁先谁后都不影响最终的 min,所以四遍能共用同一张 dp 表,每个方向数完就立刻和表里的值取 min、压一层。它甚至用一对反向下标 j 和 k 在同一重循环里同时推左右、上下四个方向的计数,连临时数组都省了,空间就一张表 O(n²)。
地雷非常多、几乎填满网格,复杂度会变吗?+
不会。无论地雷多少,四遍扫描都要把整张 n × n 的表走一遍,时间恒为 O(n²)。地雷多只会让更多格子的连续计数频繁清零、dp 值更小、答案更接近 0,但要遍历的格子数一个不少。决定复杂度的永远是 n,不是 mines 的数量。
只要盘面上有一个 1,答案至少是几?+
至少是 1。一个孤立的 1 自己就是一个 1 阶加号——1 阶只有中心那一格,四条臂长都是 0,不需要伸出任何臂。所以只要全表还剩一个 1,返回值就不会是 0;只有整张盘面被地雷填满、一个 1 都不剩时才返回 0。这也解释了为什么陆地格初值填 n 之后,最小也只会被压到 1 而不是 0。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 最大加号标志 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。