【LeetCode】1201. 丑数 III

官方链接

请你帮忙设计一个程序,用来找出第 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时)。

好吧,让我们思考所有可能的情况:

  1. 该数只能被a整除 (该数一定是a 的整数倍)
  2. 该数只能被b整除 (该数一定是b 的整数倍)
  3. 该数只能被c整除 (该数一定是c 的整数倍)
  4. 该数只能被a和b同时整除 (该数一定是a、b最小公倍数的整数倍)
  5. 该数只能被a和c同时整除 (该数一定是a、c最小公倍数的整数倍)
  6. 该数只能被b和c同时整除 (该数一定是b、c最小公倍数的整数倍)
  7. 该数只能被a和b和c同时整除(该数一定是a、b、c的最小公倍数的整数倍)

所以,我们只需要分别计算以上七项就能得到结果了!让我们分别来看(用MCM+下标表示最小公倍数):

  1. 情况1 = X/a - 情况4 - 情况5 - 情况7
  2. 情况2 = X/b - 情况4 - 情况6 - 情况7
  3. 情况3 = X/c - 情况5 - 情况6 - 情况7
  4. 情况4 = X/MCM_a_b - 情况7
  5. 情况5 = X/MCM_a_c - 情况7
  6. 情况6 = X/MCM_b_c - 情况7
  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

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值