矩阵中的幸运数 图解题解
这道题到底在问什么
- 输入
- matrix=[[3,7,8],[9,11,13],[15,16,17]]
- 输出
- [15]
- 输入
- matrix=[[7,8],[1,2]]
- 输出
- [7]
先想最直接的笨办法
两份结果都齐了:每行最小是 4、10、3,每列最大是 11、10、14、15。最后一步,把每个行最小拿去对它那一列的最大值。如果某个行最小恰好就是它那一列的最大值,说明它行里最小、列里又最大,就是幸运数。咱们一个一个判。(动画第 30 步)
最优解:为什么这么做
一句话答案:LeetCode 1380 矩阵中的幸运数:幸运数在它那行最小、同时在它那列最大。分别求出每行最小、每列最大两份集合,取交集就是答案,数字各不相同时至多一个,时间 O(m·n)。
要找的幸运数,到底长什么样
给一个 m 行 n 列、数字各不相同的矩阵 matrix。幸运数要同时满足两条:在它所在那一行里最小,在它所在那一列里又最大,缺一条都不算。要返回的是这些数本身,不是它们的行号列号,顺序随意。题面例子 matrix=[[3,7,8],[9,11,13],[15,16,17]] 的答案是 [15],换成 [[7,8],[1,2]] 答案是 [7]。
照定义逐格验,慢在哪
一个数是不是幸运数,天然要看它那一行和那一列。照定义直接做,就对每个格子往它整行扫一遍确认最小、再往它整列扫一遍确认最大。矩阵有 m·n 个格子,每个都要花 m+n 的功夫核对,叠起来是 O(m·n·(m+n)),同一行、同一列被反复扫了很多遍,浪费全在这些重复上。
两份清单一取交,条件就凑齐了
这些重复是白费的:每行的最小、每列的最大各自只有一份,不必为每个格子重算。先扫一遍矩阵,把每行的最小值凑成集合 rows;再扫一遍,把每列的最大值凑成集合 cols。一个数在行里最小,它必然出现在 rows 里;在列里最大,必然出现在 cols 里。两个条件同时成立,等价于它同时落在两份集合中,也就是落在 rows 和 cols 的交集里。求出交集,就是全部幸运数。
三步走:行最小、列最大、求交集
落到三步。第一步逐行求最小,一行一行扫,每行记下最小的数丢进 rows。第二步逐列求最大,换成一列一列扫,每列记下最大的数丢进 cols。第三步求交集,既在 rows 又在 cols 里的数就是幸运数,收集返回。参考代码里 min(row) 求行最小、zip 星号 matrix 把矩阵转置后对每行 max 求出列最大,最后 rows & cols 求交集并返回列表。
拿题面的 3×3 矩阵,两趟扫下来看结果
逐行求最小:第 0 行 3、7、8 里最小是 3,第 1 行 9、11、13 里最小是 9,第 2 行 15、16、17 里最小是 15,rows={3,9,15}。逐列求最大:第 0 列 3、9、15 里最大是 15,第 1 列 7、11、16 里最大是 16,第 2 列 8、13、17 里最大是 17,cols={15,16,17}。求交集:3 和 9 都不在 cols,只有 15 两边都有——它在第 2 行里最小、又在第 0 列里最大,答案 [15]。
复杂度,还有两处一记反就全错
求每行最小、每列最大各把矩阵完整扫一遍,都是 O(m·n),整体由扫矩阵主导,时间 O(m·n);两份集合分别存 m 个和 n 个数,空间 O(m+n)。最坑的是把两条件记反——它要行里最小、列里最大,按行最大列最小去找,方向一反答案全错。另一种漏法是只验了行最小就下结论:题面里第 0 行最小 3、第 1 行最小 9 都是各自行里最小,可它俩所在列的最大都是 15,漏验列这一半,就会把 3 和 9 也误当幸运数。数字各不相同时幸运数至多一个,不必操心去重。
▶ 动画逐步走查(共 32 步)——想跟着动画一帧帧对照就展开
- 3记牢这三步:先求每行最小、再求每列最大、最后拿行最小去对它那一列的最大值,相等的就是幸运数。下面每一帧都在套这三步,先从逐行求最小开始。
- 4橙格 = 当前看的数,浅橙 = 这行暂时的最小这就是 3 行 4 列的矩阵,里面的数字各不相同。咱们先一行一行求最小值。右边面板记每行的最小值,现在还空着。从行 0 开始,从左往右扫,边走边记住目前见过的最小数。
- 5橙 = 当前,浅橙 = 暂时最小行 0 从第一个数开始,第 0 列是 4,暂时把它当成这行的最小值。接着往右看,看有没有更小的。
- 6橙 = 当前,浅橙 = 暂时最小行 0 第 1 列是 7,比当前最小 4 大,最小值不变,还是 4。继续往右。
- 7橙 = 当前,浅橙 = 暂时最小行 0 第 2 列是 14,比当前最小 4 大,最小值不变,还是 4。继续往右。
- 8扫到第 3 列(9),本行最小 = 4行 0 第 3 列是 9,比当前最小 4 大,最小值不变。这一行扫完了,最小值是 4,在第 0 列,记进右边面板。
- 9橙 = 当前,浅橙 = 暂时最小行 1 从第一个数开始,第 0 列是 11,暂时把它当成这行的最小值。接着往右看,看有没有更小的。
- 10橙 = 当前,浅橙 = 暂时最小行 1 第 1 列是 10,比刚才的最小 11 还小,最小值换成 10,在第 1 列。继续往右。
- 11橙 = 当前,浅橙 = 暂时最小行 1 第 2 列是 13,比当前最小 10 大,最小值不变,还是 10。继续往右。
- 12扫到第 3 列(12),本行最小 = 10行 1 第 3 列是 12,比当前最小 10 大,最小值不变。这一行扫完了,最小值是 10,在第 1 列,记进右边面板。
- 13橙 = 当前,浅橙 = 暂时最小行 2 从第一个数开始,第 0 列是 3,暂时把它当成这行的最小值。接着往右看,看有没有更小的。
- 14橙 = 当前,浅橙 = 暂时最小行 2 第 1 列是 5,比当前最小 3 大,最小值不变,还是 3。继续往右。
- 15橙 = 当前,浅橙 = 暂时最小行 2 第 2 列是 6,比当前最小 3 大,最小值不变,还是 3。继续往右。
- 16扫到第 3 列(15),本行最小 = 3行 2 第 3 列是 15,比当前最小 3 大,最小值不变。这一行扫完了,最小值是 3,在第 0 列,记进右边面板。
- 17橙格 = 当前看的数,浅橙 = 这列暂时的最大每行最小值都拿到了:行 0 是 4、行 1 是 10、行 2 是 3。现在换个方向,一列一列求最大值。从第 0 列开始,从上往下扫,边走边记住目前见过的最大数。
- 18橙 = 当前,浅橙 = 暂时最大第 0 列从最上面开始,行 0 是 4,暂时把它当成这列的最大值。往下看有没有更大的。
- 19橙 = 当前,浅橙 = 暂时最大第 0 列行 1 是 11,比刚才的最大还大,最大值换成 11,在第 1 行。继续往下。
- 20扫到第 2 行(3),本列最大 = 11第 0 列行 2 是 3,比当前最大 11 小,最大值不变。这一列扫完了,最大值是 11,记进右边面板。
- 21橙 = 当前,浅橙 = 暂时最大第 1 列从最上面开始,行 0 是 7,暂时把它当成这列的最大值。往下看有没有更大的。
- 22橙 = 当前,浅橙 = 暂时最大第 1 列行 1 是 10,比刚才的最大还大,最大值换成 10,在第 1 行。继续往下。
- 23扫到第 2 行(5),本列最大 = 10第 1 列行 2 是 5,比当前最大 10 小,最大值不变。这一列扫完了,最大值是 10,记进右边面板。
- 24橙 = 当前,浅橙 = 暂时最大第 2 列从最上面开始,行 0 是 14,暂时把它当成这列的最大值。往下看有没有更大的。
- 25橙 = 当前,浅橙 = 暂时最大第 2 列行 1 是 13,比当前最大 14 小,最大值不变,还是 14。继续往下。
- 26扫到第 2 行(6),本列最大 = 14第 2 列行 2 是 6,比当前最大 14 小,最大值不变。这一列扫完了,最大值是 14,记进右边面板。
- 27橙 = 当前,浅橙 = 暂时最大第 3 列从最上面开始,行 0 是 9,暂时把它当成这列的最大值。往下看有没有更大的。
- 28橙 = 当前,浅橙 = 暂时最大第 3 列行 1 是 12,比刚才的最大还大,最大值换成 12,在第 1 行。继续往下。
- 29扫到第 2 行(15),本列最大 = 15第 3 列行 2 是 15,最大值换成 15,在第 2 行。这一列扫完了,最大值是 15,记进右边面板。
- 30橙 = 待判定的行最小,浅橙 = 它那一列的最大两份结果都齐了:每行最小是 4、10、3,每列最大是 11、10、14、15。最后一步,把每个行最小拿去对它那一列的最大值。如果某个行最小恰好就是它那一列的最大值,说明它行里最小、列里又最大,就是幸运数。咱们一个一个判。
- 31列 0 的最大是 11,更大看行 0 的最小值 4,它在第 0 列。可这一列的最大值是 11,比 4 大,在第 1 行。4 在行里虽小,在列里却不是最大,差一口气,淘汰。
- 3210 同时是列 1 的最大看行 1 的最小值 10,它在第 1 列。这一列的最大值是 10,正好也是 10。行里它最小、列里它最大,两个条件同时满足,10 就是幸运数,亮起来。
- 33列 0 的最大是 11,更大看行 2 的最小值 3,它在第 0 列。可这一列的最大值是 11,比 3 大,在第 1 行。3 在行里虽小,在列里却不是最大,差一口气,淘汰。
- 34幸运数:10三个行最小都判完了:4 输给同列的 11,3 也输给同列的 11,只有 10 既是它那一行的最小,又是第 1 列的最大,稳稳当当当上幸运数。所以答案就是 [10]。整道题就是这三步:每行求最小、每列求最大、行最小对上该列最大就是幸运数。
⚠️ 容易写错的地方
✗ 错:把两个条件记反:以为是行最大、列最小
✓ 对:幸运数是行里最小、同时列里最大
方向记反就会找错格子,务必记牢「行最小、列最大」这一对搭配
✗ 错:只验了行最小,忘了再验列最大
✓ 对:两个条件要同时成立,缺一不可
本例里 4 和 3 都是各自行的最小,但同列有更大的 11,只验一半就会把它们误判成幸运数
✗ 错:担心会有很多个幸运数要去重
✓ 对:矩阵里幸运数最多只有一个
数字各不相同时,行最小且列最大的格子至多一个,不必担心重复,但代码仍按返回列表来写
✗ 错:返回成行号列号或最大最小的统计值
✓ 对:返回的是幸运数本身的数值列表
题目要的是那些数,不是它们的位置,也不是每行最小或每列最大的清单
完整代码(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 luckyNumbers(self, matrix: List[List[int]]) -> List[int]:
rows = {min(row) for row in matrix}
cols = {max(col) for col in zip(*matrix)}
return list(rows & cols)C++
#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<int> luckyNumbers(vector<vector<int>>& matrix) {
int m = matrix.size(), n = matrix[0].size();
int rows[m];
int cols[n];
memset(rows, 0x3f, sizeof(rows));
memset(cols, 0, sizeof(cols));
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
rows[i] = min(rows[i], matrix[i][j]);
cols[j] = max(cols[j], matrix[i][j]);
}
}
vector<int> ans;
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if (rows[i] == cols[j]) {
ans.push_back(rows[i]);
}
}
}
return ans;
}
};Java
import java.util.*;
class Solution {
public List<Integer> luckyNumbers(int[][] matrix) {
int m = matrix.length, n = matrix[0].length;
int[] rows = new int[m];
int[] cols = new int[n];
Arrays.fill(rows, 1 << 30);
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
rows[i] = Math.min(rows[i], matrix[i][j]);
cols[j] = Math.max(cols[j], matrix[i][j]);
}
}
List<Integer> ans = new ArrayList<>();
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if (rows[i] == cols[j]) {
ans.add(rows[i]);
}
}
}
return ans;
}
}复杂度
时间
O(m·n)
m 是行数,n 是列数。求每行最小要把整个矩阵扫一遍 O(m·n),求每列最大再扫一遍也是 O(m·n);对照判定时,数组写法两两比是 O(m·n),集合写法求交集是 O(m+n)。整体由扫矩阵主导,是 O(m·n)
空间
O(m + n)
存每行最小要 O(m),存每列最大要 O(n),按峰值是 O(m+n)。答案最多只有 min(m,n) 个数。集合写法峰值同样是 O(m+n),不随矩阵面积平方增长
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 矩阵中的幸运数 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么数字各不相同时,幸运数最多只有一个?+
用反证法。假设有两个幸运数 a 和 b,落在不同的行、不同的列。看 a 所在行和 b 所在列交叉的那个格子 c:因为 a 是它那行的最小,所以 a 小于等于 c;因为 b 是它那列的最大,所以 c 小于等于 b。连起来得 a 小于等于 b。同理,从 b 所在行、a 所在列交叉的格子又能推出 b 小于等于 a。两边一夹,只能 a 等于 b。可数字各不相同、不许相等,矛盾。所以幸运数至多一个。
能不能不开两个集合,把空间压得更省?+
可以分两趟。第一趟只求每行最小,把这些候选记下来,最多 m 个。第二趟对每个候选,单独去它那一列扫一遍,看它是不是这列的最大。这样只额外存了候选,空间压到 O(m)。代价是逐个候选扫列,验证这步变成 O(m 的平方),当列数 n 比行数小时总时间可能反而更慢,本质是拿时间换空间。实战里直接开两份集合更直白。
Python 里 zip 星号 matrix 到底做了什么?+
星号把 matrix 这个二维列表拆成一行一行的参数喂给 zip,zip 再把它们按列对齐打包,结果就是原矩阵的转置——原来的第 j 列变成了新的第 j 行。对转置后的每一行求 max,得到的正是原矩阵每一列的最大值。它是 Python 里做列方向统计一个很顺手的写法。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 矩阵中的幸运数 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。