4

1337. 方阵中战斗力最弱的 K 行

Hi 大家好,我是张小猪。欢迎来到『宝宝也能看懂』系列之 leetcode 周赛题解。

这里是第 174 期的第 1 题,也是题目列表中的第 1337 题 -- 『方阵中战斗力最弱的 K 行』

题目描述

给你一个大小为 m * n 的方阵 mat,方阵由若干军人和平民组成,分别用 01 表示。

请你返回方阵中战斗力最弱的 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 的行的序号。也就是说,我们需要按照每行的战斗力进行排序,而战斗力就是士兵的数量。那么结合上面的信息,我们直接的思路就很清晰了。

直接方案

根据上面的分析,我们可以很容易的得到直接方案的流程如下:

  1. 对每一行的数据求和,连同序号一起放进新的数组。
  2. 按照要求对该数组进行排序。
  3. 返回前 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) 级别,也就是利用二分查找。

具体流程如下:

  1. 利用二分查找,寻找每一行第一个 0 的位置,并把它和序号一起放进新的数组。
  2. 按照要求对该数组进行排序。
  3. 返回前 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 个这样的值。

不过有一点需要注意的是,由于可能会出现多个战斗力全满的行,所以最后还需要再处理一下这种情况。

具体流程如下:

  1. 一列一列的遍历原始数据。
  2. 如果遇到出现了 0,并且是在没有被访问过的行,那么把行号放进结果,并记录这一行已经被访问了。
  3. 处理可能的战斗力全满的情况。

基于这个流程,我们可以实现类似下面的代码:

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;
    }
  }
};

总结

周赛第一题惯例送分。小猪这里提到了不同的思路和一些小优化,不过重点还是最开始根据题目描述得到的信息,因为后面所有的内容都是基于前面的信息想到的。

相关链接

qrcode_green.jpeg


张小猪粉鼻子
2.9k 声望407 粉丝

一只粉色的小猪,喜欢写写代码,看看电影,玩玩游戏社恐,不太会讲话永远跟不上热点