矩阵中的局部最大值 图解题解
这道题到底在问什么
- 输入
- grid=[[9,9,8,1],[5,6,2,6],[8,2,6,4],[6,2,2,2]]
- 输出
- [[9,9],[8,6]]
- 输入
- grid=[[1,1,1],[1,2,1],[1,1,1]]
- 输出
- [[2]]
最优解:为什么这么做
一句话答案:LeetCode 2373 矩阵中的局部最大值:一个 3 乘 3 窗口在网格上滑,每停一处把窗口九个数取最大填进结果格;结果本就只有 (n-2)² 个格,暴力枚举已是最优,时间 O(n²)、空间 O(1)。
结果网格为什么小一圈,每格填的是什么
给一张 n 行 n 列的整数网格 grid,要生成一张 (n-2) 行 (n-2) 列的网格 maxLocal。maxLocal 第 i 行第 j 列,等于 grid 里以第 i+1 行、第 j+1 列为中心那个 3 乘 3 方块里的最大值——也就是把 grid 上每一个挨着的 3 乘 3 方块的最大值都挑出来、按位置拼成结果。题面例子 grid=[[9,9,8,1],[5,6,2,6],[8,2,6,4],[6,2,2,2]],边长 4,横竖各能放下两个 3 乘 3 方块,结果就是 2 乘 2 的 [[9,9],[8,6]]。
拿窗口中心当基准往四周扩,行列偏移最先算岔
以每个方块的中心格为基准去凑那九个数,是这题最容易算岔的一种写法。中心在 grid 里的坐标是 (i+1, j+1),往四周扩要同时管 i+1 上下、j+1 左右一圈加减,行列偏移很容易写反或差一格。另一个念头是担心这样一格格枚举太慢——但结果本身就有大约 n² 个格子,每格固定看九个数,总量就是 O(n²),渐进意义上已经没法更快,暴力枚举反倒最干净。
改用左上角 (i,j) 当基准,九个数直接取最大
把基准从中心换成窗口的左上角,一切就顺了。让结果格 (i,j) 对应的窗口,左上角正好落在 grid 的 (i,j),那这个窗口盖住的就是第 i 到 i+2 行、第 j 到 j+2 列共九个数,maxLocal[i][j] 填的就是这九个数的最大值。外层两重循环让 i、j 各从 0 走到 n-3,把结果格挨个填满;内层把 i..i+2、j..j+2 这九个位置扫一遍取最大。i 最大到 n-3 时,i+2 正好是 n-1、最后一行,窗口永远贴着边、不会滑出界。
拿题面的 4 乘 4 网格把四个窗口挨个算
结果是 2 乘 2,四个窗口对应四个左上角。左上角 (0,0):盖住第 0 到 2 行、第 0 到 2 列,九个数是 9、9、8、5、6、2、8、2、6,最大 9,填进 maxLocal(0,0)。左上角 (0,1) 往右挪一列,九个数 9、8、1、6、2、6、2、6、4,最大还是 9。左上角 (1,0) 往下挪一行,九个数 5、6、2、8、2、6、6、2、2,最大 8。左上角 (1,1) 在右下,九个数 6、2、6、2、6、4、2、2、2,最大 6。四个值按位置拼起来是 [[9,9],[8,6]]。
只看九个数的代价,还有哪些地方最容易写反
复杂度上,结果有 (n-2)² 个格子,约等于 n²;每格固定扫九个数取最大,是常数工作量,乘起来抹掉常数就是时间 O(n²)。额外空间只用了几个循环变量和一个记最大值的临时量,是 O(1),结果网格是题目要返回的产物,不算额外开销。
有几处坑得先避开:n=3 时只放得下一个窗口,结果是 1 乘 1,填的就是整张网格的最大值;C++、Java 里记最大值的初值要设成 0 再取最大,因为格子值都不小于 1,若像求最小那样拿很大的数起步再取最大就整个反了;结果尺寸也务必开成 (n-2)×(n-2),开成 n 乘 n 会多出没意义的空行空列。
▶ 动画逐步走查(共 24 步)——想跟着动画一帧帧对照就展开
- 3记住这一句:一个 3 乘 3 的窗口在网格上滑,停在哪里就把窗口里九个数的最大值填到结果对应的格子。下面从左上角的窗口开始,一个窗口一个窗口地看。
- 4输入:4×4这是输入的 grid,四行四列。为什么结果是 2 乘 2 呢?因为一个 3 乘 3 的窗口在 4 格宽的行里,左上角只能放在第 0 列或第 1 列两个位置,再往右窗口就出界了,竖着同理。所以窗口一共有两行两列共四个落点,结果自然是 2 乘 2。心里先有这个数。
- 5框出窗口先把窗口框出来给你看。左上角这个 3 乘 3 的方块,盖住了 grid 左上九个数,它们的最大值就是结果 maxLocal 第 0 行第 0 列的值。橙色框住的九格就是一个窗口。接下来我把窗口里的数一行一行扫过去,盯着当前见过的最大值,看它落在哪一格。
- 6左上方块开工窗口停在左上角,左上角坐标是 (0,0),它盖住的九个数就是 grid 前三行前三列。目标是把这九个数里的最大值,填到结果 maxLocal 的 (0,0)。我准备一个记号,叫它当前最大,一开始还没看任何数。现在开始一行一行扫窗口。
- 7第 1 行,最大 9先扫窗口最上面一行,三个数是 9、9、8。从没有数到有数,当前最大直接记成这一行里最大的 9,它在 (0,0),我用紫色把它点亮。
- 8第 2 行,最大 9接着扫窗口的第 2 行,三个数是 5、6、2。这三个数都没超过之前的 9,当前最大不变,紫色格子还停在 (0,0)。
- 9第 3 行,最大 9接着扫窗口的第 3 行,三个数是 8、2、6。这三个数都没超过之前的 9,当前最大不变,紫色格子还停在 (0,0)。 九个数全扫完了,这个窗口的最大值就定在 9。
- 10左上 → 9这个窗口九个数扫完,最大是 9,在 (0,0),我把它染成绿色。把 9 写进结果 maxLocal 的 (0,0),右边答案面板多了一行,已填格子数涨到 1。窗口接着往下一个位置滑。
- 11右上方块开工窗口从上一处滑到 (0,1),这是右上方块,盖住 grid 第 0 到 2 行、第 1 到 3 列。老规矩,当前最大清零重来,再把这九个数一行一行扫一遍,找出这个窗口自己的最大值。
- 12第 1 行,最大 9先扫窗口最上面一行,三个数是 9、8、1。从没有数到有数,当前最大直接记成这一行里最大的 9,它在 (0,1),我用紫色把它点亮。
- 13第 2 行,最大 9接着扫窗口的第 2 行,三个数是 6、2、6。这三个数都没超过之前的 9,当前最大不变,紫色格子还停在 (0,1)。
- 14第 3 行,最大 9接着扫窗口的第 3 行,三个数是 2、6、4。这三个数都没超过之前的 9,当前最大不变,紫色格子还停在 (0,1)。 九个数全扫完了,这个窗口的最大值就定在 9。
- 15右上 → 9这个窗口九个数扫完,最大是 9,在 (0,1),我把它染成绿色。把 9 写进结果 maxLocal 的 (0,1),右边答案面板多了一行,已填格子数涨到 2。窗口接着往下一个位置滑。
- 16左下方块开工窗口从上一处滑到 (1,0),这是左下方块,盖住 grid 第 1 到 3 行、第 0 到 2 列。老规矩,当前最大清零重来,再把这九个数一行一行扫一遍,找出这个窗口自己的最大值。
- 17第 1 行,最大 6先扫窗口最上面一行,三个数是 5、6、2。从没有数到有数,当前最大直接记成这一行里最大的 6,它在 (1,1),我用紫色把它点亮。
- 18第 2 行,最大 8接着扫窗口的第 2 行,三个数是 8、2、6。这一行里冒出了更大的 8,当前最大被顶到 (2,0),紫色格子跟着挪过去。
- 19第 3 行,最大 8接着扫窗口的第 3 行,三个数是 6、2、2。这三个数都没超过之前的 8,当前最大不变,紫色格子还停在 (2,0)。 九个数全扫完了,这个窗口的最大值就定在 8。
- 20左下 → 8这个窗口九个数扫完,最大是 8,在 (2,0),我把它染成绿色。把 8 写进结果 maxLocal 的 (1,0),右边答案面板多了一行,已填格子数涨到 3。窗口接着往下一个位置滑。
- 21右下方块开工窗口从上一处滑到 (1,1),这是右下方块,盖住 grid 第 1 到 3 行、第 1 到 3 列。老规矩,当前最大清零重来,再把这九个数一行一行扫一遍,找出这个窗口自己的最大值。
- 22第 1 行,最大 6先扫窗口最上面一行,三个数是 6、2、6。从没有数到有数,当前最大直接记成这一行里最大的 6,它在 (1,1),我用紫色把它点亮。
- 23第 2 行,最大 6接着扫窗口的第 2 行,三个数是 2、6、4。这三个数都没超过之前的 6,当前最大不变,紫色格子还停在 (1,1)。
- 24第 3 行,最大 6接着扫窗口的第 3 行,三个数是 2、2、2。这三个数都没超过之前的 6,当前最大不变,紫色格子还停在 (1,1)。 九个数全扫完了,这个窗口的最大值就定在 6。
- 25右下 → 6这个窗口九个数扫完,最大是 6,在 (1,1),我把它染成绿色。把 6 写进结果 maxLocal 的 (1,1),右边答案面板多了一行,已填格子数涨到 4。四个窗口到这就全填完了。
- 26答案:[[9,9],[8,6]]四个窗口全部滑完了。左上窗口能看到顶上那两个 9,右上窗口能看到 (0,1) 这个 9;左下窗口的最大值是第 2 行的 8;右下窗口九个数最大是 6。把四个答案按位置拼起来,结果就是 [[9,9],[8,6]]。整道题从头到尾就是一个 3 乘 3 窗口滑过去、每停一处取一次最大值。
⚠️ 容易写错的地方
✗ 错:把窗口中心当成循环变量、再往四周扩
✓ 对:直接用窗口左上角 (i,j),扫 i..i+2 行、j..j+2 列
中心是 (i+1,j+1),用左上角当基准循环边界更干净,不容易把行列偏移算错
✗ 错:结果矩阵开成 n 乘 n
✓ 对:结果是 (n-2) 乘 (n-2)
窗口左上角横竖各只能放 n-2 个位置,开大了会多出没意义的空行空列或越界
✗ 错:C++/Java 里把 ans 初始化成很大的数再取最小
✓ 对:这题是取最大,初始化成 0 再取最大
要的是窗口最大值,初值必须比所有可能值都小;格子值不小于 1,用 0 起步最稳妥
✗ 错:担心 3×3 窗口会滑出界
✓ 对:循环上界是 n-3,天然保证 i+2、j+2 不越界
只要 i 最大到 n-3,i+2 就最大到 n-1,正好是最后一行,窗口永远贴边不出界
完整代码(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 largestLocal(self, grid: List[List[int]]) -> List[List[int]]:
n = len(grid)
ans = [[0] * (n - 2) for _ in range(n - 2)]
for i in range(n - 2):
for j in range(n - 2):
ans[i][j] = max(
grid[x][y] for x in range(i, i + 3) for y in range(j, j + 3)
)
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:
vector<vector<int>> largestLocal(vector<vector<int>>& grid) {
int n = grid.size();
vector<vector<int>> ans(n - 2, vector<int>(n - 2));
for (int i = 0; i < n - 2; ++i) {
for (int j = 0; j < n - 2; ++j) {
for (int x = i; x <= i + 2; ++x) {
for (int y = j; y <= j + 2; ++y) {
ans[i][j] = max(ans[i][j], grid[x][y]);
}
}
}
}
return ans;
}
};Java
import java.util.*;
class Solution {
public int[][] largestLocal(int[][] grid) {
int n = grid.length;
int[][] ans = new int[n - 2][n - 2];
for (int i = 0; i < n - 2; ++i) {
for (int j = 0; j < n - 2; ++j) {
for (int x = i; x <= i + 2; ++x) {
for (int y = j; y <= j + 2; ++y) {
ans[i][j] = Math.max(ans[i][j], grid[x][y]);
}
}
}
}
return ans;
}
}复杂度
时间
O(n²)
结果矩阵有 (n-2) 乘 (n-2) 个格子,约等于 n² 个;每个格子固定看 9 个数取最大,是常数工作量。9 乘 (n-2)² 抹掉常数就是 O(n²),随边长平方增长
空间
O(1)
按额外占用算。只用了几个循环变量和一个记最大值的临时量,都是常数;结果矩阵是题目要求返回的产物,不计入额外空间。所以额外空间是 O(1),不随 n 变大
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 矩阵中的局部最大值 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
这题为什么不用担心 3×3 窗口滑出界?+
关键在循环上界。让窗口左上角 (i,j) 里的 i、j 各只走到 n-3,那 i+2 最大就是 n-1、j+2 最大也是 n-1,正好是网格的最后一行、最后一列,窗口的右下角永远贴着边、不会越出去。所以循环里根本不用额外判断边界,把上界卡死在 n-3,越界的可能就从源头掐掉了。
窗口固定 3×3 已经 O(n²) 了,还有优化空间吗?+
就本题而言没有。结果矩阵本身就有大约 n² 个格子,光是把它们写出来就得 O(n²),而每格只看九个数取最大是常数,总时间已经贴着输出规模的下限,直接枚举最清楚。要是窗口大小不固定、变成 k 乘 k,才值得优化:先对每一行做长度为 k 的滑动窗口最大值,再对得到的中间矩阵按列做一次同样的滑动窗口最大值,两步就能拿到每个方块的最大值,配合能均摊到常数的滑动窗口最值做法更省。
取最大值的初值该设成多少?+
设成 0 最稳。这题网格里的数都不小于 1,把记最大值的临时量初始化成 0,第一个扫到的数一定比它大、能正常顶上去,最后留下的就是九个数里的最大值。反过来,若像求最小值那样把初值设成一个很大的数再取最大,逻辑就整个反了,结果会一直卡在那个大初值上。Python 里也可以直接用 max() 套住这九个数,连手设初值都省了。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 矩阵中的局部最大值 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。