LC 313. 超级丑数

题目描述

这是 LeetCode 上的 313. 超级丑数 ,难度为 中等

超级丑数 是一个正整数,并满足其所有质因数都出现在质数数组 primes 中。

给你一个整数 n 和一个整数数组 primes ,返回第 n 个 超级丑数 。

题目数据保证第 n 个 超级丑数 在 32-bit 带符号整数范围内。

示例 1:

1
2
3
4
5
输入:n = 12, primes = [2,7,13,19]

输出:32

解释:给定长度为 4 的质数数组 primes = [2,7,13,19],前 12 个超级丑数序列为:[1,2,4,7,8,13,14,16,19,26,28,32] 。

示例 2:
1
2
3
4
5
输入:n = 1, primes = [2,3,5]

输出:1

解释:1 不含质因数,因此它的所有质因数都在质数数组 primes = [2,3,5] 中。

提示:

  • 1 <= n <= $10^6$
  • 1 <= primes.length <= 100
  • 2 <= primes[i] <= 1000
  • 题目数据 保证 primes[i] 是一个质数
  • primes 中的所有值都 互不相同 ,且按 递增顺序 排列

基本分析

类似的题目在之前的每日一题也出现过。

本题做法与 264. 丑数 II 类似,相关题解在 这里

回到本题,根据丑数的定义,我们有如下结论:

  • $1$ 是最小的丑数。
  • 对于任意一个丑数 $x$,其与任意给定的质因数 $primes[i]$ 相乘,结果仍为丑数。

优先队列(堆)

有了基本的分析思路,一个简单的解法是使用优先队列:

  1. 起始先将最小丑数 $1$ 放入队列
  2. 每次从队列取出最小值 $x$,然后将 $x$ 所对应的丑数 $x * primes[i]$ 进行入队。
  3. 对步骤 $2$ 循环多次,第 $n$ 次出队的值即是答案。

为了防止同一丑数多次进队,我们可以使用数据结构 $Set$ 来记录入过队列的丑数,但该做法常数较大,容易被卡。

利用题目规定的答案为 $int$ 范围,以及丑数性质,我们可以直接在入队的时候做控制。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public int nthSuperUglyNumber(int n, int[] primes) {
PriorityQueue<Integer> q = new PriorityQueue<>();
q.add(1);
while (n-- > 0) {
int x = q.poll();
if (n == 0) return x;
for (int k : primes) {
if (k <= Integer.MAX_VALUE / x) q.add(k * x);
if (x % k == 0) break;
}
}
return -1; // never
}
}

  • 时间复杂度:令 $primes$ 长度为 $m$,需要从优先队列(堆)中弹出 $n$ 个元素,每次弹出最多需要放入 $m$ 个元素,堆中最多有 $n m$ 个元素。复杂度为 $O(n m \log{(n * m)})$
  • 空间复杂度:$O(n * m)$

多路归并

从解法一中不难发现,我们「往后产生的丑数」都是基于「已有丑数」而来(使用「已有丑数」乘上「给定质因数」$primes[i]$)。

因此,如果我们所有丑数的有序序列为 $a1,a2,a3,…,an$ 的话,序列中的每一个数都必然能够被以下三个序列(中的至少一个)覆盖(这里假设 $primes = [2,3,5]$):

  • 由丑数 $2$ 所得的有序序列:$1 2$、$2 2$、$3 2$、$4 2$、$5 2$、$6 2$、$8 2$ …
  • 由丑数 $3$ 所得的有序序列:$1 3$、$2 3$、$3 3$、$4 3$、$5 3$、$6 3$、$8 3$ …
  • 由丑数 $5$ 所得的有序序列:$1 5$、$2 5$、$3 5$、$4 5$、$5 5$、$6 5$、$8 5$ …

我们令这些有序序列为 $arr$,最终的丑数序列为 $ans$。

如果 $primes$ 的长度为 $m$ 的话,我们可以使用 $m$ 个指针来指向这 $m$ 个有序序列 $arr$ 的当前下标。

显然,我们需要每次取 $m$ 个指针中值最小的一个,然后让指针后移(即将当前序列的下一个值放入堆中),不断重复这个过程,直到我们找到第 $n$ 个丑数。

当然,实现上,我们并不需要构造出这 $m$ 个有序序列。

我们可以构造一个存储三元组的小根堆,三元组信息为 $(val, i, idx)$:

  • $val$ :为当前列表指针指向具体值;
  • $i$ :代表这是由 $primes[i]$ 构造出来的有序序列;
  • $idx$:代表丑数下标,存在关系 $val = ans[idx] * primes[i]$。

起始时,我们将所有的 $(primes[i], i, 0)$ 加入优先队列(堆)中,每次从堆中取出最小元素,那么下一个该放入的元素为 $(ans[idx + 1] * primes[i], i, idx + 1)$。

另外,由于我们每个 $arr$ 的指针移动和 $ans$ 的构造,都是单调递增,因此我们可以通过与当前最后一位构造的 $ans[x]$ 进行比较来实现去重,而无须引用常数较大的 Set 结构。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public int nthSuperUglyNumber(int n, int[] primes) {
int m = primes.length;
PriorityQueue<int[]> q = new PriorityQueue<>((a,b)->a[0]-b[0]);
for (int i = 0; i < m; i++) {
q.add(new int[]{primes[i], i, 0});
}
int[] ans = new int[n];
ans[0] = 1;
for (int j = 1; j < n; ) {
int[] poll = q.poll();
int val = poll[0], i = poll[1], idx = poll[2];
if (val != ans[j - 1]) ans[j++] = val;
q.add(new int[]{ans[idx + 1] * primes[i], i, idx + 1});
}
return ans[n - 1];
}
}

  • 时间复杂度:需要构造长度为 $n$ 的答案,每次构造需要往堆中取出和放入元素,堆中有 $m$ 个元素,起始时,需要对 $primes$ 进行遍历,复杂度为 $O(m)$。整体复杂度为 $O(\max(m, n\log{m}))$
  • 空间复杂度:存储 $n$ 个答案,堆中有 $m$ 个元素,复杂度为 $O(n + m)$

最后

这是我们「刷穿 LeetCode」系列文章的第 No.313 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。