大家好,我是挨打的阿木木,爱好算法的前端摸鱼老。最近会频繁给大家分享我刷算法题过程中的思路和心得。如果你也是想提高逼格的摸鱼老,欢迎关注我,一起学习。
题目
313. 超级丑数
超级丑数 是一个正整数,并满足其所有质因数都出现在质数数组 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 <= 1061 <= primes.length <= 1002 <= primes[i] <= 1000- 题目数据 保证
primes[i]是一个质数 primes中的所有值都 互不相同 ,且按 递增顺序 排列
思路
这道题目是我们上一道题目丑数的进阶,如果还不知道怎么刷这种题型的题目的话可以先看我上一篇文章。[路飞]_一起刷leetcode 264. 丑数 II。
只不过这道题目是把上到题目的 [2, 3, 5] 改成了现在指定的数组primes,不过刷题的思路是一样的,那么我们就不能直接指定三根指针了,我们可以动态创建一个等于primes.length的数组来指定每个位置的指针所在位置。由于题目中已经说了: primes 中的所有值都 互不相同 ,且按 递增顺序 排列,那么我们还可以在代码中做一个优化,即遇到第一个指针为0的就不用往下执行了,因为递增序列中再往后也找不到比当前值更小的了。
实现
/**
* @param {number} n
* @param {number[]} primes
* @return {number}
*/
var nthSuperUglyNumber = function(n, primes) {
// 老规矩,第一个元素从1开始
let result = [ 1 ];
// 创建一个跟数组一样长度的指针数组, 一一对应
const len = primes.length;
let dp = new Array(len).fill(0);
while (result.length < n) {
// 拿最大值
let minValue = Number.MAX_VALUE;
let maxIndex = len;
// 遇到第一个为0的指针直接终止
for (let i = 0; i < len; i++) {
minValue = Math.min(minValue, primes[i] * result[dp[i]]);
if (dp[i] === 0) {
maxIndex = i;
break;
}
}
// 这里也一样,到位置直接结束
for (let i = 0; i <= maxIndex; i++) {
if (primes[i] * result[dp[i]] === minValue) {
dp[i]++;
}
}
result.push(minValue);
}
return result[n - 1];
};
结果
最后发现通用解法并不快,于是咱们继续做优化。还是那个问题,我们每一轮中的计算都得重复执行两遍,那么我们如何缓存每一个值的计算结果呢?这里可以用Map也可以用数组,为了方便这道题目我们就直接用数组吧,创建一个同样长度的数组,如果有计算值我们就进行缓存,如果没有的话就默认值用1填充。为什么用1不用0呢? 这就是加法和乘法基数的区别了。加法都是从0开始加,乘法都是从1开始乘。
优化代码
/**
* @param {number} n
* @param {number[]} primes
* @return {number}
*/
var nthSuperUglyNumber = function(n, primes) {
// 老规矩,第一个元素从1开始
let result = [ 1 ];
// 创建一个跟数组一样长度的指针数组, 一一对应
const len = primes.length;
let dp = new Array(len).fill(0);
// 创建一个存储每个节点的值的数组
let nums = [];
while (result.length < n) {
// 拿最大值
let minValue = Number.MAX_VALUE;
let maxIndex = len;
// 遇到第一个为0的指针直接终止
for (let i = 0; i < len; i++) {
nums[i] = nums[i] || primes[i] * result[dp[i]];
minValue = Math.min(minValue, nums[i]);
if (dp[i] === 0) {
maxIndex = i;
break;
}
}
// 这里也一样,到位置直接结束
for (let i = 0; i <= maxIndex; i++) {
if (nums[i] === minValue) {
dp[i]++;
// 值往后走一步
nums[i] = primes[i] * result[dp[i]]
}
}
result.push(minValue);
}
return result[n - 1];
};
优化到了这一步,发现效果还是不明显,于是我又做了一轮优化,一开始就把结果数组和记录每一步的值数组都创建好,然后直接进行赋值操作不重新去push元素了。而且把while循环改成for循环,这样子一来速度提升了一大半。
最终代码
/**
* @param {number} n
* @param {number[]} primes
* @return {number}
*/
var nthSuperUglyNumber = function(n, primes) {
const len = primes.length;
// 老规矩,第一个元素从1开始, 直接创建好整个数组
let result = new Array(n).fill(1);;
// 创建一个跟数组一样长度的指针数组, 一一对应
let dp = new Array(len).fill(0);
// 创建一个存储每个节点的值的数组
let nums = new Array(len).fill(0);
for (let j = 1; j < n; j++) {
// 拿最大值
let minValue = Number.MAX_VALUE;
// 遇到第一个为0的指针直接终止
for (let i = 0; i < len; i++) {
nums[i] = nums[i] || primes[i] * result[dp[i]];
minValue = Math.min(minValue, nums[i]);
}
result[j] = minValue;
// 这里也一样,到位置直接结束
for (let i = 0; i <= len; i++) {
if (nums[i] === minValue) {
dp[i]++;
// 值往后走一步
nums[i] = primes[i] * result[dp[i]]
}
}
}
return result[n - 1];
};
最终结果
看懂了的小伙伴可以点个关注、咱们下道题目见。如无意外以后文章都会以这种形式,有好的建议欢迎评论区留言。