文章目录
- 263. 丑数
- 思路
- 代码
- 264. 丑数 II
- 方法一:最小堆
- 思路
- 代码
- 方法二:动态规划(三指针法)
- 思路
- 代码
- 1201. 丑数 III
- 方法:二分查找 + 容斥原理
- 思路
- 代码
- 313. 超级丑数
- 方法:“多路归并”
- 思路
- 代码
- 总结
- 参考资料
263. 丑数
思路
- 首先,丑数必须是正整数,因此对于
n<1
都可以直接返回 false; - 对于
n >= 1
,如果 n 能够被 2/3/5 整除,说明它们是丑数。
代码
class Solution {
public:bool isUgly(int n) {// ugly只能是正整数if(n < 1) return false;vector<int> factors = {2, 3, 5};for(int i=0; i<=2; ++i){while(n % factors[i] == 0){n /= factors[i];}}return n == 1;}
};
264. 丑数 II
方法一:最小堆
思路
- 要得到第 n 个丑数,可以使用最小堆实现。
- 初始化堆为空,首先将最小的丑数 1 加入。每次取出堆顶元素 x ,则 x 是堆中最小的丑数,2x、3x、5x 必然也是丑数,因此将它们也加入最小堆。
- 但是上述做法会出现重复元素,为了避免这种情况,用哈希集合去重,避免相同元素多次加入堆。
- 在排除重复元素的情况下,第 n 次从最小堆中取出的元素即为第 n 个丑数。
代码
class Solution {
public:int nthUglyNumber(int n) {vector<int> factor = {2, 3, 5};priority_queue<long, vector<long>, greater<long>> heap;unordered_set<long> s;// 先把丑数 1 加入s.insert(1L);heap.push(1L);long cur;for(int i=0; i<n; ++i){cur = heap.top();heap.pop();for(int f : factor){// 依次计算 2x 3x 5xlong temp = cur * f;// s中没有temp 将其存入if(!s.count(temp)){s.insert(temp);heap.push(temp);}}}return (int)cur;}
};
方法二:动态规划(三指针法)
思路
-
方法一使用最小堆,会预先存储较多的丑数,维护最小堆的过程也导致时间复杂度较高。可以使用动态规划的方法进行优化。
-
定义数组 dp,其中 dp[i] 表示第 i 个丑数,则 dp[n] 为这道题的答案。其中,dp[1] = 1。
-
剩余的丑数我们可以通过三个指针 p2 、p3、p5 计算得到。pi 的含义是有资格同 i 相乘的最小丑数的位置。这里资格指的是:如果一个丑数nums[pi]通过乘以 i 可以得到下一个丑数,那么这个丑数nums[pi]就永远失去了同 i 相乘的资格,我们把pi++,让 nums[pi] 指向下一个丑数即可。
-
举例说明:
一开始,丑数只有{1},1 可以同 2,3,5相乘,取最小的 1×2=2 添加到丑数序列中。
现在丑数中有{1,2},在上一步中,1 已经同 2 相乘过了,所以今后没必要再比较 1×2 了,因此认为 1 失去了同 2 相乘的资格。
现在 1 有与 3,5 相乘的资格,2 有与 2,3,5 相乘的资格,但是 2×3 和 2×5 肯定比 1×3 和 1×5 大,因此没必要比较。所以我们只需要比较 1×3,1×5,2×2。
依此类推,每次我们都分别比较有资格同 2,3,5 相乘的最小丑数,选择最小的那个作为下一个丑数,假设选择到的这个丑数是同 i(i=2,3,5)相乘得到的,所以它失去了同 i 相乘的资格,把对应的pi++,让 pi 指向下一个丑数即可。
代码
class Solution {
public:int nthUglyNumber(int n) {vector<int> dp(n+1);dp[1] = 1;int p2 = 1, p3 = 1, p5 = 1;for(int i=2; i<=n; ++i){int num2 = 2 * dp[p2];int num3 = 3 * dp[p3];int num5 = 5 * dp[p5];dp[i] = min(min(num2, num3), num5);// 确定dp[i]是由哪个数字生成的if(dp[i] == num2) p2++;if(dp[i] == num3) p3++;if(dp[i] == num5) p5++;}return dp[n];}
};
1201. 丑数 III
方法:二分查找 + 容斥原理
思路
-
首先要注意的是,这道题和前两题对于“丑数”的定义并不相同。这里不能直接枚举丑数 x(三指针法),因为 x 太大了,会导致超时。
-
这道题是 878. 第 N 个神奇数字 的升级版。把两个数改为了三个数,难度更高。与 878 题相比,这题的只需要修改求小于等于 x 的丑数个数的函数,二分查找部分一模一样。建议先复习 878 题,下面附上该题的思路图。
-
把小于等于 x 的 a 的倍数、b 的倍数、c 的倍数组成的集合分别叫做 A、B、C ,小于等于 x 的丑数相当于集合 A∪B∪C ,集合的元素个数为 ∣A∪B∪C∣,由 容斥原理可得:
∣A∪B∪C∣=∣A∣+∣B∣+∣C∣−∣A∩B∣−∣A∩C∣−∣B∩C∣+∣A∩B∩C∣
-
因此,小于等于 x 的丑数的个数为:
x/a + x/b + x/c - x/lcm_a_b - x/lcm_b_c - x/lcm_a_c + x/lcm_a_b_c
。可以使用最小公倍数函数std::lcm
。 -
接下来通过二分搜索,不断缩小边界,直到某个位置所对应的数恰好包含了 n 个丑数因子为止。
代码
class Solution {
public:using ll = long long;ll nthUglyNumber(ll n, ll a, ll b, ll c) {ll lcm_a_b = std::lcm(a, b), lcm_a_c = std::lcm(a, c), lcm_b_c = std::lcm(b, c);ll lcm_a_b_c= std::lcm(lcm_a_b, c);// 最小的丑数必然是a、b、c的最小值ll left = min(min(a, b), c);ll right = n * left;while(left + 1 < right){ll mid = (left + right) / 2;if(mid/a + mid/b + mid/c - mid/lcm_a_b - mid/lcm_a_c - mid/lcm_b_c + mid/lcm_a_b_c < n){left = mid;}else right = mid;}return right;}
};
313. 超级丑数
方法:“多路归并”
思路
-
这道题其实是
264. 丑数 II
的进阶,前者固定使用三个指针,分别对应于 2、3、5,而这道的primes数组长度不固定,因此使用指针数组来对应 primes 的每一个值。 -
第一个丑数一定是 1,而「往后产生的丑数」都是基于「已有丑数」而来(使用「已有丑数」乘上「给定质因数」primes[i] )。具体过程如图所示。
- 显然,我们需要每次取值最小的一个,然后让指针后移(指向下一个丑数),不断重复这个过程,直到找到第 n 个丑数。
- 另外,由于我们每个指针移动和 dp的构造,都是单调递增,因此可以通过与 dp[i-1] 进行比较来实现去重,而无须引用 Set 结构。
代码
class Solution {
public:long nthSuperUglyNumber(int n, vector<int>& primes) {int len = primes.size();// 指针数组vector<long> ptr(len, 1);vector<long> dp(n+1, 0);dp[1] = 1;for(int i=2;i<=n;++i){int flag = 0;dp[i] = primes[0] * dp[ptr[0]];for(int j=0; j<len; ++j){long cur = primes[j] * dp[ptr[j]];if(cur < dp[i]){flag = j;dp[i]= cur;} }ptr[flag]++;// 如果当前值和上一个丑数一样,那么跳过该丑数if(dp[i] == dp[i-1]) i--;}return dp[n];}
};
总结
- 以上是丑数的所有相关题目,丑数的定义并不固定,因此需要仔细理解题意后再开始计算。
- 对于
263. 丑数
,运用简单的数学思想即可解决; - 对于
264. 丑数II
,使用了最小堆和动态规划(即三指针的方法),但是最小堆的方法比较费时,更推荐方法二; - 对于
1201. 丑数III
,与一般的丑数定义不同,是 878. 第 N 个神奇数字 的升级版,使用了二分查找和容斥原理; - 对于
313.超级丑数
,是264. 丑数II
的升级版,将三指针方法扩展为多指针即可求解,也可以理解为多路归并法。
参考资料
- 丑数 II 官方题解
- 三指针方法的理解方式
- 313. 超级丑数:【宫水三叶】一题双解 :「优先队列」&「多路归并」