矩阵中战斗力最弱的 K 行 图解题解
这道题到底在问什么
- 输入
- mat=[[1,0],[1,1]], k=1
- 输出
- [0]
- 输入
- mat=[[1,1,0],[1,0,0],[1,1,1]], k=2
- 输出
- [1,0]
先想最直接的笨办法
现在每行军人数都有了。排序规则有两条:军人数少的更弱,排在前面;军人数一样时,行号小的更弱。咱们就照这个规则,从最弱的一行开始,一个一个往答案里挑。(动画第 19 步)
最优解:为什么这么做
一句话答案:LeetCode 1337 矩阵中战斗力最弱的 K 行:数出每行军人数,每行 1 都在前、有序可二分定界,再按军人数和行号排序取前 k 个行号,时间 O(m log n + m log m)。
军人多就强、军人少就弱,这题要返回哪几行
给一个只含 0 和 1 的矩阵,1 是军人、0 是平民,每行里军人都排在平民前头。一行军人越多战斗力越强,越少越弱;两行军人一样多时,行号小的那行算更弱。要返回战斗力最弱的 k 行行号,从最弱排到最强。题面例子 mat=[[1,1,0],[1,0,0],[1,1,1]]、k 取 2,三行军人数分别是 2、1、3,最弱两行是行 1、行 0,输出 [1, 0]。
每行从左数一遍 1,能过、可惜没吃到有序这张牌
每行从左往右一个个数 1,碰到第一个 0 就停,得到军人数;m 行数完再按军人数排序、取前 k 行——这样能过,只是每行数 1 是 O(n)。可题目给了一个条件:每行军人都在平民前面,这一行前半段全 1、后半段全 0,本身是有序的。找 1 和 0 的分界不必一格格挪,在有序的一行里二分——每次砍掉一半来定位分界,O(log n) 就够,行很长时省得明显。
数军人、按军人数和行号排序、取前 k 行
整道题拆成三步。一,数出每行军人数,也就是这行 1 的个数,用二分找 1 和 0 的分界即可。二,把每行看成「军人数、行号」这一对,按军人数从少到多排,军人数相同再按行号从小到大排。三,取最前面 k 个的行号,就是从最弱到最强的前 k 行。
平局靠行号分高下,稳定排序省一半事
军人数相同的两行怎么排,是这题唯一的岔子。规则是行号小的更弱、排前面,把「军人数、行号」一起当排序关键字,军人数打平时自动看行号就对了。还有个更省的写法:行号本来就是 0、1、2 递增排着的,用稳定排序——也就是关键字相等时保持原来先后次序的排法——只按军人数排,军人数一样的几行会保留原本的行号升序,同样满足规则。若排序不保证这点、又没显式比行号,平局两行就可能被打乱,输出就错。
三行例子、k 取 2,最弱两行怎么挑出来
先数军人。行 0 是 1、1、0,前两格是军人、第三格是平民,2 名;行 1 是 1、0、0,只有第一格是军人,1 名;行 2 是 1、1、1,全是军人,3 名。三行配上行号就是「2, 0」「1, 1」「3, 2」。
再排序。按军人数从少到多:1 名的行 1 最弱排第一,2 名的行 0 第二,3 名的行 2 最强排最后。k 是 2,取最前面两行的行号,答案 [1, 0]。换第一个例子 mat=[[1,0],[1,1]]、k 取 1 也走一遍:行 0 有 1 名、行 1 有 2 名,最弱是行 0,返回 [0]。
把军人数当答案返回、平局比反行号,都会让输出错位
这题栽跟头多在收尾。返回的是行号、不是军人数,写成返回军人数就答非所问;只取最弱的前 k 个,漏了截断会多吐几行。平局那下也别把行号比反,行号小的才更弱、排前面。全 0 或军人数全打平时,纯按行号从小到大即可。
复杂度上,数军人每行二分 O(log n),m 行合起来 O(m log n);给 m 行按「军人数、行号」排序 O(m log m),整体 O(m log n + m log m)。Python 参考写法把每行逆序切片再 bisect,反转一行要 O(n),数军人那部分退成 O(m·n)。空间存每行军人数和行号是 O(m)。
▶ 动画逐步走查(共 23 步)——想跟着动画一帧帧对照就展开
- 3记牢这三步:数军人、按军人数和行号排序、取前 k 行。下面每一帧都在套这三步,先从数军人开始。
- 4绿格 = 军人(1),蓝格 = 平民(0)这就是 4 行 5 列的矩阵,绿色格子是军人也就是 1,蓝色格子是平民也就是 0。每行 1 都挤在左边。右边面板要记下每行的军人数,现在还空着。咱们一行一行来,从行 0 开始,从左往右数 1。
- 5橙格 = 正在看的格子看行 0 的第 0 格,是 1,一名军人,军人数加到 1。这一格是军人,继续往右看下一格。
- 6橙格 = 正在看的格子看行 0 的第 1 格,是 1,一名军人,军人数加到 2。这一格是军人,继续往右看下一格。
- 7橙格 = 停下的第一个平民行 0 的第 2 格是 0,平民,数 1 到此为止。这行一共 2 名军人,记进右边面板。
- 8橙格 = 正在看的格子看行 1 的第 0 格,是 1,一名军人,军人数加到 1。这一格是军人,继续往右看下一格。
- 9橙格 = 正在看的格子看行 1 的第 1 格,是 1,一名军人,军人数加到 2。这一格是军人,继续往右看下一格。
- 10橙格 = 正在看的格子看行 1 的第 2 格,是 1,一名军人,军人数加到 3。这一格是军人,继续往右看下一格。
- 11橙格 = 正在看的格子看行 1 的第 3 格,是 1,一名军人,军人数加到 4。这一格是军人,继续往右看下一格。
- 12橙格 = 停下的第一个平民行 1 的第 4 格是 0,平民,数 1 到此为止。这行一共 4 名军人,记进右边面板。
- 13橙格 = 正在看的格子看行 2 的第 0 格,是 1,一名军人,军人数加到 1。这一格是军人,继续往右看下一格。
- 14橙格 = 停下的第一个平民行 2 的第 1 格是 0,平民,数 1 到此为止。这行一共 1 名军人,记进右边面板。
- 15橙格 = 正在看的格子看行 3 的第 0 格,是 1,一名军人,军人数加到 1。这一格是军人,继续往右看下一格。
- 16橙格 = 正在看的格子看行 3 的第 1 格,是 1,一名军人,军人数加到 2。这一格是军人,继续往右看下一格。
- 17橙格 = 停下的第一个平民行 3 的第 2 格是 0,平民,数 1 到此为止。这行一共 2 名军人,记进右边面板。
- 18军人数 = 2、4、1、2四行都数完了。行 0 有 2 名,行 1 有 4 名,行 2 只有 1 名,行 3 有 2 名。军人数就是 2、4、1、2。注意行 0 和行 3 都是 2 名,打平了,这种平局等会儿要靠行号来分高下。下面进入排序。
- 19军人数少的更弱;平局看行号现在每行军人数都有了。排序规则有两条:军人数少的更弱,排在前面;军人数一样时,行号小的更弱。咱们就照这个规则,从最弱的一行开始,一个一个往答案里挑。
- 20橙格 = 已入答案的行先找军人数最少的。行 2 只有 1 名军人,是全场最少,妥妥的最弱一行。把行 2 第一个放进答案,现在答案是 [2]。
- 21浅橙 = 正在比较的两行接着找次弱的。剩下行 0、行 1、行 3,军人数分别是 2、4、2。最少的是 2,但行 0 和行 3 都是 2 名,打平了。这时候看行号:行 0 的行号比行 3 小,所以行 0 更弱,该它先走。
- 22橙格 = 已入答案的行平局靠行号小的更弱,行 0 胜出,排在行 3 前面。把行 0 放进答案,现在答案是 [2, 0]。还差一行就凑齐 3 个了。
- 23橙格 = 已入答案的行剩下行 1 和行 3,军人数 4 和 2。行 3 的 2 名更少,更弱,该它了。把行 3 放进答案,现在答案是 [2, 0, 3],正好凑满 3 行。剩下行 1 有 4 名军人,是最强的,排最后,用不上。
- 24弱 → 强:行2、行0、行3、行1把四行从弱到强排满就是行 2、行 0、行 3、行 1,对应军人数 1、2、2、4。其中行 0 和行 3 这对平局,靠行号让行 0 在前。k 是 3,只取最前面三行,后面的行 1 灰掉不要。
- 25战斗力最弱的 3 行战斗力最弱的 3 行,从弱到强就是行 2、行 0、行 3,答案数组是 [2, 0, 3]。这就是整道题:数清每行军人,按军人数和行号排序,取最弱的前 k 行行号。
⚠️ 容易写错的地方
✗ 错:把「战斗力」想成别的指标
✓ 对:战斗力就看军人数,也就是这行 1 的个数
1 越多越强、越少越弱,数清前导 1 的个数是整道题的根
✗ 错:军人数相同时忘了比行号或比反了
✓ 对:军人数一样时,行号小的更弱、排在前面
行 0 和行 3 都是 2 名,行号 0 比 3 小,所以行 0 更弱在前,搞反顺序答案就错
✗ 错:只按军人数排却用了不稳定排序
✓ 对:要么把(军人数,行号)一起当关键字排,要么用稳定排序
不稳定排序会打乱平局两行的原始行号次序,导致平局顺序错乱
✗ 错:返回成军人数或忘了只取前 k
✓ 对:返回的是行的索引,而且只取最弱的前 k 个
题目要的是行号列表、长度是 k,不是每行的军人数
完整代码(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 kWeakestRows(self, mat: List[List[int]], k: int) -> List[int]:
m, n = len(mat), len(mat[0])
ans = [n - bisect_right(row[::-1], 0) for row in mat]
idx = list(range(m))
idx.sort(key=lambda i: ans[i])
return idx[:k]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:
int search(vector<int>& m) {
int l = 0;
int h = m.size() - 1;
while (l <= h) {
int mid = l + (h - l) / 2;
if (m[mid] == 0)
h = mid - 1;
else
l = mid + 1;
}
return l;
}
vector<int> kWeakestRows(vector<vector<int>>& mat, int k) {
vector<pair<int, int>> p;
vector<int> res;
for (int i = 0; i < mat.size(); i++) {
int count = search(mat[i]);
p.push_back({count, i});
}
sort(p.begin(), p.end());
for (int i = 0; i < k; i++) {
res.push_back(p[i].second);
}
return res;
}
};Java
import java.util.*;
class Solution {
public int[] kWeakestRows(int[][] mat, int k) {
int m = mat.length, n = mat[0].length;
int[] res = new int[m];
List<Integer> idx = new ArrayList<>();
for (int i = 0; i < m; ++i) {
idx.add(i);
int[] row = mat[i];
int left = 0, right = n;
while (left < right) {
int mid = (left + right) >> 1;
if (row[mid] == 0) {
right = mid;
} else {
left = mid + 1;
}
}
res[i] = left;
}
idx.sort(Comparator.comparingInt(a -> res[a]));
int[] ans = new int[k];
for (int i = 0; i < k; ++i) {
ans[i] = idx.get(i);
}
return ans;
}
}复杂度
时间
C++/Java O(m·log n + m·log m);Python O(m·n + m·log m)
m 是行数,n 是列数。数每行军人:C++/Java 用二分是 O(log n),m 行共 O(m·log n);Python 用逆序切片反转每行再 bisect,反转就是 O(n),m 行共 O(m·n)(和直接从左扫同阶,慢在反转那一抄)。给 m 行按(军人数,行号)排序三家都是 O(m·log m)。整体由这两部分相加,差别只在数军人那块
空间
O(m)(Python 反转额外 O(n))
存每行军人数和行号这一组数据是 O(m),答案数组 O(k)。Python 每行逆序切片会另抄一份长度 n 的临时行,峰值多占 O(n)。排序内部开销:C++ 与 Java 约 O(log m) 递归栈,Python 的 sort 最坏 O(m),按峰值整体记 O(m)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 矩阵中战斗力最弱的 K 行 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
数每行军人,为什么能用二分、比从左扫快在哪?+
因为题目保证每行 1 都排在 0 前面,这一行其实是有序的,前半段全 1、后半段全 0。要找 1 和 0 的分界,就是在有序的一行里找第一个 0 的位置,二分一下就能定位,时间是 O(log n);从左一个个扫是 O(n)。当每行很长时,二分明显更快。
平局时为什么稳定排序能自动按行号升序,不用显式比行号?+
一开始行号本来就是 0、1、2 这样从小到大排的。稳定排序的特点是,关键字相等时保持元素原来的先后次序。所以只按军人数排,军人数相同的几行会保持它们原来的行号升序,正好满足「行号小的更弱在前」。要更直接,就把「军人数、行号」打包成一对排序,平局自动看行号。
如果 k 远小于行数,有没有更省的办法?+
可以用一个大小为 k 的大顶堆,按「军人数、行号」从强到弱比较。遍历每行,把这行的「军人数、行号」入堆,堆超过 k 个就弹掉当前最强的那个。扫完后堆里剩下的就是最弱的 k 行,再按弱到强整理输出。堆维护部分是 O(m log k),加上每行数军人,总时间 C++ 与 Java 为 O(m log n + m log k)。当 k 远小于行数时,这比整体排序更省。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 矩阵中战斗力最弱的 K 行 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。