直接枚举是一个很容易想到同时又很容易否决的方法,它的耗时是无法接受的。
考虑第二种方法,由于丑数的2、3、5倍仍然是丑数,所以就可以通过目前已有的丑数获得新的丑数。
为了方便,将通过一个丑数寻找另3个丑数的过程成为丑数的扩展。我们可以将丑数存储起来并依次对它们进行扩展。
刚开始我没完全掌握这个思路,弄错了,用了queue来存。虽然queue、vector这些结构可以按照顺序取出丑数并对其进行扩展,但它无法保证取丑数时按照递增的顺序来取: 1 → 1 , 2 , 3 , 5 → 1 , 2 , 3 , 5 , 4 , 6 , 10 1\rarr1,2,3,5\rarr1,2,3,5,4,6,10 1→1,2,3,5→1,2,3,5,4,6,10
可以看到,找到4发生在找到5之后。
如果将找到的第n个丑数直接作为答案,那么就会导致错误。不过当然,也可以将找到的所有丑数存起来,当找到的丑数数目超过n,甚至于是远超n,直到可以确保前n个丑数都被找出时,才返回从小到大的第n个丑数。这样的方案固然可行但很浪费。
使用priority_queue或者set之类按顺序存储数据的结构来代替可以避免丑数产生的顺序非递增的情况。
class Solution {
public:
int nthUglyNumber(int n) {
unordered_set<long long> vis;
priority_queue<long long,vector<long long>,greater<long long>> q;
q.push(1);
vis.insert(1);
while(--n)
{
int p=q.top();
q.pop();
if(!vis.count(p*2ll))
{
vis.insert(p*2ll);
q.push(p*2ll);
}
if(!vis.count(p*3ll))
{
vis.insert(p*3ll);
q.push(p*3ll);
}
if(!vis.count(p*5ll))
{
vis.insert(p*5ll);
q.push(p*5ll);
}
}
return q.top();
}
};
可以注意到,上述的过程中,在优先队列中堆积了大量待处理的数。实际上这些数并不需要早早地被处理出来。如果能够一次一个地找丑数,那么应该可以避免由于排序而带来的性能损耗。
贪心地想,每次选择最小的数,乘上2、3、5中最小的乘数,那么应该可以找到下一个丑数。
在开始时,已知的丑数为1,按照上述思路,应当选2为下一个丑数。但是再接下来就出现了冲突:最小的丑数是1,但1*2已经被选过了。
这时候就要放宽限制,允许找次小的丑数,或者允许选次小的乘数,那么下一个丑数应该是1*3或2*2。比较之后选择3为下一个丑数。
再下一个呢?是1*5还是2*2还是3*2?再之后呢?但是等下,观察这三个可供选择的项,可以看出与2、3、5相乘的数已经不再一致。因此,可以考虑使用三个指针分别指向2、3、5应当乘的下一个丑数,然后每当需要选择下一个丑数时,比较三者算出的丑数的大小,选择最小的。然后对应的指针移动一位。
class Solution {
public:
int nthUglyNumber(int n) {
int p2=0,p3=0,p5=0;
int dp[n];
memset(dp,0,sizeof(dp));
dp[0]=1;
for(int i=1;i<n;++i)
{
int newVal=min(2ll*dp[p2],min(3ll*dp[p3],5ll*dp[p5]));
dp[i]=newVal;
if(newVal==2ll*dp[p2])
++p2;
if(newVal==3ll*dp[p3])
++p3;
if(newVal==5ll*dp[p5])
++p5;
}
return dp[n-1];
}
};
然后翻题解又翻到了一种解法:分别计算2、3、5的次幂,然后将它们相乘。具体的代码写法就是三重循环,每层循环的循环变量每次分别乘2、3、5。当查找的数足够多时,就可以确保前n个丑数都被覆盖在内。