给你四个整数:n
、a
、b
、c
,请你设计一个算法来找出第 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]
的范围内
没有什么思路,一看是中等题,感觉普通的暴力应该过不了,毕竟提供的示例里面就已经有数据比较大的样例了,意思就是告诉你,这题用暴力方法连自测都过不去的。
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|
∣A∪B∪C∣=∣A∣+∣B∣+∣C∣−∣A∩B∣−∣B∩C∣−∣C∩A∣+∣A∩B∩C∣
最后这个加上三者的交集,是因为前面减去三者两两之间的交集的时候,中间这一块也被减去所有重复次数了,变为了空,那么从可视化集合关系的角度来看就是中间空了一块,所以我们需要补上去。容斥原理的公式是通用的,不仅限于三个集合,可以是两个或者多个,比如当情况退化为两个集合的容斥时,就是:
∣
A
∪
B
∣
=
∣
A
∣
+
∣
B
∣
−
∣
A
∩
B
∣
|A\cup B| = |A|+|B|-|A\cap B|
∣A∪B∣=∣A∣+∣B∣−∣A∩B∣
多个集合的容斥时就比较复杂了,比如四个集合的时候,基本思路虽然差不多,不过需要额外补全的重复减去的小片的交集就比较多。
对于本题来说,能被
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}
ABCA∩BB∩CC∩AA∩B∩C=⌊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, x−1] 范围内的数都是不符合题意能满足有 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=m−1 ,否则有可能当 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;
}
};