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

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

 

示例 1:

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

示例 2:

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

 

提示:

  • 1 <= n <= 1690
Related Topics
  • 哈希表
  • 数学
  • 动态规划
  • 堆(优先队列)
  • \n
  • 👍 710
  • 👎 0
  • 题解

    先说明一个概念

    每个合数都可以写成几个质数(也可称为素数)相乘的形式 ,这几个质数就都叫做这个合数的质因数。如果一个质数是某个数的因数,那么就说这个质数是这个数的质因数;而这个因数一定是一个质数。(来自于百度百科)

    举例

    • 1没有质因子。
    • 5只有1个质因子,5本身。(5是质数)
    • 6的质因子是2和3。(6 = 2 × 3)
    • 2、4、8、16等只有1个质因子:2。(2是质数,4 =2²,8 = 2³,如此类推)
    • 10有2个质因子:2和5。(10 = 2 × 5)

    所以我们知道了,所求数字即为,给定的2、3、5相互组合相乘得到的结果,可以重复相乘,即可以使2x2x2x3x3x5x5x5这样的组合。

    而题中所求的第n位即是将所得结果按照从小到大的顺序有序排列的第n位数字,且根据质因数的概念,第一位一定是数字1。

    那么,我们可以按照题意按部就班的来求解,结果集的第一位是1,那么结果集此时是【1】

    来到第二位。可以是前面的结果集第一位的1×2,也可以是1×3,也可以是1×5,但是按照从小到大的顺序,应该是1×2,此时结果集为【1,2】

    来到第三位,因为前面的1×2已经算过,可以直接忽略,那么我们要比较的就是2×2(此处的2是前面得到1×2的结果的2,不是自然数递增的2),和上次剩下的1×3,1×5,显然结果应该是3,此时的结果集【1,2,3】

    来到第四位需要比较的是2×2,2×3,1×5,显然应该是4,此时的结果集为【1,2,3,4】

    后面依次:

    第五位 3×2,2×3,1×5,结果集【1,2,3,4,5

    第六位 3×2,2×3,2×5,结果集是【1,2,3,4,5,6】,而此时的3×2和2×3都能得到6这个结果,显然是重复的,应该只记一次

    第七位 4×2,3×3,2×5,结果集是【1,2,3,4,5,6,8

    第八位 5×2,3×3,2×5,结果集【1,2,3,4,5,6,8,9

    第九位 5×2,4×3,2×5,结果集【1,2,3,4,5,6,8,9,10

    所以自己手动算了这么多步骤,规律应该可以看出来了,数字2,3,5分别在结果集中维护了一个指针,一开始都指向了索引0。结果集中第一位索引0上的值为1。

    然后每次迭代找出2,3,5与索引位置上的值相乘的乘积中最小的一个,将这个结果放入结果集中的下一位,并将当前对应数字的指针位置沿着索引往后移动一位。如果有多个数与索引位置的乘积相等都为最小值,那么这些数字的指针都往后移动一位。

    
    //leetcode submit region begin(Prohibit modification and deletion)
    class Solution {
        public int nthUglyNumber(int n) {
            int[] res = new int[n];
            res[0] = 1;
            int p2 = 0, p3 = 0, p5 =0;
            int k = 1;
            while (k < n){
                int num = Math.min(Math.min(res[p2]*2,res[p3]*3),res[p5]*5);
                if (res[p2]*2==num){
                    p2++;
                }
                if (res[p3]*3==num){
                    p3++;
                }
                if (res[p5]*5==num){
                    p5++;
                }
                res[k] = num;
                k++;
            }
            return res[k-1];
        }
    }
    //leetcode submit region end(Prohibit modification and deletion)

    当前题目中固定了丑数范围为2,3,5,更多的非固定丑数范围的题目详见LeetCode刷题【313】 超级丑数

    新更新内容:

    重新做了LeetCode刷题【263】丑数,并再经过了前一段时间动态规划相关题目的练习之后,得到新的理解与启示

    class Solution {
        public int nthUglyNumber(int n) {
            int[] dp = new int[n];
            dp[0] = 1;
            int[] numArr = new int[]{2,3,5};
            int[] pArr = new int[3];
            for (int i = 1; i < n; i++) {
                dp[i] = Math.min(Math.min(dp[pArr[0]]*numArr[0],dp[pArr[1]]*numArr[1]),dp[pArr[2]]*numArr[2]);
                for (int p = 0; p < pArr.length; p++) {
                    if (dp[i] == dp[pArr[p]]*numArr[p]){
                        pArr[p] = pArr[p]+1;
                    }
                }
            }
            return dp[n-1];
        }
    }

    代码可能大同小异,不过在理解上,

    以一个现有的丑数序列为例[1,2,3,4,5,6,8,9,10,12],一开始其实只有一个初始的索引0位置的1。根据丑数的特性,我们知道丑数F(n)一定能由之前的某个丑数乘以2或者乘以3或者乘以5得到,那么究竟取哪个呢,因为要保持递增性,所以应该是较小的。

    而有了这个理解之后,在去看LeetCode刷题【313】 超级丑数的代码其实很容易从上面的新的代码能修改得到