大家好,我是 @负雪明烛。点击右上方的「+关注,优质题解不间断!

题目大意

小时候我们都背过“九九乘法表”对吧?今天这个题目给出的乘法表不是 668. 乘法表中第k小的数 - 图1的,而是 668. 乘法表中第k小的数 - 图2的。

要我们求这个乘法表中第 668. 乘法表中第k小的数 - 图3 小的数字。

下面是一个 668. 乘法表中第k小的数 - 图4的乘法表:

解题方法

分析

要求“第 668. 乘法表中第k小的数 - 图5 小的数字 668. 乘法表中第k小的数 - 图6” 。

如果给出的是一维数组,那么这个问题就是 668. 乘法表中第k小的数 - 图7经典问题,参考题目 215. 数组中的第K个最大元素

如果给出的是一个行列都递增的二维矩阵,那么也是个经典问题,可以从左下角或者右下角开始搜索,参考题目
240. 搜索二维矩阵 II

其实乘法表也是一个行列都递增的二维矩阵,但是由于 668. 乘法表中第k小的数 - 图8的范围很大(668. 乘法表中第k小的数 - 图9),单单把乘法表构造出来 668. 乘法表中第k小的数 - 图10的时间复杂度会超时。

因此我们必须要用更优的算法。

二分查找

二分查找说白了就是「尝试」,只不过是更加聪明的尝试。

首先,给了一个 668. 乘法表中第k小的数 - 图11乘法表的二维矩阵,我们其实能很快地找到矩阵中有多少个数字小于等于 668. 乘法表中第k小的数 - 图12

先看下图:

由于第 668. 乘法表中第k小的数 - 图13行的数字分别是 668. 乘法表中第k小的数 - 图14,因此这一行小于等于 668. 乘法表中第k小的数 - 图15的数字个数为 668. 乘法表中第k小的数 - 图16

那么矩阵中小于等于668. 乘法表中第k小的数 - 图17的数字个数 668. 乘法表中第k小的数 - 图18 各行的累加,即 668. 乘法表中第k小的数 - 图19 个。

注意求和公式伴随着 668. 乘法表中第k小的数 - 图20的增加,也是单调递增的。

很好理解,乘法表中小于等于 668. 乘法表中第k小的数 - 图21的数字个数,肯定随着 668. 乘法表中第k小的数 - 图22的增加而增加。

这题希望得到出什么呢?

希望得到668. 乘法表中第k小的数 - 图23 时的 668. 乘法表中第k小的数 - 图24值。

含义就是得到乘法表中小于等于 668. 乘法表中第k小的数 - 图25的数字个数恰好为 668. 乘法表中第k小的数 - 图26时, 668. 乘法表中第k小的数 - 图27的值。

这个 668. 乘法表中第k小的数 - 图28怎么求出来呢?就是用「二分法」去搜索尝试。

大家应该都玩过「猜数字」的游戏,玩家报一个数字,裁判告诉你猜的这个数字是「大了」还是「小了」。然后玩家就可以缩小下次猜数字的区间。这就是二分法的实际运用。

这个题也是一样,我们尝试某一个 668. 乘法表中第k小的数 - 图29,相当于问:“乘法表中小于等于 668. 乘法表中第k小的数 - 图30的数字个数是 668. 乘法表中第k小的数 - 图31吗?”

根据 668. 乘法表中第k小的数 - 图32的值,我们可以知道 668. 乘法表中第k小的数 - 图33是「大了」还是「小了」;然后就知道下一步是缩小 668. 乘法表中第k小的数 - 图34还是放大 668. 乘法表中第k小的数 - 图35

我们这题在区间 668. 乘法表中第k小的数 - 图36上进行二分搜索,找到符合条件的最小数字。

需要注意的是,本题中的搜索空间中可能有多个符合条件的数字。因此我们需要搜索到符合条件的并且最小的数字。

为什么呢?

因为 668. 乘法表中第k小的数 - 图37区间的数字并不一定都在乘法表中存在,所以我们可能猜测了一些无效的且符合要求的数字。我们找到符合要求的且最小的数字一定在乘法表中。

比如下面的例子,我们查找小于等于 $5$ 的数字有 $6$ 个,和小于等于 $4$ 的结果个数相等。但是数字 $5$ 并不在乘法表中,因此我们必须要找到符合要求并且最小的数字,即 $4$。

用动图演示一下这个过程:

代码

  1. class Solution {
  2. public:
  3. int findKthNumber(int m, int n, int k) {
  4. int left = 1;
  5. int right = m * n;
  6. // [left, right]
  7. while (left < right) {
  8. int mid = left + (right - left) / 2;
  9. if (count(m, n, mid) >= k) {
  10. right = mid;
  11. } else {
  12. left = mid + 1;
  13. }
  14. }
  15. return left;
  16. }
  17. // 统计乘法表中有多少个小于等于 k 的数目
  18. int count(int m, int n, int k) {
  19. int res = 0;
  20. // 统计每行小于等于 k 的数目
  21. for (int i = 1; i <= m; ++i) {
  22. res += min(k / i, n);
  23. }
  24. return res;
  25. }
  26. };
class Solution {
    public int findKthNumber(int m, int n, int k) {
        int left = 1;
        int right = m * n;
        // [left, right]
        while (left < right) {
            int mid = left + (right - left) / 2;
            if (count(m, n, mid) >= k) {
                right = mid;
            } else {
                left = mid + 1;
            }
        }
        return left;
    }

    // 统计乘法表中有多少个小于等于 k 的数目
    int count(int m, int n, int k) {
        int res = 0;
        // 统计每行小于等于 k 的数目
        for (int i = 1; i <= m; ++i) {
            res += Math.min(k / i, n);
        }
        return res;
    }
}

复杂度

  • 时间复杂度:668. 乘法表中第k小的数 - 图38
  • 空间复杂度:$O(1)$。

    总结

  1. 本题是二分查找的应用,可以多看看我的题解理解一下这个过程。



我是 @负雪明烛 ,刷算法题 1000 多道,写了 1000 多篇算法题解,收获阅读量 300 万。
关注我,你将不会错过我的精彩动画题解、面试题分享、组队刷题活动,进入主页 @负雪明烛 右侧有刷题组织,从此刷题不再孤单。