【刷穿 LeetCode】求「第 n 个超级丑数」的两种方式 :「优先队列」&「多路归并」

简介: 【刷穿 LeetCode】求「第 n 个超级丑数」的两种方式 :「优先队列」&「多路归并」

网络异常,图片无法展示
|


题目描述



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


Tag : 「优先队列」、「多路归并」


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


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


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


示例 1:


输入: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:


输入:n = 1, primes = [2,3,5]
输出:1
解释:1 不含质因数,因此它的所有质因数都在质数数组 primes = [2,3,5] 中。
复制代码


提示:


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


基本分析



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


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


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


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


优先队列(堆)



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


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


为了防止同一丑数多次进队,我们需要使用数据结构 SetSet 来记录入过队列的丑数。


代码:


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; // never
    }
}
复制代码


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


多路归并



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


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


  • 由丑数 * 22 所得的有序序列:1 * 2122 * 2223 * 2324 * 2425 * 2526 * 2628 * 282 ...
  • 由丑数 * 33 所得的有序序列:1 * 3132 * 3233 * 3334 * 3435 * 3536 * 3638 * 383 ...
  • 由丑数 * 55 所得的有序序列:1 * 5152 * 5253 * 5354 * 5455 * 5556 * 5658 * 585 ...


我们令这些有序序列为 arrarr,最终的丑数序列为 ansans


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


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


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


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


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


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


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


代码:


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];
    }
}
复制代码


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


最后



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


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


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


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

相关文章
|
12月前
【Leetcode -263.丑数 -268.丢失的数字】
【Leetcode -263.丑数 -268.丢失的数字】
24 0
|
5月前
【Leetcode 2583】二叉树中的第K大层和 —— 优先队列 + BFS
解题思路: - 使用队列保存节点,按层序依次保存该层节点 - 使用优先队列保存每层节点值的总和,最后剔除前k个大数即可得到
|
5月前
[leetcode 优先队列] 2512. 奖励最顶尖的 K 名学生 M
[leetcode 优先队列] 2512. 奖励最顶尖的 K 名学生 M
|
11月前
leetcode每日一题 2021/4/10 263. 丑数
leetcode每日一题 2021/4/10 263. 丑数
44 0
LeetCode剑指 Offer 49. 丑数(dp/打表)
LeetCode剑指 Offer 49. 丑数(dp/打表)
力扣刷题记录——258. 各位相加、263.丑数、268.丢失的数字
力扣刷题记录——258. 各位相加、263.丑数、268.丢失的数字
力扣刷题记录——258. 各位相加、263.丑数、268.丢失的数字
|
C++
【力扣·每日一题】630. 课程表 III (C++ 贪心 优先队列)
【力扣·每日一题】630. 课程表 III (C++ 贪心 优先队列)
62 0
【力扣·每日一题】630. 课程表 III (C++ 贪心 优先队列)
【力扣·每日一题】1005. K 次取反后最大化的数组和 (贪心 优先队列)
【力扣·每日一题】1005. K 次取反后最大化的数组和 (贪心 优先队列)
45 0
【力扣·每日一题】1005. K 次取反后最大化的数组和 (贪心 优先队列)