请你帮忙设计一个程序,用来找出第 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, 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] 的范围内
方案:二分法思路剖析
基础思路
首先,为什么第一时间能想到二分法?
让我们观察题目,可以看到,最终状态(即n)的范围非常大。试图自底向上递推或是按照通常的自顶向下回溯显然会超时(比如动态规划、DFS等方法)。面对这么大的状态空间,二分法的时间复杂度是logN,因此能够大大压缩需要遍历的状态数目。
思路剖析
既然已经确定了二分法作为切入点,关键问题来了,如何二分呢?
按照题意,所谓丑数是可以至少被a、b、c三者中的一者整除的,那么对于一个丑数X,我们能够确定它是第几个丑数吗?
答案显然是可以的,我们只需要计算X中包含了多少个丑数因子即可。
即只需要知道在 [0,X] 范围内,还有多少个丑数即可,而这些丑数,无非就是一些能被a或者b或者c所整除的数。
那么显然,我们直接用X/a、X/b、X/c就能计算出[0,X]范围内有多少数能被a或者b或者c整除,然后把它们加起来就是答案!
但是仔细思考一下,我们是不是重复计算了些什么?如果一个数既能被a整除,又能被b整除,那么实际上该数在先前的计算中就被重复计算了一次(分别是在计算X/a和X/b时)。
好吧,让我们思考所有可能的情况:
- 该数只能被a整除 (该数一定是a 的整数倍)
- 该数只能被b整除 (该数一定是b 的整数倍)
- 该数只能被c整除 (该数一定是c 的整数倍)
- 该数只能被a和b同时整除 (该数一定是a、b最小公倍数的整数倍)
- 该数只能被a和c同时整除 (该数一定是a、c最小公倍数的整数倍)
- 该数只能被b和c同时整除 (该数一定是b、c最小公倍数的整数倍)
- 该数只能被a和b和c同时整除(该数一定是a、b、c的最小公倍数的整数倍)
所以,我们只需要分别计算以上七项就能得到结果了!让我们分别来看(用MCM+下标表示最小公倍数):
- 情况1 = X/a - 情况4 - 情况5 - 情况7
- 情况2 = X/b - 情况4 - 情况6 - 情况7
- 情况3 = X/c - 情况5 - 情况6 - 情况7
- 情况4 = X/MCM_a_b - 情况7
- 情况5 = X/MCM_a_c - 情况7
- 情况6 = X/MCM_b_c - 情况7
- 情况7 = X/MCM_a_b_c
让我们整理上述方程后也就得到:
sum(情况) = X/a + X/b + X/c - X/MCM_a_b - X/MCM_a_c - X/MCM_b_c + X/MCM_a_b_c
好了,现在也就得到了计算X中包含多少个丑数因子的方法了!
至于计算最小公倍数的方法,这里不多介绍,概括而言就是对于两个数a和b,它们的最小公倍数 = a*b/(a和b的最大公约数),最大公约数可以通过辗转相除法得到。
二分搜索
在得到了计算任意数中包含了多少个丑数因子的方法后,我们实际上只需要通过二分法,不断缩小边界范围,直到某个位置所对应的数恰好包含了n个丑数因子为止。
注意,通过二分法计算的答案并非是最终答案,因为可以有很多数同时包含有n个丑数因子!
比如第 n 个丑数是 X,那么 [X,X + min(a,b,c)) 这个半开区间内的所有数都同时包含 n 个丑数因子,我们通过二分法得到的答案也随机分布于这个区间中。而实际上我们只需要得到该区间的左端即可。处理方法很简单:假设我们得到的临时答案是 K(K∈[X,X + min(a,b,c))), 那么 K - min(K%a,K%b,K%c) = X. 也就是只需要把临时答案减去其与a、b、c三者中取余的最小值即可!
from math import gcd
class Solution:
def nthUglyNumber(self, n: int, a: int, b: int, c: int) -> int:
# 三数最小公倍数 = 两数最小公倍数与第三个数的最小公倍数
def Lcm3(x,y,z):
a = (x*y)//gcd(x,y)
return (a*z)//gcd(a,z)
'''
计算有多少个丑数小于等于x
= + x整除a,b,c - 整除ab,bc,ac最小公倍数 + 整除abc最小公倍数
'''
def uglynum(x):
return x//a + x//b + x//c \
- x//(a*b//gcd(a,b)) - x//(a*c//gcd(a,c)) - x//(b*c//gcd(b,c)) \
+ x//Lcm3(a,b,c)
'''
二分搜索,注意只要 uglynum(mid)<n, left就=mid+1 所以最后得到的left就是所求
例如测试用例2中 a=2,b=3,c=4
括号中为丑数 1,(2),(3),(4),5,(6),7,(8)
小于等于它们的丑数个数分别为 0, 1 , 2 , 3 ,3, 4, 4, 5
若n==4
如果uglynum(mid)<4 则left一定能直接取到6而不是7
'''
left=1
right=n*min(a,b,c)
while left<right:
mid=(left+right)//2
if uglynum(mid)<n:
left=mid+1
else:
right=mid
return left