264.丑数II

[TOC]

一、题目描述

编写一个程序,找出第 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
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

二、解题方法

1. 暴力法


思路:

最原始的暴力法,令u_i表示第i个丑数的实际数值,则对于从1 \sim u_n的每一个数字,我们都令它对235做因式分解,来判定他是否为丑数,直至找到第n个丑数。
粗略估算,一个计算的上界是\sum_{i=1}^{u_n}log\ i
进一步的简化估算,我们可以得到时间复杂度:O(u_nlog\ u_n),空间复杂度:O(1)

2. 记忆化搜索


思路:

我们重新思考一下题目,丑数的定义是“只包含质因数 2, 3, 5 的正整数”。
那反过来说,"若k为大于1的丑数,则k/2k/3k/5中至少有一个是丑数"。
利用这这一点,我们可以得到一个递推方程:
dp[i] = \begin{cases} 1, & i\text{ mod }2=0\quad \text{ and }\quad dp[i/2]=1 \\ 1, & i\text{ mod }3=0\quad \text{ and }\quad dp[i/3]=1 \\ 1, & i\text{ mod }5=0\quad \text{ and }\quad dp[i/5]=1 \\ 0, & others \end{cases}
初始条件有dp[1]=1,令u_i表示第i个丑数的实际数值,则对于2 \sim u_n,我们都可以在常数时间内判断出它是否为丑数,但我们需要保存1 \sim u_n中的每一个数字,以方便之后的快速判断。

时间复杂度:O(u_n),空间复杂度:O(u_n)

3. 递推


思路:

上述两个方法,都需要计算u_n个数字,考虑到丑数虽然前期分布比较密集,但到后期相对就会稀疏很多,因而这样的计算量有些浪费。

在方法2中,我们提到,“若k为大于1的丑数,则k/2k/3k/5中至少有一个是丑数”,并且给出了递推方程。但观察递推方程,我们可以明显地看到,需要进行的判断太多了,不够优雅。
这句话如果反过来说就是,“若k为丑数,则2k3k5k都是丑数”。

可以利用这一点进行迅速递推,对于每一个丑数,我们都可以得到三个新的丑数,那么对于第n个丑数,必定会在至多3n次乘法计算中出现(某些丑数会被计算多次,比如6可以分别由2和3递推得到)

嗯?等等,存在重复?存在重复不利于我们计算第n个吧?因为谁知道前面出现了多少重复?必须得把重复去掉。
那么,怎么去重复呢?常见的手段,自然就是使用哈希表来做标记了。

至此,我们已经完成了查重的判断了。下一个步骤就是在一堆没有重复的丑数当中,取出第n大的丑数。

那么,该怎么做呢?

由于我们是要求第n个丑数,换句话说就是要求这些丑数中第n小的。这样我们不由自主地会想到对数组排序,或者直接使用堆。
进一步的,显然我们希望每一次都拿出还未进行递推的丑数中最小的那一个进行递推(这样能避免真的每次都递推到3n)
结合二者,于是这里决定选择堆来处理。进一步的,因为每次都要取出最小的,我们决定使用小顶堆来处理。

对于每一个丑数,递推出三个新的丑数复杂度为O(1),哈希表中查重平均复杂度为O(1),取堆顶部元素复杂度为O(1),往堆中插入和删除元素复杂度都为O(logn),而外循环共需执行n次。

于是我们有,总的时间复杂度:O(n+nlogn),总的空间复杂度:O(n)


代码:

class Solution {
public:
    int nthUglyNumber(int n) {
        if (n < 7)      return n;

        priority_queue<unsigned long long, vector<unsigned long long>, greater<unsigned long long>> q;
        unordered_set<unsigned long long> s;
        q.push(1);
        s.insert(1);
        while (--n) {
            auto num = q.top();
            q.pop();
            if (s.find(num * 2) == s.end()) {
                q.push(num * 2);
                s.insert(num * 2);
            }
            if (s.find(num * 3) == s.end()) {
                q.push(num * 3);
                s.insert(num * 3);
            }
            if (s.find(num * 5) == s.end()) {
                q.push(num * 5);
                s.insert(num * 5);
            }
        }
        return q.top();
    }
};

4. 动态规划


思路:

方法3是已经足够解决问题了,但总觉得好像还不够看。
为了判断重复而单独使用了一个哈希表,对于强迫真来说这真的令人坐立不安。有没有什么办法能取消掉那个哈希表?

既然想要去掉哈希表,那么首先肯定得搞清楚这里的哈希表有什么用。正如方法3所属,因为递推的时候很可能递推到重复的数字,所以我们使用哈希表进行去重。
那么想去掉哈希表,问题就转化成了,如何避免递推到重复的数字。

要避免递推到重复的数字,我们可以将计算目标转换为当前这个丑数,而不是递推出的另外三个丑数。记当前正在计算的第i个丑数为dp[i]。根据丑数的定义,我们知道,1以外的丑数,或者是某个丑数乘以2,或者是某个丑数乘以3,或者是某个丑数乘以5。如果分别设这三个数字为xyz,我们一定dp[i] = min(2x, 3y, 5z)(因为升序,所以每个丑数因该是能得到的最小值)

于是进一步的,问题又转化为如何获得并维护xyz。由于起初的三个丑数235可以看作是2 \times 13 \times 15 \times 1,所以最初xyz都可以设置为1。那么接着当计算出一个新的丑数的时候,该如何对xyz进行维护呢?可以想象一下,以x为例,当dp[i] \ge 2x时,显然x已经不足以用去递推dp[i+1],因而我们应该将x设置为比x大的丑数。同时为了保证dp[i+1]尽可能地小,x应该仅仅向后移动一位,更新为仅次于x的丑数yz同理

时间复杂度:O(n),空间复杂度:O(n)


代码:

class Solution {
public:
    int nthUglyNumber(int n) {
        vector<int> dp(n, numeric_limits<int>::max());
        vector<int> ptr(3, 0);
        vector<int> primes = { 2, 3, 5 };
        dp[0] = 1;
        for (auto i = 1; i < n; ++i) {
            for (auto j = 0; j < 3; ++j)
                dp[i] = min(dp[i], dp[ptr[j]] * primes[j]);
            for (auto j = 0; j < 3; ++j)
                if (dp[i] >= dp[ptr[j]] * primes[j])
                    ptr[j]++;
        }
        return dp[n - 1];
    }
};
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

  • 题目描述 编写一个程序,找出第 n 个丑数。 丑数就是只包含质因数 2, 3, 5 的正整数。 示例 1: 输入:...
    zhipingChen阅读 246评论 0 1
  • 题目链接难度:中等 类型: 数学、数组 编写一个程序,找出第 n 个丑数。 丑数就是只包含质因数...
    wzNote阅读 533评论 0 1
  • 题目描述 编写一个程序,找出第 n 个丑数。 丑数就是只包含质因数 2, 3, 5 的正整数。 示例: 思路 1....
    youzhihua阅读 271评论 0 0
  • 编写一个程序,找出第 n 个丑数。 丑数就是只包含质因数 2, 3, 5 的正整数。 示例: 输入: n = 10...
    calm_peng阅读 323评论 0 0
  • “都知道的,我谈过很多次恋爱,次数多了,不免相信量变生成质变,觉得上了无数趟秋名山,即便出车祸也不至于把引擎撞烂。...
    LiebeNiall阅读 156评论 0 0

友情链接更多精彩内容