[路飞]_算法_丑数 II ——暴力解法,动态规划

1,037 阅读2分钟

题目描述

给你一个整数 n ,请你找出并返回第 n 个 丑数 。

丑数 就是只包含质因数 2、3 和/或 5 的正整数。

示例 1:

输入:n = 10
输出:12
解释:[1, 2, 3, 4, 5, 6, 8, 9, 10, 12] 是由前 10 个丑数组成的序列。

示例 2:

输入:n = 1
输出:1
解释:1 通常被视为丑数。

提示:

  • 1 <= n <= 1690

来源:力扣(LeetCode) 链接:leetcode-cn.com/problems/ug… 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

解题思路

方法一:暴力解锁

  1. 从1递增,判断每个数是不是丑数,每找到一个丑数num+=1,num为找到丑数的数量
  2. 每个数字判断是否是丑数的方法为,除2除到除不尽,再除3除到除不尽,再除5除到除不尽,最后剩下的数为1就是丑数
  3. 当找到第n个丑数就返回
代码

/**
 * @param {number} n
 * @return {number}
 */
 var nthUglyNumber = function(n) {
    if(n===1) return 1;

   let num=1;//找到丑数的数量
     let j=2;
     while(num){
        if(isUgly(j)) num+=1;
         if(num===n) {
             num=null;
             return j;//在找到第n个丑数时候,返回
         }
         j++;
     } 
 };
/**
判断一个数是否是丑数 */
 function isUgly(num){
    //首先除2,直到不能整除为止,然后除5到不能整除为止,然后除3直到不能整除为止。最终判断剩余的数字是否为1,如果是1则为丑数,否则不是丑数。\
     while(num%2===0){
        num=num/2
     }
     while(num%3===0){
         num=num/3
     }
     while(num%5===0){
         num=num/5
     }
     return num===1;
 }

方法二:动态规划

以上方法要判断所有的数是不是丑数,需要及大量的运算,时间复杂度非常高,接下来用动态规划的方式解题。性能还不错 image.png

  • 第一个丑数是1,丑数的特点是能被2,3或者5整除,换句话说,丑数一定是2,3或者5的倍数。
  • 所以,除了方法一遍历所有数字去判断是否是丑数以外,还能从1开始,通过乘以2,3,5来产生。
  • 比如1是丑数,由1衍生出三个丑数,2=12,3=13,5=1*5,第二个丑数就是这三个中最小的。 按照以上思路我们来尝试下,得到1之后的丑数
思路:
  1. 定义数组dp,其中dp[i] 表示第 i 个丑数,第 n 个丑数即为dp[n]。

  2. 由于最小的丑数是 1,因此 dp[1]=1。

  3. 如何得到其余的丑数呢?定义三个指针 p2,p3,p5,表示下一个丑数是当前指针指向的丑数乘以对应的质因数。初始时,三个指针的值都是 1。

  4. 当 2<= i <= n 时,dp[i]就是 min(dp[p2​]×2,dp[p3​]×3,dp[p5​]×5),然后分别比较 dp[i]与dp[p2​]×2,dp[p3​]×3,dp[p5​]×5是否相等,如果相等则将对应的指针加 1。

以上就是整体思路,看到这里,可能很多人会不明白,描述中的指针pi是什么?以下会详细分析

实际上pi的含义是有资格同i相乘的最小丑数的位置。这里资格指的是:如果一个丑数nums[pi]通过乘以i可以得到下一个丑数,那么这个丑数nums[pi]就永远失去了同i相乘的资格(没有必要再乘了),我们把pi++让nums[pi]指向下一个丑数即可。

不懂的话举例说明:

一开始,丑数只有{1},p2,p3,p5都指向1,1可以同2,3,5相乘,取最小的1×2=2添加到丑数序列中。这一步就是上面描述中的min(dp[p2​]×2,dp[p3​]×3,dp[p5​]×5)=> min(2,3,5) ,借用leetcode图进一步说明。

image.png

现在丑数中有{1,2},在上一步中,1已经同2相乘过了,所以今后没必要再比较1×2了,我们说1失去了同2相乘的资格。 所以上面一步p2会+=1,指向2,p3,p5和已经诞生的丑数不相等,还有资格,所以保留不变。如下图,p2指向2,p3,p5还是1

现在1有与3,5相乘的资格,2有与2,3,5相乘的资格,但是2×3和2×5是没必要比较的,因为有比它更小的1可以同3,5相乘,所以我们只需要比较1×3,1×5,2×2。这次最小的是dp[p3​]×3=3,所以p3+=1指向2,另外两个不变

image.png

依此类推,每次我们都分别比较有资格同2,3,5相乘的最小丑数,选择最小的那个作为下一个丑数,假设选择到的这个丑数是同i(i=2,3,5)相乘得到的,所以它失去了同i相乘的资格,把对应的pi++,让pi指向下一个丑数即可。

image.png

综上:

从i=2开始,遍历到i=n即可得到n个丑数,返回dp[i]即可

代码
var nthUglyNumber = function(n) {
     if(n===1) return 1;
     let p2=1,p3=1,p5=1;//指针
     const dp=new Array(n+1).fill(0);//丑数数组
     dp[1]=1;
    for (let i = 2; i <= n; i++) {
         const num2=dp[p2]*2,num3=dp[p3]*3,num5=dp[p5]*5;//本轮待比较的数
         dp[i]=Math.min(Math.min(num2,num3),num5);//比较最小的,得到第i个丑数
         if(num2===dp[i]){
         //而不是if...else if...这样能避免相同的丑数(如:2*3 和 3*2)重复占坑。
             p2++;
         }
         if(num3===dp[i]){
              p3++;
         }
         if(num5===dp[i]){
             p5++;
         }
     }
     return dp[n]
}