什么是丑数?

先看一下百度百科的解释:

说法一(ugly number):把只包含质因子2,3和5的数称作丑数(Ugly Number)。例如6、8都是丑数,但7、14不是,因为它们包含质因子7。 习惯上我们把1当做是第一个丑数。

说法二(humble number):对于一给定的素数集合 S = {p1, p2, …, pK},考虑一个正整数集合,该集合中任一元素的质因数全部属于S。这个正整数集合包括,p1、p1p2、p1p1、p1p2p3…(还有其它)。该集合被称为S集合的“丑数集合”。

相关题目

leetcode上丑数相关题目有四个, 这篇文章来解决并总结这四道题目的解法.

截屏2020-05-31下午3.25.57

263. 丑数

编写一个程序判断给定的数是否为丑数。

丑数就是只包含质因数 2, 3, 5 的正整数。

示例 1:

输入: 6 输出: true 解释: 6 = 2 × 3 示例 2:

输入: 8 输出: true 解释: 8 = 2 × 2 × 2 示例 3:

输入: 14 输出: false 解释: 14 不是丑数,因为它包含了另外一个质因数 7。 说明:

1 是丑数。 输入不会超过 32 位有符号整数的范围: [−231, 231 − 1]。

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

题解:

思路一:

  • 判断一个数字是不是丑数, 从头到位计算每一位比当前数字小或者相等的丑数
  • 再计算下一个丑数,如果下一个丑数刚好 == 传入数字。 则是丑数
  • 否则不是丑数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/*
    * 第一种思路
    * 在丑数小于 num 时
    * 依次计算,丑数数列的下一个
    * 直到数列的数字 >= 丑数时
    * 当 == 丑数时,true。 当 > 丑数时, false。
    * */
    public static boolean isUgly(int num) {
        if (num <= 0) return false;

        int p2 = 0, p3 = 0, p5 = 0;
        ArrayList<Integer> list = new ArrayList<>();
        list.add(1);

        int i = 0;
        while (list.get(i) < num){
            list.add(Math.min(Math.min(list.get(p2) * 2, list.get(p3) * 3), list.get(p5) * 5));

            i ++;
            if (list.get(i) == list.get(p2) * 2) p2 ++;
            if (list.get(i) == list.get(p3) * 3) p3 ++;
            if (list.get(i) == list.get(p5) * 5) p5 ++;
        }

        return list.get(i) == num;
    }

截屏2020-05-30下午10.14.26

此解法,因为n 的 取值范围**[−231, 231 − 1]**, 当n过大时, 额外申请的数组的存储空间会很大,超出内存限制。

时间复杂度 : O(N)

空间复杂度 : O(1)

思路二:

  • 如果是丑数, 则num / 2i /3j / 5k 最终为一

  • 如果余数 != 0, 则代表除不尽, 退出循环

  • 继续除下一个

  • 一直除到5, 如果最终数为1,则为丑数, 否则不是丑数

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
    /*
        *
        * 思路二:
        * 如果是丑数, 则 除2 , 除3, 除5 最终为1。
        * 如果 余数 != 0, 代表除不尽, 退出循环
        * 继续除下一个
        * 一直除到5,如果最终数为1, 则为丑数,否则不是丑数
        * */
        public static boolean isUgly1(int num) {
            if (num <= 0) return false;
      
            while (num % 2 == 0) num /= 2;
            if (num == 1) return true;
      
            while (num % 3 == 0) num /= 3;
            if (num == 1) return true;
      
            while (num % 5 == 0) num /= 5;
            if (num == 1) return true;
      
            return false;
        }
    

    截屏2020-05-30下午10.16.56

思路三:

递归

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/*
* 
* 思路三:
* 递归
* 跟思路二类似
* 迭代 换成 递归
* */
public static boolean isUgly(int num) {
    if (num < 1) return false;
    if (num == 1) return true;
    return search(num);
}

