leetcode算法-264. 丑数 II

这篇博客探讨了使用动态规划、堆和哈希表等方法解决寻找第n个丑数的问题。作者通过不断优化算法,从超时的暴力破解到使用堆和哈希表避免重复计算,再到动态规划的解决方案,详细分析了每个方法的思路和遇到的挑战,如数据溢出和顺序问题。最终,作者给出了动态规划的高效解法,通过三个指针跟踪丑数乘以2、3、5的情况,避免了顺序混乱和重复元素的错误。
摘要由CSDN通过智能技术生成

概述

这道题看着挺容易的,但是优化了挺多次才得到最后的结果,印象挺深刻的

方法1:超时的方法

一上来很容易想到暴力破解,for循环每一个数,然后质因数分解判断是不是丑数,但是这种方法一定超时。当时想到,一个数如果是丑数,除以2、3或者5也是个丑数,于是很开心想到了第一个方法,“动态规划”,后来发现高兴的太早了。

#include<unordered_set>
class Solution {
public:
    int nthUglyNumber(int n) {
        unordered_set<int>numberSet;
        numberSet.insert(1);
        int now=1;//已经找到了一个数
        int last=1;//当前最后一个丑数
        int deal=2;
        while(now<n){
            if(isUgly(deal,numberSet)){
                numberSet.insert(deal);
                now++;
                last=deal;
            }
            deal++;
        }
        return last;
    }
    bool isUgly(int nums,const unordered_set<int>& set){
        if(nums%2==0&&set.find(nums/2)!=set.end()){
            return true;
        }
        if(nums%3==0&&set.find(nums/3)!=set.end()){
            return true;
        }
        if(nums%5==0&&set.find(nums/5)!=set.end()){
            return true;
        }
        return false;
    }
};

利用unordered_set库保存已经搜索到的数,然后找到一个数可以很快判断他是不是丑数。但是测试超时了,原因是丑数在n很大的时候,在数轴上应该是比较“稀疏",很多次for循环都白遍历了。

方法2:堆+哈希表

既然除的不行,那么逆向思维。利用已有的丑数,乘2、3和5,再用乘的数再乘。生成一个丑数数组。在我的脑中浮现出的是一颗3叉树…事实证明这个模型在某些程度上阻碍了我的解题,后面再说。

此外,按照本题的要求,需要获得丑数的顺序。但是在按树状顺序生成的过程中,结果是交错的。例如1 产生了 2 3 5,而2产生了4 6 10,如果只是简单的把生成的数插入丑数数组的最后会产生 1 2 3 5 4 6 10很显然顺序是不对的。

针对顺序的问题我想到用最小堆来解决。先将1插入堆,然后重复弹出堆顶,将堆顶元素分别和2 3 5相乘之后插入并调整堆。这样第n次必定弹出第n个丑数。

但是这样还有两个问题,第一个是元素重复,例如:2会产生4 6 10,而3会产生6 9 15。这就导致6被插入了两次,这样的结果不是我们想要的,因此我沿用了方法一的思路,用unordered_set保存已经插入过堆中(为什么加个过是因为有元素从堆里弹出来,但是也记录在unordered_set里面)的元素。

第二个问题是数据溢出的问题。题目的返回值是int且指明n<=1690,说明第1690个丑数还在int的范围内。但是我们在扩展的过程中,并不能精确地“停”在1690个丑数而是可能多找了后面的,就会造成数据溢出。所以我在每次做乘法前,都会检查一下乘法是否溢出,如果会溢出就不做乘法,忽略那个丑数。(由于题目的返回值是int,所以忽略的也不会是答案)

#include<unordered_set>
#include<algorithm>
class Solution {
public:
    static vector<int> result;
    int nthUglyNumber(int n) {
        if(result.size()>100){
            return result[n];
        }
        int n1=1690;
        vector<int>ugly;
        unordered_set<int>uglySet;
        ugly.push_back(1);
        uglySet.insert(1);
        make_heap(ugly.begin(),ugly.end(),greater<int>());
        int last=1;
        int now=1;
        for(;now<n1;now++){
            int least=ugly[0];
            pop_heap(ugly.begin(),ugly.end(),greater<int>());
            ugly.pop_back();
            result.push_back(least);
            if(INT_MAX/2>=least&&uglySet.find(2*least)==uglySet.end()){
                ugly.push_back(2*least);
                push_heap(ugly.begin(),ugly.end(),greater<int>());
                uglySet.insert(2*least);
            }
            if(INT_MAX/3>=least&&uglySet.find(3*least)==uglySet.end()){
                ugly.push_back(3*least);
                push_heap(ugly.begin(),ugly.end(),greater<int>());
                uglySet.insert(3*least);
            }
            if(INT_MAX/5>=least&&uglySet.find(5*least)==uglySet.end()){
                ugly.push_back(5*least);
                push_heap(ugly.begin(),ugly.end(),greater<int>());
                uglySet.insert(5*least);
            }     
        }
        last=ugly.front();
        result.push_back(last);
        return result[n];
    }
};
vector<int> Solution::result{0};

另外一个很骚的操作是定义成静态数组…牛逼这是我看答案的时候发现的。

方法3:动态规划

在方法2中提到,在我的脑中先入为主地产生了一个3叉树,我很自然地认为乘2 3 5的过程需要同时对同一个丑数进行,这就产生了顺序的问题。但是在官方动态规划的解法中,引入了三个指针(mul2,mul3,mul5),分别指向将要被乘以2,将要被乘以3和将要被乘以5的数,可以单独移动某一个指针。

因此算法变成,每次比较 ugly[mul2]*2, ugly[mul3]*3, ugly[mul5]*5中最小的,移动对应的指针,由于只移动最小的指针,保证不会出现顺序乱了的问题。

class Solution {
public:
    static int ugly[1690];
    static int tag;
    int nthUglyNumber(int n) {
        if(tag==1){
            return ugly[n-1];
        }
        tag=1;
        int mul2=0;
        int mul3=0;
        int mul5=0;
        ugly[0]=1;
        for(int i=1;i<1690;i++){
            int minIn3=min(ugly[mul2]*2,ugly[mul3]*3);
            minIn3=min(minIn3,ugly[mul5]*5);
            if(minIn3==ugly[mul2]*2){
                mul2++;
            }
            if(minIn3==ugly[mul3]*3){
                mul3++;
            }
            if(minIn3==ugly[mul5]*5){
                mul5++;
            }
            ugly[i]=minIn3;//如果把push分别放到三个if语句里,会出现重复的错误,用else if结果也同样会出错
        }
        return ugly[n-1];
    }
};
int Solution::tag=0;
int Solution::ugly[1690];

写的时候有两个注意的地方,1.三个if语句不能用else if,如果用了,报错。2.习惯性把ugly[i]的赋值放到三个if语句里面,也错。原因都是因为插入重复元素引起的。在方法2中提过,有可能出现不同丑数乘 2 3 5后结果相同的问题。这里如果出现相同,要把多个指针同时前移一格,并且只在ugly中插入1个元素。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值