1. 题目解析与核心思路
1337题要求我们找出矩阵中战斗力最弱的K行。这里的"战斗力"定义为每行中1的个数(行内元素非递减排列,1总是出现在0之前)。我们需要先计算每行的战斗力值,然后根据这些值进行排序,最终返回前K个最弱行的索引。
1.1 输入输出分析
输入是一个m×n的二进制矩阵mat,其中:
- 每行的元素非递减排列(即所有1都在0的左侧)
- 需要返回战斗力最弱的K个行的索引(从0开始计数)
- 如果多行战斗力相同,则按行号从小到大排列
示例: 输入:mat = [[1,1,0,0,0], [1,1,1,1,0], [1,0,0,0,0], [1,1,0,0,0], [1,1,1,1,1]], k = 3 输出:[2,0,3]
1.2 关键算法选择
这道题的核心在于如何高效计算每行的战斗力(即1的个数),然后进行排序。由于题目给出的矩阵每行都是非递减排列,这给了我们优化空间。
常见解法有:
- 暴力遍历每行统计1的个数 - 时间复杂度O(mn)
- 二分查找每行最后一个1的位置 - 时间复杂度O(m log n)
- 从右往左线性扫描 - 最优情况下O(m+n)
2. 最优解法实现
2.1 二分查找法实现
对于每行,我们可以使用二分查找来快速定位最后一个1的位置:
def kWeakestRows(mat, k): def count_soldiers(row): left, right = 0, len(row) while left < right: mid = (left + right) // 2 if row[mid] == 1: left = mid + 1 else: right = mid return left strengths = [(count_soldiers(row), i) for i, row in enumerate(mat)] strengths.sort() return [i for (_, i) in strengths[:k]]2.2 线性扫描优化
由于矩阵的特殊性质,我们还可以从右往左扫描:
def kWeakestRows(mat, k): m, n = len(mat), len(mat[0]) res = [] visited = set() # 按列扫描 for j in range(n): for i in range(m): if i not in visited and mat[i][j] == 0: res.append(i) visited.add(i) if len(res) == k: return res # 处理全1的行 for i in range(m): if i not in visited: res.append(i) if len(res) == k: return res return res3. 复杂度分析与比较
3.1 时间复杂度
- 暴力法:O(mn) - 最坏情况下需要遍历所有元素
- 二分法:O(m log n) - 对每行进行二分查找
- 线性扫描:O(m + n) - 最优解,只需扫描到找到足够的行
3.2 空间复杂度
三种方法都是O(m)空间,需要存储每行的战斗力值或结果集。
4. 边界条件与测试用例
4.1 常见边界情况
- K等于矩阵行数
- 所有行战斗力相同
- 矩阵只有一列
- 矩阵所有元素都是1
4.2 测试用例设计
test_cases = [ ([[1,1],[1,1],[1,0]], 2), # 正常情况 ([[1],[1],[0]], 3), # 单列情况 ([[1,1,1],[1,1,1]], 2), # 全1矩阵 ([[0,0],[0,0]], 1), # 全0矩阵 ([[1,0],[1,1],[1,1]], 1) # K=1情况 ]5. 实际编码技巧
5.1 Python优化技巧
- 使用enumerate同时获取索引和值
- 利用元组自动排序的特性(先按第一个元素,再按第二个)
- 列表推导式简化代码
5.2 常见错误
- 忘记处理行号相同的情况
- 二分查找边界条件错误
- 没有考虑全1行的情况
提示:在面试中,建议先提出暴力解法,然后逐步优化,展示思考过程比直接给出最优解更重要。
6. 扩展思考
6.1 变种问题
- 如果矩阵不是非递减排列,如何解决?
- 如果要找战斗力最强的K行?
- 如果矩阵很大无法放入内存?
6.2 实际应用
这类问题在实际中可用于:
- 用户评分分析(找出评分最低的K个商品)
- 系统监控(找出性能最差的K个节点)
- 特征选择(选择区分度最高的K个特征)
7. 性能优化实战
我在LeetCode提交时发现,当矩阵非常大时(如1000×1000),即使是二分法也可能超时。这时可以考虑以下优化:
- 提前终止:当找到足够的弱行时就停止计算
- 并行计算:使用多线程分别处理不同行
- 位运算优化:如果矩阵用位表示,可以用位操作加速统计
# 并行计算示例 from concurrent.futures import ThreadPoolExecutor def parallel_kWeakestRows(mat, k): def count_row(i): row = mat[i] left, right = 0, len(row) while left < right: mid = (left + right) // 2 if row[mid] == 1: left = mid + 1 else: right = mid return (left, i) with ThreadPoolExecutor() as executor: strengths = list(executor.map(count_row, range(len(mat)))) strengths.sort() return [i for (_, i) in strengths[:k]]8. 语言特定实现
8.1 C++实现
vector<int> kWeakestRows(vector<vector<int>>& mat, int k) { vector<pair<int, int>> strength; for (int i = 0; i < mat.size(); ++i) { int cnt = count(mat[i].begin(), mat[i].end(), 1); strength.emplace_back(cnt, i); } sort(strength.begin(), strength.end()); vector<int> res; for (int i = 0; i < k; ++i) { res.push_back(strength[i].second); } return res; }8.2 Java实现
public int[] kWeakestRows(int[][] mat, int k) { PriorityQueue<int[]> pq = new PriorityQueue<>( (a, b) -> a[0] != b[0] ? b[0] - a[0] : b[1] - a[1]); for (int i = 0; i < mat.length; i++) { int soldiers = 0; for (int val : mat[i]) { if (val == 1) soldiers++; else break; } pq.offer(new int[]{soldiers, i}); if (pq.size() > k) pq.poll(); } int[] res = new int[k]; while (k > 0) res[--k] = pq.poll()[1]; return res; }9. 可视化理解
为了更好理解算法,我们可以将矩阵可视化:
行0: [1,1,0,0,0] → 战斗力2 行1: [1,1,1,1,0] → 战斗力4 行2: [1,0,0,0,0] → 战斗力1 行3: [1,1,0,0,0] → 战斗力2 行4: [1,1,1,1,1] → 战斗力5排序后得到战斗力序列:[ (1,2), (2,0), (2,3), (4,1), (5,4) ] 取前K=3个得到结果:[2,0,3]
10. 总结与个人心得
这道题看似简单,但考察了多个重要知识点:
- 二分查找的应用与变种
- 排序算法的灵活使用
- 边界条件的处理能力
在实际编码中,我发现有几点特别重要:
- 二分查找的终止条件容易写错,需要仔细验证
- Python中元组排序的特性可以大大简化代码
- 对于特殊测试用例(如全1矩阵)要单独考虑
建议在面试中,可以先写出暴力解法,然后分析其瓶颈,再逐步优化到二分查找或线性扫描解法,展示完整的思考过程。同时要注意代码的整洁性和变量命名的规范性。