Leetcode 1201. 丑数 III

1201. 丑数 III

给你四个整数:nabc ,请你设计一个算法来找出第 n 个丑数。

丑数是可以被 a b c 整除的 正整数

示例 1:

输入: n = 3, a = 2, b = 3, c = 5
输出: 4
解释: 丑数序列为 2, 3, 4, 5, 6, 8, 9, 10… 其中第 3 个是 4。

示例 2:

输入: n = 4, a = 2, b = 3, c = 4
输出: 6
解释: 丑数序列为 2, 3, 4, 6, 8, 9, 10, 12… 其中第 4 个是 6。

示例 3:

输入: n = 5, a = 2, b = 11, c = 13
输出: 10
解释: 丑数序列为 2, 4, 6, 8, 10, 11, 12, 13… 其中第 5 个是 10。

示例 4:

输入: n = 1000000000, a = 2, b = 217983653, c = 336916467
输出: 1999999984

提示:

  • 1 <= n, a, b, c <= 10^9
  • 1 <= a * b * c <= 10^18
  • 本题结果在 [1, 2 * 10^9] 的范围内

解答参考:简洁代码 容斥原理 + 二分查找 - Tizzi

没有什么思路,一看是中等题,感觉普通的暴力应该过不了,毕竟提供的示例里面就已经有数据比较大的样例了,意思就是告诉你,这题用暴力方法连自测都过不去的。

class Solution {
public:
    int nthUglyNumber(int n, int a, int b, int c) {
        int ans = 1;
        int cnt = 0;
        while (cnt < n) {
            if (ans % a == 0 || ans % b == 0 || ans % c == 0) {
                cnt++;
            }
            ans++;
        }
        return ans-1;
    }
};

容斥原理 + 二分查找

首先,在 [ 1 ,   i ] [1,\ i] [1, i] 中能被数 a a a 整除的数的个数是 ⌊ i a ⌋ \lfloor\frac{i}{a}\rfloor ai ,这是显然的,因为在 [ 1 ,   i ] [1,\ i] [1, i] 中能包含的被 a a a 整除的数是 1 a ,   2 a ,   3 a ,   … 1a,\ 2a,\ 3a,\ \dots 1a, 2a, 3a,  ,那么最大能去到多少整数倍的 a a a 呢,显然就是 ⌊ i a ⌋ \lfloor\frac{i}{a}\rfloor ai 了 。

其次我们需要重新复习一下容斥原理,其实这个东西在高中或者初中的教材中,学习集合的概念的时候应该是见过的,不过后来不是什么考点所以没怎么用过,我也忘得差不多了。

假设一个班级中,喜欢语文的同学集合为 A A A ,喜欢数学的同学集合为 B B B ,喜欢英语的同学集合为 C C C ,那么班级中至少喜欢一门课程的同学人数为多少?是不是就是简单的直接将三者集合的势(集合的大小)相加呢?当然不是了,因为其中有些同学可能既喜欢语文又喜欢数学,甚至可能一部分同学三门课程都喜欢,所以我们必须减去这一部分重复的计数。假设最极端的情况是三个集合两两互有交集,如图所示:
在这里插入图片描述

则有:
∣ A ∪ B ∪ C ∣ = ∣ A ∣ + ∣ B ∣ + ∣ C ∣ − ∣ A ∩ B ∣ − ∣ B ∩ C ∣ − ∣ C ∩ A ∣ + ∣ A ∩ B ∩ C ∣ |A\cup B\cup C| = |A|+|B|+|C|-|A\cap B|-|B\cap C|-|C\cap A|+|A\cap B\cap C| ABC=A+B+CABBCCA+ABC
最后这个加上三者的交集,是因为前面减去三者两两之间的交集的时候,中间这一块也被减去所有重复次数了,变为了空,那么从可视化集合关系的角度来看就是中间空了一块,所以我们需要补上去。容斥原理的公式是通用的,不仅限于三个集合,可以是两个或者多个,比如当情况退化为两个集合的容斥时,就是:
∣ A ∪ B ∣ = ∣ A ∣ + ∣ B ∣ − ∣ A ∩ B ∣ |A\cup B| = |A|+|B|-|A\cap B| AB=A+BAB
多个集合的容斥时就比较复杂了,比如四个集合的时候,基本思路虽然差不多,不过需要额外补全的重复减去的小片的交集就比较多。