public static boolean search(int num){
    if (num == 1) return true;

    // 如果对 2 3 5都不能整除, 则说明这条路走不通, 返回上一层
    if (num % 2 != 0 && num % 3 != 0 && num % 5 != 0) return false;

    if (num % 2 == 0){
        return search(num >> 1);
    }else if (num % 3 == 0){
        return search(num / 3);
    }
    return search(num / 5);
}

截屏2020-05-30下午10.28.49

264. 丑数 II

编写一个程序,找出第 n 个丑数。

丑数就是质因数只包含 2, 3, 5 的正整数。

示例:

输入: n = 10 输出: 12 解释: 1, 2, 3, 4, 5, 6, 8, 9, 10, 12 是前 10 个丑数。 说明:

1 是丑数。 n 不超过1690。

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

题解:

思路一:

  • 利用最小堆 (Java中的优先级队列)
  • 利用优先级队列自动排序的功能
  • 每次取出对头元素,存入对头元素 * 2, 对头元素 * 3, 对头元素 * 5
  • 但注意,像12这个元素,可以是 4 * 3得到,也可以是 2 * 6得到,所以注意去重
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
 /*
    *
    * 思路一:
    * 最小堆
    * 利用优先队列有自动排序的功能
    * 每次取出队头元素,存入队头元素*2、队头元素*3、队头元素*5
    * 但注意,像12这个元素,可由4乘3得到,也可由6乘2得到,所以要注意去重
    * */
    public int nthUglyNumber1(int n) {

        PriorityQueue<Long> queue = new PriorityQueue<>();

        long answer = 1;
        for (int i = 1; i < n; i++) {
            queue.add(answer * 2);
            queue.add(answer * 3);
            queue.add(answer * 5);

            answer = queue.remove();
            while (!queue.isEmpty() && queue.peek() == answer) queue.remove();
        }

        return (int)answer;
    }

截屏2020-05-30下午10.57.00

思路二:

对思路一的优化, 用set去重复

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
    /*
    * 进一步使用 set 优化 去重
    * */
    public int nthUglyNumber2(int n) {

        PriorityQueue<Long> queue = new PriorityQueue<>();
        HashSet<Long> set = new HashSet<>();
        int[] nums = {2,3,5};

        long answer = 1;
        for (int i = 1; i < n; i++) {
            long tmp = queue.peek();
            for (int j = 0; j < nums.length; j++) {
                if (set.contains(nums[i] * tmp)){
                    set.add(nums[i] * tmp);
                    queue.add(nums[i] * tmp);
                }
            }
            answer = queue.remove();
        }
        return (int)answer;
    }

截屏2020-05-30下午11.03.43

思路三:

  • 动态规划,三指针法
  • 起初三个指针都指向数组0的位置
  • 当数组的下一个是 *2 得来时, p2 ++, *3得来时,p3++, *5得来时,p5++
  • 注意去重, 有可能下一个数字即是 * 2得来,又是 * 3得来时,p2 和 p3都需要 ++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
    /*
     *
     * 思路三:
     * 动态规划 三指针法
     * 起初三根指针都指向数组 0 位置
     * 当数组的下一个是 *2 得出时, p2++, *3 得出时, p3++, *5 得出时, p5++
     * 注意有可能,同时是 2 和 3的公倍数, 比如6, 当下一个为6时, p2 和 p3同时 ++
     * */
    public int nthUglyNumber(int n) {
        int p2 = 0, p3 = 0, p5 = 0;
        int[] dp = new int[n];
        dp[0] = 1;
        for (int i = 1; i < n; i++) {
            dp[i] = Math.min(Math.min(dp[p2] * 2, dp[p3] * 3), dp[p5] * 5);

            if (dp[i] == dp[p2] * 2) p2 ++;
            if (dp[i] == dp[p3] * 3) p3 ++;
            if (dp[i] == dp[p5] * 5) p5 ++;
        }
        return dp[n - 1];
    }

截屏2020-05-30下午11.06.48

1201. 丑数 III

请你帮忙设计一个程序,用来找出第 n 个丑数。

丑数是可以被 a 或 b 或 c 整除的 正整数。

