【C/C++】668. 乘法表中第k小的数

142 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第14天,点击查看活动详情


题目链接:668. 乘法表中第k小的数

题目描述

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

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

提示:

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

示例 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 * n 的乘法表,以及一个整数 k,要求我们找到乘法表中第 k 小的数字。

解题思路分析

首先观察题目数据范围:

  • 乘法表的 mn 数据范围在 30000 以内,乘法表个数最大可达 91089 * 10^8 个,而 k 就在这个数据范围内。 这个数据范围是不允许我们暴力遍历整个乘法表的,会超时(TLE)。
  1. 我们可以将问题转换为:给定一个数 x,求矩阵中有多少个数比 x,这样同样能够求得当前 x 是乘法表中的第几小的数;
  2. 通过与 k 进行比较来确定 x 是取大了还是取小了,由于 x 越大那么 x 在乘法表中的排位就越大,x 越小在乘法表中的排位就越小,因此我们可以 二分 x 找到第一个排位等于 kx 作为答案,二分的初始边界为乘法表的元素范围,即 [1, m * n]
  • 这里需要注意有些数字不在乘法表中,所以二分的 x 需要是第一个排位等于 k 的数,也就是排位等于 k 的最小 x,所以在二分的时候要使得满足排位等于 kx 尽可能小。
  • 那么问题又来了,为什么满足排位等于 k 的最小 x 一定在乘法表中呢?这是因为我们在计算排位的时候 count(x - 1) < kcount(x) == k,那么 count(x) - count(x-1) > 0 ,也就是等于 x 的个数不为 0,也就是说 x 在乘法表中。
  1. 问题转换到如何求 x 在乘法表中是第几小的数呢,我们可以遍历乘法表的每一行或者每一列,此时可以直接计算得知当前行或者列有多少个数小于 x,比如当前为第 i 行,所有数字都是 i 的倍数,因此不超过 x 的数字有 min(xi,n)\min(\Big\lfloor\dfrac{x}{i}\Big\rfloor,n) 个(这里的 n 也可以是 m,具体看遍历的是行还是列)。

注意一个优化,对于每个乘法表来说,我们每次选择 nm 中最小的那一个进行遍历,可以使得时间复杂度更优一点。

具体实现

  1. 选择 nm 中较小的一个作为二分时遍历的行或者列;
  2. 初始化二分的区间范围 [l, r]
  3. 二分查找排位等于 k 的最小 x
  4. 遍历乘法表中的行或者列,统计乘法表中小于等于 mid 的个数,那么 mid 在表格中就是第 cnt 小的数字;

需要注意的是:我们不能在 count = k 时直接返回结果,而应该继续进行二分查找,最终返回能满足 count = k 的 x 最小值。

复杂度分析

  • 时间复杂度:O(mlogmn)O(m \log mn)。二分的次数为 O(logmn)O(\log mn),每次二分需要 O(m)O(m) 的时间计算。
  • 空间复杂度:O(1)O(1)。仅需要常数空间存储。

代码实现

class Solution {
public:
    int findKthNumber(int m, int n, int k) {
        //优化二分中遍历的m为较小一个
        if(m > n) swap(n, m);
        //答案在[1, m * n]中
        int l = 0, r = m * n + 1;
        while(l + 1 != r){
            int mid = (l + r) >> 1;
            //统计表格中小于等于mid的个数,那么mid在表格中就是第cnt小的数字
            int cnt = (mid / n) * n;
            for(int i = (mid / n) + 1; i <= m; i++){
                cnt += mid / i;
            }
            //判断cnt与k的大小关系从而二分查找第一个大于等于k的cnt所对应的mid
            if(cnt < k){
                l = mid;
            }
            else r = mid;
        }
        return r;
    }
};

总结

  • 套路总结: 当元素过多,无法遍历求解二维矩阵里第 k 小的数时,都可以用二分猜答案的 套路 进行解题,将题目转化为 “给定一个数,求矩阵中有多少个数比这个数小”,进而实现二分查找。
  • 该题需要注意很多细节和优化,二分返回满足 count = k 的 x 最小值,选取 mn 中较小的作为遍历对象。
  • 测试结果: 微信截图_20220607181219.png

结束语

对于一艘没有目标的帆船来说,任何方向的风都可能时逆风。人生也是如此,浑浑噩噩没有目标,只会让时光蹉跎。想让现状有所改变,就要明确前进的方向。当你有了想要追逐的东西,旅途就会变得充实而又希望。