相关题目
- 263.丑数
- 264.丑数II
- 剑指 Offer 49. 丑数
(这道题的内容和上面的丑数II相同) - 313.超级丑数
- 1201.丑数III
解答
丑数
这道题是判断一个数字是否为丑数,也就是只由235构成的数。
想法也比较简单,我们只需要不断剥离这些因子,最终得到1就说明为丑数。
代码:
def isUgly(self, n: int) -> bool:
if n <= 0:
return False
while n%2 == 0:
n = n>>1
while n%3 == 0:
n /= 3
while n%5 == 0:
n /= 5
return n ==1
因为2、3、5这些数不相关,所以没有除的先后之分。
丑数II
给出第n个由2、3、5构成的丑数,这个题就开始不对劲了。
首先,这个题我们是没法遍历每一个数字,然后判断是否为丑数,计数器自增的,(从1开始,如果是丑数i++,直到为n),因为n最大1690,估计应该是int型的最大范围了,直接超时。
那么我们就只能构造丑数了,根据题目,丑数是由2、3、5构成的,那么首先[1, 2, 3, 5]这些一定是丑数(1是题目限定的),然后[4, 6, 10]和[6, 9, 15]这些衍生出来的丑数就也是了。
其实到这里,就有想法了,我们要自己构造丑数,这里给出两个方法,堆和dp。
堆:
由于新的丑数是根据之前的丑数来的,那么我们就将[1,2,3,5]这些初始丑数放入一个堆,每一次选出最小的元素,乘以[2,3,5]后得到新的一匹丑数,放入堆中,直到我们取出第n个根。
问题在于,我们难免构造出相同的丑数,这里的建议的使用哈希表。
class Solution:
def nthUglyNumber(self, n: int) -> int:
h = [1,2,3,5]
# 构造小顶堆
heapq.heapify(h)
num_set = set(h)
for _ in range(n):
min_num = heapq.heappop(h)
if min_num*2 not in num_set:
num_set.add(min_num*2)
heapq.heappush(h,min_num*2)
if min_num*3 not in num_set:
num_set.add(min_num*3)
heapq.heappush(h,min_num*3)
if min_num*5 not in num_set:
num_set.add(min_num*5)
heapq.heappush(h,min_num*5)
return min_num
有一说一,空间上确实会大很多。
dp:
这里的dp,也是采用构造丑数的方式,这个方法也可以叫做3指针。
在堆中,有一个问题就是,最开始形成的是[2,3,5]一组丑数,添加到堆中,然后又pop出2,添加了新的一匹丑数[4,6,10],
此时堆中剩下的是[3,4,5,6,10],然后拿出3,拿出4,然后拿出5。(宁搁着搁着呢?)
别,你看,我最开始就把5丢进去了,结果跑了好几次才轮到他,就一直放在堆里面能不占地方吗。
那我就不能等等把5放进去?因为新丑数都是根据之前的丑数来的,如果我发现当前5乘以丑数x还放不进去,下一次放入就还是5x,在x之后pop的数据根本不用看。
这样其实就不存在放入了,因为一共就三个数(2、3、5分别对应一个丑数),新的最小丑数一定在他们中间,直接选择即可。
所以我们可以这样,将所有pop出的数据放在一起呈一个数组,并对2、3、5分别给一个指针,指向数组中的一个下标,这样就能产生新的三个丑数,选择最小的元素,就是下一个丑数,同时在选择后将指针后移即可。
class Solution:
def nthUglyNumber(self, n: int) -> int:
ret = [0]*n
ret[0] = 1
p_two , p_three, p_five = 0, 0, 0
for i in range(1, n):
# 选择三个数中最小的作为新丑数
ret[i] = min(ret[p_two]<<1, (ret[p_three]<<1)+ret[p_three], (ret[p_five]<<2)+ret[p_five])
if ret[i] == ret[p_two]<<1:
p_two += 1
if ret[i] == (ret[p_three]<<1)+ret[p_three]:
p_three += 1
if ret[i] == (ret[p_five]<<2)+ret[p_five]:
p_five += 1
return ret[-1]
可能会注意到一个问题,那就是为什么我没有用if-else,而是并列是三个if,和上面的一样,难免产生相同的丑数,这时我们只需要取一个数,但是多个指针都需要移动,防止产生相同的数。
超级丑数
这个和丑数II基本上一样,所以不做太多的说明,也就是将3指针换成k指针。
class Solution:
def nthSuperUglyNumber(self, n: int, primes: List[int]) -> int:
ret = [0]*n
ret[0] = 1
ls = [0]*len(primes) # 索引
res = primes[:] # 乘积
for i in range(1, n):
ret[i] = min(res)
for j in range(len(primes)):
if res[j]==ret[i]:
ls[j] += 1
res[j] = ret[ls[j]]*primes[j]
# print(ret)
return ret[-1]
丑数III
这题,其实一看还是之前的思路,但是真要用之前的那个,不管是dp还是堆,你看第二个示例和他数据范围,直接当场去世。
这题,我们还要换一个思路:
前面的题,我们发现其实丑数就是用你给的丑数去乘得到的,那么对于一个数,我们能否判断他是第几个丑数呢?
能!
我们想一下,对于一个数x,他前面的数无非也就是由a、b、c相乘得到的,那么我们只要计算x前面有多少个含有a、b和c的数,加起来是不是就是前面有多少个元素了?
我们看一下数35,其中a=3,b=5,c=7:
a:3、6、9、12、15、18、21、24、27、30、33
b:5、10、15、20、25、30、35
c:7、14、21、28、35
很明显出现了重复,这里我们就需要舍弃这些重复的。我第一反应也是不会,但是请看这个:
集合论还不清楚?
然后我们再丢一个公式:x//a + x//b + x//c - x//lcm(a,b) - x//lcm(a,c) - x//lcm(b,c) + x//lcm(lcm(a,b),c)
(其中lcm是计算最小公倍数)
但对于一个数,我们根据当前的方式,第n个丑数其实是有很多个的,他们都能根据公式计算出前面有n-1个丑数,这时我们就要想明白,第n个丑数是这些丑数中最小的!。
现在我们就能知道一个数可能是第几个丑数了,另一个问题就是我们对于这么大的一个数集,其实能采用的也就是二分法了。
下限为min(a,b,c),上限本来应该是max(a,b,c)n,但n过大实际上是不可行的,我们使用的是给出的返回值最大值2000000009。
def nthUglyNumber(n, a, b, c):
def get_pos(x):
def lcm(x,y):
# 返回最小公倍数
m = max(x, y)
n = min(x, y)
while m%n:
m, n = n, m%n
return x*y//n
return x//a + x//b + x//c - x//lcm(a,b) - x//lcm(a,c) - x//lcm(b,c) + x//lcm(lcm(a,b),c)
l, r = min(a,b,c), 2000000009
while l < r:
mid = (l+r)>>1
#print(mid)
if get_pos(mid) < n:
l = mid+1
else:
r = mid
return l