示例 1:

输入:n = 3, a = 2, b = 3, c = 5 输出:4 解释:丑数序列为 2, 3, 4, 5, 6, 8, 9, 10… 其中第 3 个是 4。 示例 2:

输入:n = 4, a = 2, b = 3, c = 4 输出:6 解释:丑数序列为 2, 3, 4, 6, 8, 9, 12… 其中第 4 个是 6。 示例 3:

输入:n = 5, a = 2, b = 11, c = 13 输出:10 解释:丑数序列为 2, 4, 6, 8, 10, 11, 12, 13… 其中第 5 个是 10。 示例 4:

输入:n = 1000000000, a = 2, b = 217983653, c = 336916467 输出:1999999984

提示:

1 <= n, a, b, c <= 10^9 1 <= a * b * c <= 10^18 本题结果在 [1, 2 * 10^9] 的范围内

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

题解:

拿到题目只想到, 递推去算下一个丑数,算到末尾,看是否N是不是丑数。

算了半天,还超时了。 想不到思路,去查看题解中思路。

以下是 题解区域大神思路

基础思路

首先,为什么第一时间能想到二分法?

让我们观察题目,可以看到,最终状态(即n)的范围非常大。试图自底向上递推或是按照通常的自顶向下回溯显然会超时(比如动态规划、DFS等方法)

面对这么大的状态空间,二分法的时间复杂度是logN,因此能够大大压缩需要遍历的状态数目

思路剖析

既然已经确定了二分法作为切入点,关键问题来了,如何二分呢?

按照题意,所谓丑数是可以至少被a、b、c三者中的一者整除的,那么对于一个丑数X,我们能够确定它是第几个丑数吗?

–答案显然是可以的,我们只需要计算X中包含了多少个丑数因子即可。

即只需要知道在[0,X]范围内,还有多少个丑数即可,而这些丑数,无非就是一些能被a或者b或者c所整除的数。

那么显然,我们直接用X/a、X/b、X/c就能计算出[0,X]范围内有多少数能被a或者b或者c整除,然后把它们加起来就是答案!

但是仔细思考一下,我们是不是重复计算了些什么?如果一个数既能被a整除,又能被b整除,那么实际上该数在先前的计算中就被重复计算了一次(分别是在计算X/a和X/b时)。

–好吧,让我们思考所有可能的情况

1.该数只能被a整除 (该数一定是a 的整数倍)

2.该数只能被b整除 (该数一定是b 的整数倍)

3.该数只能被c整除 (该数一定是c 的整数倍)

4.该数只能被a和b同时整除 (该数一定是a、b最小公倍数的整数倍)

5.该数只能被a和c同时整除 (该数一定是a、c最小公倍数的整数倍)

6.该数只能被b和c同时整除 (该数一定是b、c最小公倍数的整数倍)

7.该数只能被a和b和c同时整除(该数一定是a、b、c的最小公倍数的整数倍)

所以,我们只需要分别计算以上七项就能得到结果了!让我们分别来看(用MCM+下标表示最小公倍数):

情况1 = X/a - 情况4 - 情况5 - 情况7 情况2 = X/b - 情况4 - 情况6 - 情况7 情况3 = X/c - 情况5 - 情况6 - 情况7 情况4 = X/MCM_a_b - 情况7 情况5 = X/MCM_a_c - 情况7 情况6 = X/MCM_b_c - 情况7 情况7 = X/MCM_a_b_c

让我们整理上述方程后也就得到:

sum(情况) = X/a + X/b + X/c - X/MCM_a_b - X/MCM_a_c - X/MCM_b_c + X/MCM_a_b_c

好了,现在也就得到了计算X中包含多少个丑数因子的方法了!

至于计算最小公倍数的方法,这里不多介绍,概括而言就是对于两个数a和b,它们的最小公倍数 = a*b/(a和b的最大公约数),最大公约数可以通过辗转相除法得到

二分搜索

在得到了计算任意数中包含了多少个丑数因子的方法后,我们实际上只需要通过二分法,不断缩小边界范围,直到某个位置所对应的数恰好包含了n个丑数因子为止。