对于本题来说,能被 a ,   b ,   c a,\ b,\ c a, b, c 其中之一整除的数是不是就是简单的做统计加法而已呢?
⌊ i a ⌋ + ⌊ i b ⌋ + ⌊ i c ⌋ \lfloor\frac{i}{a}\rfloor + \lfloor\frac{i}{b}\rfloor + \lfloor\frac{i}{c}\rfloor ai+bi+ci
很明显当然不是,有一些数能同时被多个数整除的,这是重叠计数的部分。根据前文的容斥原理的公式,我们可以计算得到三者的并集,这里需要用到 最大公约数 和 最小公倍数 来表示同时被两个数整除的最小约束,为什么呢?比如能同时被 3 或者 6 整除的最小数并不是 3 × 6 = 18 3\times 6 = 18 3×6=18 而是 6 6 6 才对,因此一个数要同时能被 a a a b b b 整除则等价于被 a a a b b b 的最小公倍数整除,求最小公倍数的算法是建立在最大公约数的基础之上的;最大公约数(Greatest Common Divider)和 最小公倍数(Least Common Multiple):
A = ⌊ i a ⌋ B = ⌊ i b ⌋ C = ⌊ i c ⌋ A ∩ B = ⌊ i l c m ( a ,   b ) ⌋ B ∩ C = ⌊ i l c m ( b ,   c ) ⌋ C ∩ A = ⌊ i l c m ( c ,   a ) ⌋ A ∩ B ∩ C = ⌊ i l c m ( a ,   b ,   c ) ⌋ \begin{aligned} A &= \lfloor\frac{i}{a}\rfloor \\ B &= \lfloor\frac{i}{b}\rfloor \\ C &= \lfloor\frac{i}{c}\rfloor \\ A\cap B &= \lfloor\frac{i}{lcm(a,\ b)}\rfloor \\ B\cap C &= \lfloor\frac{i}{lcm(b,\ c)}\rfloor \\ C\cap A &= \lfloor\frac{i}{lcm(c,\ a)}\rfloor \\ A\cap B\cap C &= \lfloor\frac{i}{lcm(a,\ b,\ c)}\rfloor \\ \end{aligned} ABCABBCCAABC=ai=bi=ci=lcm(a, b)i=lcm(b, c)i=lcm(c, a)i=lcm(a, b, c)i

根据题目的数据范围来看, n n n 的取值可以去到非常大的 1 0 9 10^9 109 ,那么如果是暴力求解的话,肯定会超时的。因为我们现在知道如何计算 [ 1 ,   i ] [1,\ i] [1, i] 中能被 a ,   b ,   c a,\ b,\ c a, b, c 三者之一整除的数的个数了,所以我们可以使用二分查找的方式来确定这个临界点到底在哪里。对于临界点 x x x 来说, [ 1 ,   x − 1 ] [1,\ x-1] [1, x1] 范围内的数都是不符合题意能满足有 n n n 个数被 a ,   b ,   c a,\ b,\ c a, b, c 三者之一整除的,而在 [ x + 1 ,   + ∞ ) [x+1,\ +\infty) [x+1, +) 范围内的数,都是至少 n n n 个数能被 a ,   b ,   c a,\ b,\ c a, b, c 整除的,所以我们可以依据这一点来完成二分。

既然说到了二分,那么一定注意二分的停止条件和范围缩小的策略,因为临界点自身是符合题目的答案(我在代码中的 check 函数判断用的是 ≥ \geq ),所以右边界往左边缩小的时候应该是 r = m r=m r=m 而不是 r = m − 1 r=m-1 r=m1 ,否则有可能当 r r r 刚好就是 m m m 的时候下一次更新 m m m 把正确的临界点给滑到窗口之外去了……

class Solution {
private:
    long gcd(long x, long y) {
        if (x % y == 0) return y;
        if (y % x == 0) return x;
        long m;
        while (x % y) {
            m = x % y;
            x = y;
            y = m;
        }
        return m;
    }
    long lcm(long x, long y) {
        return x / gcd(x, y) * y;
    }
public:
    int nthUglyNumber(int n, int a, int b, int c) {
        function<bool(const int&)> check = [&](const long& x) {
            long ab = lcm(a, b);
            long bc = lcm(b, c);
            long ca = lcm(c, a);
            long abc = lcm(lcm(a, b), c);
            long cnt = x/a + x/b + x/c - x/ab - x/bc - x/ca + x/abc;
            return cnt >= n;
        };
        long l = 1, r = INT_MAX, m;
        while (l < r) {
            m = l + ((r - l) >> 1);
            if (check(m)) r = m;
            else l = m + 1;
        }
        return r;
    }
};
  • 7
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值