Skip to content

Latest commit

 

History

History
213 lines (195 loc) · 7.11 KB

T3-丑数问题.md

File metadata and controls

213 lines (195 loc) · 7.11 KB

丑数问题

丑数的标准定义:丑数 就是只包含质因数 2、3 和/或 5 的正整数, 1为最小的丑数

在该定义的基础上,对质因数和求解目标进行调整,形成了leetcode中常见的丑数问题; 因为涉及质数,因此丑数问题中往往也需要一定的质数算法思想。基本的考察问题为:

  • 判断是否为丑数
  • 计算第n个丑数
  • 改变质因数或者拓展丑数定义,计算第n个丑数 其中计算第n个丑数,常用思路为 排序 + 存储
  • 基于优先队列/堆进行实现
  • 基于动态规划+多指针进行实现

263. 丑数

给你一个整数 n ,请你判断 n 是否为 丑数 。如果是,返回 true ;否则,返回 false 。 丑数 就是只包含质因数 2、3 和/或 5 的正整数 **1 通常被视为丑数。 **

  • 简单的数学判断题目
  • 可以利用循环相除进行余数判断即可
    • 需要注意对特殊情况: 0的判断
class Solution {
public:
    bool isUgly(int n) {
        int factors[3] = {2, 3, 5};
        int i = 0;
        while (n > 0 && i < 3) {
            if (n % factors[i] == 0) {
                n /= factors[i];
            }
            else {
                i++;
            }
        }
        if (n == 1) return true;
        return false;
    }
};

264. 丑数II

给你一个整数 n ,请你找出并返回第 n 个 丑数 。 丑数 就是只包含质因数 2、3 和/或 5 的正整数。

  • 承接[LC263.], 本题需要搜索第n个丑数,最简单的方法为暴力遍历,取第n个数
  • 从算法角度考虑,进行搜索优化:
    • 使用优先队列进行处理
    • 利用最小堆进行中间丑数维护,每次将2、3、5的倍数插入堆中,
    • 进行n次出堆操作,最后一次出堆即为第n个丑数
    • 堆维护了数字的有序性,每次出堆即得到升序的第i个丑数
    • 由于过程中可能出现重复值,使用哈希表进行去重控制
  • 时间复杂度 O(NlogN)
  • 空间复杂度 O(N)
class Solution {
public:
    int nthUglyNumber(int n) {
        // 使用long 避免溢出
        priority_queue<long, vector<long>, greater<long>> q;
        int ugly = 1;
        vector<int> factors = {2, 3, 5};
        unordered_set<long> seen;
        q.push(ugly);
        for (int i = 0; i < n; i++) {
            long cur = q.top();
            ugly = cur;
            q.pop();
            for (auto p : factors) {
                if (!seen.count(p * cur)) {
                    q.push(p * cur);
                    seen.insert(p*cur);
                }
            }
        }
        return ugly;
    }
};
  • 基于动态规划的思想进行搜索:
    • dp[i]定义: 第i个丑数
    • 状态转移: dp[i] = min(dp[p1]*2, dp[p2]*3, dp[p3]*5), 2 <= i <= n
    • 其中p1 p2 p3 为三种因子的数量指针: 代表的是第几个数的2倍、第几个数 3 倍、第几个数 5 倍
    • 本质上计算与最小堆的入堆/出堆逻辑相同:
      • 小顶堆的方法是先存再排,dp的方法则是先排再存
    • 初始化: dp[1] = 1 p1 = p2 = p3 = 1
  • 时间复杂度 O(N) 空间复杂度 o(N)
  • 本题关键: 如何维护有序性 三指针 + 动态规划
class Solution {
public:
    int nthUglyNumber(int n) {
        // 使用long 避免溢出
        vector<long> dp(n + 1);
        dp[1] = 1;
        int p1 = 1;
        int p2 = 1;
        int p3 = 1;
        for (int i = 2; i <= n; i++) {
            dp[i] = min(dp[p1]*2, min(dp[p2]*3, dp[p3]*5));
            // 各个因子指针进行同步比较更新
            if (dp[i] == dp[p1]*2) {
                p1++;
            }
            if (dp[i] == dp[p2]*3) {
                p2++;
            }
            if (dp[i] == dp[p3]*5) {
                p3++;
            }
        }
        return dp[n];
    }
};

313. 超级丑数

超级丑数是指其所有质因数都是长度为 k 的质数列表 primes 中的正整数 1 是任何给定 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] 。
  • 与计算第n个丑数的问题的区别在于,本题是一个质数列表,是对质数做了扩展,但是丑数的本质没有变:
    • 1 是任何给定 primes 的超级丑数
  • 仍然采用最小堆或者多指针的动态规划进行实现
class Solution {
public:
    int nthSuperUglyNumber(int n, vector<int>& primes) {
        vector<int> ps(primes.size(), 1);
        vector<int> dp(n+1);
        dp[1] = 1;
        for (int i = 2; i <= n; i++) {
            dp[i] = dp[ps[0]] * primes[0];
            for (int j = 1; j < primes.size(); j++) {
                dp[i] = min(dp[ps[j]] * primes[j], dp[i]);
            }
            for (int j = 0; j < primes.size(); j++) {
                if (dp[i] == dp[ps[j]] * primes[j]) {
                    ps[j]++;
                }
            }
        }
        return dp[n];
    }
};

1201. 丑数III

给你四个整数:n 、a 、b 、c ,请你设计一个算法来找出第 n 个丑数。 丑数是可以被 a 或 b 或 c 整除的 正整数 。

输入:n = 3, a = 2, b = 3, c = 5
输出:4
解释:丑数序列为 2, 3, 4, 5, 6, 8, 9, 10... 其中第 3 个是 4。
  • 注意: 此题中1 不是默认的丑数,此处为广义上的丑数

  • 由于a b c因子是输入的, 那么该题目的范围应该是比较大的

  • 区别与上面两题中标准的丑数定义,本题无法用上面的dp、堆来解题,一方面时间复杂度还是比较高,另一方面存在临界问题

  • 本题的思路是采用二分法+数学分析来完成的

    • 查找第n个丑数,我们可以通过数学分析得到[0,X]范围内的丑数数量:
    • X/a + x/b + X/c - X/lcm(a,b) - X/lcm(a, c) - X/lcm(b,c) + X/lcm(a,b,c)
      • 即计算被因子整除的数量,其中要避免重复计算的情况,因此需要计算因子间的最小公倍数,根据容斥原理得到实际的丑数数量
      • 计算最小公倍数: lcm(a,b)
      • 计算最大公约数: gcd(a,b)
  • 关键点: 最小公倍数+丑数数量分析

class Solution {
public:
    int nthUglyNumber(int n, int a, int b, int c) {
        long left = 0;
        long right = 2e9;
        // 最小公倍数计算
        // 统一使用long  避免乘法计算溢出
        long la = a;
        long lb = b;
        long lc = c;
        long lab = lcm(la, lb);
        long lac = lcm(la, lc);
        long lbc = lcm(lb, lc);
        long labc = lcm(lab, lc);
        while ( left < right) {
            long mid = left + (right - left) / 2;
            long sum = mid / la + mid / lb + mid / lc - mid / lab - mid/lac -mid/lbc + mid/labc;
            if (sum < n) {
                left = mid + 1;
            }
            else {// sum >= n  
                right = mid;
            }
        }
        return left;
    }
};