概述
这道题看着挺容易的,但是优化了挺多次才得到最后的结果,印象挺深刻的
方法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个元素。