1337. 方阵中战斗力最弱的 K 行
Hi 大家好,我是张小猪。欢迎来到『宝宝也能看懂』系列之 leetcode 周赛题解。
这里是第 174 期的第 1 题,也是题目列表中的第 1337 题 -- 『方阵中战斗力最弱的 K 行』
题目描述
给你一个大小为 m * n
的方阵 mat
,方阵由若干军人和平民组成,分别用 0
和 1
表示。
请你返回方阵中战斗力最弱的 k
行的索引,按从最弱到最强排序。
如果第 i
行的军人数量少于第 j
行,或者两行军人数量相同但 i
小于 j
,那么我们认为第 i
行的战斗力比第 j
行弱。
军人 总是 排在一行中的靠前位置,也就是说 1 总是出现在 0 之前。
示例 1:
输入: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]
解释:
每行中的军人数目:
行 0 -> 2
行 1 -> 4
行 2 -> 1
行 3 -> 2
行 4 -> 5
从最弱到最强对这些行排序后得到 [2,0,3,1,4]
示例 2:
输入:mat =
[[1,0,0,0],
[1,1,1,1],
[1,0,0,0],
[1,0,0,0]],
k = 2
输出:[0,2]
解释:
每行中的军人数目:
行 0 -> 1
行 1 -> 4
行 2 -> 1
行 3 -> 1
从最弱到最强对这些行排序后得到 [0,2,3,1]
提示:
m == mat.length
n == mat[i].length
2 <= n, m <= 100
1 <= k <= m
-
matrix[i][j]
不是 0 就是 1
官方难度
EASY
解决思路
题目把简单的内容进行了一点包装,不过相信小伙伴们也很容易能看出来。什么,还有宝宝没看出来?那就让小猪来揭开它神秘的头盖骨吧! yeah~
首先是给定的数据是一个二维数组,其中每一行里有士兵(用 1 表示)和平民(用 0 表示),并且士兵一定是在平民前面。这句话背后透露了几个信息:
- 数据只有 0 和 1,并且一行的士兵数量其实就是这一行值求和的结果。
- 如果一个位置是平民,那么士兵的数量一定小于这个值。即这一行的数据是有序的。
然后我们再看,需求是要返回战斗力排名前 k
的行的序号。也就是说,我们需要按照每行的战斗力进行排序,而战斗力就是士兵的数量。那么结合上面的信息,我们直接的思路就很清晰了。
直接方案
根据上面的分析,我们可以很容易的得到直接方案的流程如下:
- 对每一行的数据求和,连同序号一起放进新的数组。
- 按照要求对该数组进行排序。
- 返回前
k
个的需要。
基于这个流程,我们可以实现类似下面的代码:
const kWeakestRows = (mat, k) => {
const m = mat.length;
const n = mat[0].length;
const ret = [];
for (let i = 0; i < m; ++i) {
let cur = 0;
for (let j = 0; j < n; ++j, ++cur) {
if (mat[i][j] === 0) break;
}
ret.push([cur, i]);
}
return ret
.sort((a, b) => a[0] === b[0] ? a[1] - b[1] : a[0] - b[0])
.slice(0, k)
.map(item => item[1]);
};
优化
上面的方案其实我们只用到了信息中的第一条。那么第二条信息,士兵一定在平民左边,每一行是有序的,这个我们该如何利用呢?我们可以想象一下,结合这一条信息,如果我们知道了最后一个士兵的位置,是不是就已经知道了士兵的数量?而在一个有序数组中,寻找一个目标值通用的最快的方式应该可以从 O(n) 变成 O(logn) 级别,也就是利用二分查找。
具体流程如下:
- 利用二分查找,寻找每一行第一个 0 的位置,并把它和序号一起放进新的数组。
- 按照要求对该数组进行排序。
- 返回前
k
个的需要。
基于这个流程,我们可以实现类似下面的代码:
const kWeakestRows = (mat, k) => {
const m = mat.length;
const n = mat[0].length;
const rows = [];
const ret = new Uint8Array(k);
for (let i = 0; i < m; ++i) {
rows.push([search(mat[i], 0, n), i]);
}
rows.sort((a, b) => a[0] === b[0] ? a[1] - b[1] : a[0] - b[0]);
for (let i = 0; i < k; ++i) {
ret[i] = rows[i][1];
}
return ret;
function search(arr, left, right) {
if (left === right) return left;
const mid = Math.floor((left + right) / 2);
return arr[mid] === 0 ? search(arr, left, mid) : search(arr, mid + 1, right);
}
};
换个思路
我们再试试从另外一个角度看这个问题。根据我们之前得到的信息,如果一个位置出现了平民,那么它的右边就不再会有士兵了,也就是和所它的战斗力已经被确定了,也就是说其实它在我们上面排序中的位置也就已经确定了。
那么基于这个思路,我们来纵向的看一下数据,即一列一列的看。我们会发现,当我们在某一列遇到某行第一次出现 0 的时候,它其实就是我们目前状态下的最小战斗力。而我们最终需要的其实就是前 k
个这样的值。
不过有一点需要注意的是,由于可能会出现多个战斗力全满的行,所以最后还需要再处理一下这种情况。
具体流程如下:
- 一列一列的遍历原始数据。
- 如果遇到出现了 0,并且是在没有被访问过的行,那么把行号放进结果,并记录这一行已经被访问了。
- 处理可能的战斗力全满的情况。
基于这个流程,我们可以实现类似下面的代码:
const kWeakestRows = (mat, k) => {
const m = mat.length;
const n = mat[0].length;
const ret = new Uint8Array(k);
const visited = new Uint8Array(m);
let idx = 0;
for (let i = 0; i < n; ++i) {
for (let j = 0; j < m; ++j) {
if (visited[j] === 0 && mat[j][i] === 0) {
ret[idx] = j;
visited[j] = 1;
if (++idx === k) return ret;
}
}
}
for (let i = 0; i < m; ++i) {
if (visited[i] === 0) {
ret[idx] = i;
if (++idx === k) return ret;
}
}
};
总结
周赛第一题惯例送分。小猪这里提到了不同的思路和一些小优化,不过重点还是最开始根据题目描述得到的信息,因为后面所有的内容都是基于前面的信息想到的。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。