注意,通过二分法计算的答案并非是最终答案,因为可以有很多数同时包含有n个丑数因子!

比如第n个丑数是X,那么[X,X + min(a,b,c))这个半开区间内的所有数都同时包含n个丑数因子,我们通过二分法得到的答案也随机分布于这个区间中。而实际上我们只需要得到该区间的左端即可。处理方法很简单:假设我们得到的临时答案是K(K∈[X,X + min(a,b,c))),那么K - min(K%a,K%b,K%c) = X.也就是只需要把临时答案减去其与a、b、c三者中取余的最小值即可!

作者:Alfeim 链接:https://leetcode-cn.com/problems/ugly-number-iii/solution/er-fen-fa-si-lu-pou-xi-by-alfeim/ 来源:力扣(LeetCode) 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public static int nthUglyNumber(int n, int a, int b, int c) {
    // 看到 n 的范围就应该想到二分
    long low = Math.min(Math.min(a, b), c);

    long high = low * n;

    long res = binarySearch(low, high, a, b, c, n);
    long cnta = res % a;
    long cntb = res % b;
    long cntc = res % c;
    res -= Math.min(cnta, Math.min(cntb, cntc));

    return (int) res;
}


public static long binarySearch(long low , long high, int a, int b, int c, long n){
    if (low >= high) return low;

    long mid = (low + high) >> 1;

    long MCM_a_b = MCM(a, b);
    long MCM_a_c = MCM(a, c);
    long MCM_b_c = MCM(b, c);

    long MCM_a_b_c = MCM(MCM_a_b, c);

    long count = mid / a + mid / b + mid / c - mid / MCM_a_b - mid / MCM_a_c - mid / MCM_b_c + mid / MCM_a_b_c;

    if (count == n) return mid;

    if (count > n) return binarySearch(low, mid - 1, a, b, c, n);
    return binarySearch(mid + 1, high, a, b, c, n);
}

// 求最小公倍数 : 两数相乘 除以 最小公约数
public static long MCM(long m, long n){

    long Multi = m * n;
    while (n > 0){
        long tmp = m % n;
        m = n;
        n = tmp;
    }
    return Multi / m;
}

313. 超级丑数

编写一段程序来查找第 n 个超级丑数。

超级丑数是指其所有质因数都是长度为 k 的质数列表 primes 中的正整数。

示例:

输入: 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] 。 说明:

1 是任何给定 primes 的超级丑数。 给定 primes 中的数字以升序排列。 0 < k ≤ 100, 0 < n ≤ 106, 0 < primes[i] < 1000 。 第 n 个超级丑数确保在 32 位有符整数范围内。

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

题解:

思路一:

动态规划

  • 此题目与 264 不一样的地方,在于此题质数在数组中,不确定。而264质数就是2 3 5三个
  • 使用动态规划, 264三个指针足以。 而此题需要指数数组中元素个数数量的指针
  • 所以除了使用 dp 数组以外,还需要一个 pointers 数组,用来存放与质数数组对应的位置指针指向的位置
  • 代码思路,跟264 基本类似
  • 依然需要注意, 同一个丑数,可能同时由两个质数 * 其dp对应位置得出, 所以需要去重!

代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public int nthSuperUglyNumber(int n, int[] primes) {
        if (n <= 1) return 1;

        // 存放 primes 对应位置的数字 指针指向的位置
        int[] pointers = new int[primes.length];

        int[] dp = new int[n];
        dp[0] = 1;
        for (int i = 1; i < n; i++) {
            int next = Integer.MAX_VALUE;
            for (int j = 0; j < primes.length; j++) {
                next = Math.min(next, primes[j] * dp[pointers[j]]);
            }

            dp[i] = next;

            for (int j = 0; j < pointers.length; j++) {
                if (next == primes[j] * dp[pointers[j]]) pointers[j]++;
            }

        }
        return dp[n - 1];
    }

截屏2020-05-30下午11.25.20