668. 乘法表中第k小的数 : 经典「二分 + 计数判定」运用题

题目描述

这是 LeetCode 上的 668. 乘法表中第k小的数 ,难度为 困难

Tag : 「二分」、「计数」

几乎每一个人都用 乘法表,但是你能在乘法表中快速找到第 k k 小的数字吗?

给定高度 m m  、宽度 n n 的一张  m × n m \times n 的乘法表,以及正整数 k k ,你需要返回表中第 k k  小的数字。

例 1:

输入: m = 3, n = 3, k = 5

输出: 3

解释: 
乘法表:
1	2	3
2	4	6
3	6	9

第5小的数字是 3 (1, 2, 2, 3, 3).
复制代码

例 2:

输入: m = 2, n = 3, k = 6

输出: 6

解释: 
乘法表:
1	2	3
2	4	6

第6小的数字是 6 (1, 2, 2, 3, 4, 6).
复制代码

注意:

  • m m 和  n n  的范围在 [ 1 , 30000 ] [1, 30000] 之间。
  • k k 的范围在 [ 1 , m × n ] [1, m \times n] 之间。

二分 + 计数判定

由于 n n m m 的数据范围都是 3 × 1 0 4 3 \times 10^4 ,总数本身就超过了 1 0 7 10^7 ,我们考虑线性复杂度往下的对数复杂度。

题目要求我们求一维展开有序序列中的第 k k 小的数,问题本身具有「二段性」:

  • 答案右边的每个数均 满足「其在一维展开有序序列中左边数的个数大于等于 k k 个」
  • 答案左边的每个数均 不满足「其在一维展开有序序列中左边数的个数大于等于 k k 个」

我们考虑如何进行「二分答案」: 假设当前我们二分到的值是 m i d mid ,对于乘法表中的每行和每列均是单调递增,我们可以通过累加统计 每行/每列 中比 m i d mid 小的数,记作 a a ,累加统计 每行/每列 中等于 m i d mid 的数,记作 b b ,那么 c n t = a + b cnt = a + b 即是整个乘法表中小于等于 m i d mid 的数的个数,再通过 c n t cnt k k 的大小关系来指导左右指针的变化。

具体的,假设我们通过枚举行来统计 a a b b ,当前枚举到的行号为 i i (行号从 1 1 开始),该行的最大数为 i × m i \times m

  • i × m < m i d i \times m < mid ,整行都是小于 m i d mid 的数,直接在 a a 基础上累加 m m
  • i × m > = m i d i \times m >= mid ,根据 m i d mid 是否存在于该行进行分情况讨论:
    • m i d mid 能够被 i i 整除,说明 m i d mid 存在于该行,那么比 m i d mid 小的数的个数为 m i d i 1 \frac{mid}{i} - 1 ,将其累加到 a a ,同时对 b b 进行加一;
    • m i d mid 不能够被 i i 整除,说明 m i d mid 不存在于该行,那么比 m i d mid 小的数的个数为 m i d i \frac{mid}{i} ,将其累加到 a a

一些细节:由于乘法表具有对称性,我们统计时可以对 行和列 中较小的一方进行遍历。

代码:

class Solution {
    int n, m, k;
    public int findKthNumber(int _m, int _n, int _k) {
        n = Math.min(_m, _n); m = Math.max(_m, _n); k = _k;
        int l = 1, r = m * n;
        while (l < r) {
            int mid = l + r >> 1, cnt = getCnt(mid);
            if (cnt >= k) r = mid;
            else l = mid + 1;
        }
        return r;
    }
    int getCnt(int mid) {
        int a = 0, b = 0;
        for (int i = 1; i <= n; i++) {
            if (i * m < mid) {
                a += m;
            } else {
                if (mid % i == 0 && ++b >= 0) a += mid / i - 1;
                else a += mid / i;
            }
        }
        return a + b;
    }
}
复制代码
  • 时间复杂度: O ( min ( n , m ) × log n m ) O(\min(n, m) \times \log{nm})
  • 空间复杂度: O ( 1 ) O(1)

最后

这是我们「刷穿 LeetCode」系列文章的第 No.668 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:github.com/SharingSour…

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。

猜你喜欢

转载自juejin.im/post/7098892656813539365