313. 超级丑数

177 阅读3分钟

题目介绍

力扣313题:leetcode-cn.com/problems/su…

image.png

方法一:优先队列(堆)

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

  • 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)。