算法练习之 878.第N个神奇的数字

题目原型

  神奇的数字:如果正整数X可以被A或B整除,那么它是神奇的。
  求:返回第N个神奇数字。由于答案可能非常大,返回它模10^9 + 7 的结果。
  示例
   输入N=1, A =2, B =3; 输出:2
   输入N=4, A =2, B =3; 输出:6
   输入N=5, A =2, B =4; 输出:10
   输入N=3, A =6, B =4; 输出:8
  提示:
    1 <= N <= 10^9
    2 <= A <= 40000
    2 <= B <= 40000

求解思路

  求解此题时,我们可以直接使用暴力破解,但是对于大数据类型的就比较头疼了。我们这里求解的思想是基于二分法的,在讲解方法之前,我们先列几条信息:

  1. 对于一个数A,和C,小于C并且可以整除A的数的个数N_a = C/A(这里默认使用了整型强转,也就是向下取整)
  2. 整数A和B,判断小于数字C并且同时可以整除A和B的数的个数N_ab = C/最小公倍数(A, B)
    其实根据上面两点我们就可以求解题目。但是为了更为直观,再补充一点:
  3. 小于数字C,并且可以整除A或B的数字的个数N_a|b = C/A + C/B - C/最小公倍数(A, B) = N_a + N_b - N_ab

  根据上面三点我们可以确定数字C是否是那个神奇的数字,但是如何找到这个C呢,我们可以根据二分法寻找。题目中给出了N和A、B的范围,我们可以在int64的范围内寻找。也就是left = 0,right= 1<<63 - 1(int64最大数值)。通过计算中位数mid = (left + right)/2,并计算出小于中位数mid,并且可以整除A或B有多少个数字:N_mid = mid/A + mid/B - mid/最小公倍数(A, B)。如果N_mid < N,则表明那个神奇的数字,在mid的右侧,将left = mid +1 重新进行循环;如果N_mid >= N,则表明神奇数字在mid的左侧,可以将right= mid,进行计算。计算直到 left > right 结束,最后输出的right就是最中结果。

代码

/*
 二分法
	1.如果一个数对于一个数C,判断小于C,并且可以整除A、B的数的个数,可以由C/A, C/B得到。
	2.对于一个数C,判断小于C的所有数,同时可以整除A、B的数的个数可以通过计算C/最小公倍数(A,B)来得到。
	3.根据上面两点,我们就可以计算出小于C下,同时可以整除A和B的数的个数:
		C/A + C/B - C/(最小公倍数(A,B))
这样计算,是因为C/A 和C/B中同时存在可以整除A和B的数,所以要减去一次
 */
func NthMagicalNumber(N int, A int, B int) int {
	left := 0
	right := math.MaxInt64
	//最小公倍数
	minMul := A * B / MaxDiv(A, B)
	for left < right {
		mid := (left + right) / 2
		temp := mid/A + mid/B - mid/minMul
		if temp < N {
			left = mid + 1
		} else {
			right = mid
		}
	}
	return right % 1000000007
}

//求最大公约数
func MaxDiv(a, b int) int {
	if a < b {
		tmp := a
		a = b
		b = tmp
	}
	for a%b != 0 {
		t := b
		b = a % b
		a = t
	}
	return b
}

改进

  二分法固然比暴力破解法好多了,但是二分法的范围是一个问题,因为这里的范围是0-1<<63 - 1(int64最大数值)。这范围内使用二分法也是比较消耗时间的。所以根据范围,我们可以做出改进。
  我们都知道两个数字A、B的最小公倍数minMul可以表示成A和B的一个周期。这个周期指的是长度为minMul- Min(A,B),并且每个周期之间相差Min(A, B) -1,并且每个周期可以整除A或B的数字的数量是一样的。
  我们可以利用这个周期,来将二分法的范围缩小。同样我们需要计算最小公倍数minMul,以及将A、B按着从小到大重新进行赋值(之后就默认A是小的那个数字)。为了计算那个神奇的数字在第几个周期内,我们需要得到每个周期内有多少个神奇数字:cycleDivNum = minMul/A + minMul/B -1。知道了每个周期内有多少个这样的神奇数字,就可以计算出第N个神奇数字在第几个周期:cycleN = N/cycleDivNum + (if N%cycleDivNum !=0 { 1 }else { 0 }(如果N%cycleDivNum取余有值,则加1)。这样我们就可以确定二分法left和right的范围了,就是这个周期的起末:left = (cycleN -1) * (minMul - A +1) + (cycleN -1)*( A -1 ) + A,
right = left + minMul - A。剩下的就是二分法了。不多说,上代码

代码

//利用周期将二分法的范围缩小
func TwoDivPro(N, A, B int) int {
	//最小公倍数
	minMul := A * B / MaxDiv(A, B)
	if A > B {
		t := A
		A = B
		B = t
	}
	//fmt.Println(minMul)
	//一个最小公倍数周期内,可以整除a或b的个数
	cycleDivNum := minMul/A + minMul/B - 1
	//确定 数字在第几个周期
	cycleN := N / cycleDivNum
	if N%cycleDivNum != 0 {
		cycleN++
	}
	//确定了周期,则可以找出周期首尾
	left := (cycleN-1)*(minMul-A+1) + (cycleN-1)*( A-1) + A
	right := left + minMul - A
	//然后可以使用二分法进行查找
	for left < right {
		mid := (left + right) / 2
		temp := mid/A + mid/B - mid/minMul
		if temp < N {
			left = mid + 1
		} else {
			right = mid
		}
	}
	//fmt.Println(left, "  ", right)
	return right
}

//求最大公约数
func MaxDiv(a, b int) int {
	if a < b {
		tmp := a
		a = b
		b = tmp
	}
	for a%b != 0 {
		t := b
		b = a % b
		a = t
	}
	return b
}

性能测试

   为了测试改进前和改进后的算法,我们可以执行这个测试用例:
       N = 10000, A = 23423, B = 34324
改进前:
在这里插入图片描述
改进后:
在这里插入图片描述
  我们可以看到,相同时间内,改进后可以执行双倍的操作。也就是每次改进后执行的时间比改进前的缩短了一倍。
  由于测试用例较少,提交到力扣(leetCode)上执行结果如下:
地方额在这里插入图片描述  其中最后一个是改进前的,第一个是改进后的。都是0ms,是因为力扣只能精确到毫秒。

总结

  算法还算成功。哈哈哈哈。。。。。。。。。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值