题目介绍
力扣313题:leetcode-cn.com/problems/su…
方法一:优先队列(堆)
根据丑数的定义,我们有如下结论:
- 1 是最小的丑数。
- 对于任意一个丑数 x,其与任意给定的质因数 primes[i]相乘,结果仍为丑数。
有了基本的分析思路,一个简单的解法是使用优先队列:
- 起始先将最小丑数 1 放入队列
- 每次从队列取出最小值 x,然后将 x 所对应的丑数 x * primes[i] 进行入队。
- 对步骤 2 循环多次,第 n 次出队的值即是答案。
为了防止同一丑数多次进队,我们需要使用数据结构 Set 来记录入过队列的丑数。
代码如下:
class Solution {
public int nthSuperUglyNumber(int n, int[] primes) {
Set<Long> set = new HashSet<>();
PriorityQueue<Long> q = new PriorityQueue<>();
q.add(1L);
set.add(1L);
while (n-- > 0) {
long x = q.poll();
if (n == 0) return (int)x;
for (int k : primes) {
if (!set.contains(k * x)) {
set.add(k * x);
q.add(k * x);
}
}
}
return -1;
}
}
复杂度分析
- 时间复杂度:令 primes 长度为 m,需要从优先队列(堆)中弹出 n 个元素,每次弹出最多需要放入 m 个元素,堆中最多有 n * m 个元素。复杂度为 O(n∗mlog(n∗m))
- 空间复杂度:O(n * m)O(n∗m)
但是使用上述方法在力扣上提交会出现超时。
方法二:动态规划
状态定义:dp[i] 表示第 i + 1 个超级丑数,第 1 个超级丑数是 dp[0],第 2 个超级丑数是 dp[1],……
以「示例 1」为例,primes = [2, 7, 13, 19]。
-
第 1 个超级丑数是 dp[0] = 1;
-
第 2 个超级丑数是基于第 1 个超级丑数 dp[0]「乘以 2」或者「乘以 7」或者「乘以 13」或者「乘以 19」得到,选出最小者为 dp[0] 「乘以 2」,即 dp[1] = dp[0] * 2 = 2。那么
下一个丑数如果是因为「乘以 2」得到,一定不是基于 dp[0] 而是基于 dp[1],这一点很重要。
- 第 3 个超级丑数,比较这四个数:dp[1] * 2 = 4,dp[0] * 7 = 7、dp[0] * 13 = 13、dp[0] * 19 = 19,选出最小的是 dp[2] = dp[1] * 2 = 4,那么下一个丑数如果是因为「乘以 2」得到,一定是基于 dp[2]。
以此类推选下去,直到选出第 n 个丑数 dp[n - 1]。基于哪一个超级丑数,可以使用一个长度和 primes 相等的数组 indexes 记录下来,indexes[i] 表示下一个丑数如果选择了 primes[i] 是基于哪一个下标的超级丑数得到的。
选超级丑数的过程就相当于有 primes.length 这么多指针,在超级丑数列表上前进,一开始都在下标 0,然后 每一次可能选出多干个指针(注意这个细节,代码中有注释),让它们各前进一步,不回头。
代码如下:
class Solution {
public int nthUglyNumber(int n) {
int[] primes = new int[]{2,3,5};
int pLen = 3;
int[] indexes = new int[pLen];
int[] dp = new int[n];
dp[0] = 1;
for (int i = 1; i < n; i++) {
// 因为选最小值,先假设一个最大值
dp[i] = Integer.MAX_VALUE;
for (int j = 0; j < pLen; j++) {
dp[i] = Math.min(dp[i], dp[indexes[j]] * primes[j]);
}
// dp[i] 是之前的哪个丑数乘以对应的 primes[j] 选出来的,给它加 1
for (int j = 0; j < pLen; j++) {
if (dp[i] == dp[indexes[j]] * primes[j]) {
// 注意:这里不止执行一次,例如选出 14 的时候,2 和 7 对应的最小丑数下标都要加 1,大家可以打印 indexes 和 dp 的值加以验证
indexes[j]++;
}
}
}
return dp[n - 1];
}
}
解释:indexes 数组的含义:如果下一个超级丑数要选择 primes[j] 作为质因数,基于之前的哪一个丑数,选过之后就移到下一个丑数(不能移到下下一个,因为下下一个肯定更大)。可以打印数组 indexes 和 dp 看一下。
复杂度分析:
- 时间复杂度:O(nm),这里 n 就是题目中的 n,m 是质数数组的长度,外层循环次 n,内层循环遍历了 2 次质数数组, O(n×2×m)=O(nm);
- 空间复杂度:O(n+m)。