丑数
定义
丑数的定义是指只包含质因数2、3、5的正整数。认为1是第一个丑数。1、2、3、4、5、6...
广义的丑数不限定质因数为2,3,5,而是给定一个数组。
丑数:判断给定的整数是不是丑数。
很简单,直接判断是否只包含这三种因子。也就是当它们能够整除2的时候,就一直做除法。3、5同理。最后得到结果为1就是丑数。不然就是有其他因子。
public boolean isUgly(int num) {
if(num<=0){
return false;
}
//判断是否只包含这三种因子
while((num&1)==0){
num>>=1;
}
while(num%3==0){
num/=3;
}
while(num%5==0){
num/=5;
}
return num==1;
}
第n个丑数
丑数 II:找出第n个丑数。丑数序列是1、2、3、4、5、6、8、9...
动态规划
每个丑数都是由前面某个数x2或者x3或者x5得到的。保留三个指针,第一个指针指向2要乘以的“某个数”,第二个指针指向3要乘以的某个数。
丑数序列是从小到大排列的,所以要对2、3、5乘以之后得到的结果作为候选值,来比较大小,选择最小的。如果最小值是2乘积得到的结果,则2对应的指针需要后移。可能同时存在2、3乘积得到的结果都是最小值,这时候他们的指针都要后移。
比如上图,当已有1、2、3、4、5,此时,i2指向2,代表2要取的乘数为res[2]=3,对应候选值23=6,3要取的乘数为res[1]=2,对应候选值32=6,5要取的乘数为res[1]=2,对应候选值为5*2=10.三个候选值中,最小值是6,且2和3都对应6,所以i2和i3同时向后走。
public int nthUglyNumber(int n) {
if(n<=0){
return -1;
}
int factor2=0,factor3=0,factor5=0;
int[] res=new int[n];
res[0]=1;
for(int i=1;i<n;i++){
int candi2=res[factor2]*2;
int candi3=res[factor3]*3;
int candi5=res[factor5]*5;
int min=Math.min(Math.min(candi2,candi3),candi5);
res[i]=min;
if(min==candi2){
factor2++;
}
if(min==candi3){
factor3++;
}
if(min==candi5){
factor5++;
}
}
return res[n-1];
}
堆
通过在最小堆的堆顶取值得到最小值。并且如果有连续都是min则要一直取。
每次遍历得到res[i]时,把res[i]和三个因子相乘得到的结果放入最小堆中。
这样的时间复杂度为O(nlogn)。
超级丑数
变形的丑数:
超级丑数:找第n个超级丑数。所有质因数都是质数列表primes数组(长度k)里的数。
思路:类似上面丑数的解决方式,但是需要把三个元素扩展到对数组的处理。
动态规划
求res[i]时,是求k个质因数乘以自己的乘数得到的候选数的集合中的最小值。然后谁和最小值相等,则谁的指针后移。
时间复杂度为O(N*k)
堆
对应上一题的用堆的解法,也是直接把求res[i],并把res[i]和所有prime的乘积放入堆中。
动态规划+堆
根据动态规划的解法,可以稍微优化是用最小堆来保存这k个数的候选数,拿出最小值,并把它对应的质因数下标后移。并且如果还有最小值,则继续后移。
时间复杂度O(N x m x logk),m是连续能有多少个相等的。
public int nthSuperUglyNumber(int n, int[] primes) {
if(n<=0){
return -1;
}
int m=primes.length;
int[] res=new int[n];
res[0]=1;
int[] factors=new int[m];
PriorityQueue<int[]> queue=new PriorityQueue<>((a,b)->(a[0]-b[0]));
for(int i=0;i<m;i++){
queue.add(new int[]{res[factors[i]]*primes[i],i});
}
for(int i=1;i<n;i++){
int[] node=queue.poll();
int min=node[0];
int id=node[1];
res[i]=min;//注意这个要放在前面来,res[++factors[id]]可能需要
queue.add(new int[]{res[++factors[id]]*primes[id],id});
while(!queue.isEmpty()&&queue.peek()[0]==min){
node=queue.poll();
id=node[1];
queue.add(new int[]{res[++factors[id]]*primes[id],id});
}
}
return res[n-1];
}
特殊的丑数,只要求能整除任意一个
丑数 III:找到第n个丑数。这道题的丑数和之前的都不一样,”丑数是可以被a或b或c整除的正整数“。
输入:n = 5, a = 2, b = 11, c = 13
输出:10
解释:丑数序列为 2, 4, 6, 8, 10, 11, 12, 13... 其中第 5 个是 10。
结果在 [1, 2 * 109] 的范围内
n, a, b, c <= 109
这里能看到6能够整除2,但是6包含因子3并不在a、b、c中。所以只要求能整除,并不要求所有因子都在abc中。
这里如果套用以前的做法,用i2代替res[i2],则会超时,因为n,abc的范围是<=109,求n的时候,需要O(N*3)的时间。
用以前类似堆的做法:对每个i,都把i*a,b,c加入堆中。然后获得第n个(这里可以不用动态更新,所以可以完全加入之后再排序)。——时间复杂度也是O(NlogN),会超时。
在O(N)都会超时的情况下,只能想到O(logN)了,所以可以尝试用二分查找。对于找到的mid,判断是不是第n个。
找x是第几个丑数:就是找[0,x]有多少个数能被a或者b或者c整除。0~x有多少个数能被a整除意味着x/a有多少个。
画个韦恩图,避免把同时是ab的倍数的重复计算。总共有:cnt=x/a+x/b+x/c-x/a/b-x/a/c-x/b/c+x/a/b/c
注意这里不能用x/a/b代表x同时能够被a、b整除。因为当a=4,b=6,其实x只需要能整除12,就能整除a和b。所以应该求最小公倍数
x/a+x/b+x/b-x/lcm(a,b)-x/lcm(b,c)-x/lcm(a,c)+x/lcm(a,b,c)
找到这样的x后,x不一定是丑数。如果p是第n个丑数,x属于[p+min(a,b,c))(左闭右开的区间)都能满足x对应的cnt=n。所以二分法求得n对应的mid后,需要在[mid,mid-min(a,b,c))上找到能够%a或者b或者c的。这个方法时间当a、b、c都不小时,复杂度有点高
第一个满足条件的
找到满足条件的第一个mid即可。
注意ab的最小公倍数有可能超出int范围,需要用long表示。
public int nthUglyNumber(int n, int a, int b, int c) {
long lcm_ab=getLCM(a,b);
long lcm_ac=getLCM(a,c);
long lcm_bc=getLCM(b,c);
long lcm_abc=getLCM(lcm_ab,c);
int min=Math.min(Math.min(a,b),c);
int left=min;
int right=2000000000;
while(left<=right){
int mid=((right-left)>>1)+left;
long cnt=getCnt(mid,lcm_ab,lcm_ac,lcm_bc,lcm_abc,a,b,c);
if(cnt<n){//右
left=mid+1;
}else if(cnt>n){//左
right=mid-1;
}else{//cnt==n
if(getCnt(mid-1,lcm_ab,lcm_ac,lcm_bc,lcm_abc,a,b,c)<n){//第一个=
return mid;
}else{//左
right=mid-1;
left=Math.max(left,mid-min+1);//这里能够稍微缩小一下left的范围
}
}
}
return -1;
}
private long getCnt(long mid,long lcm_ab,long lcm_bc,long lcm_ac,long lcm_abc,int a,int b,int c){//求[0,mid]有多少个数字能被abc整除
return (mid/a+mid/b+mid/c-mid/lcm_ac-mid/lcm_bc-mid/lcm_ab+mid/lcm_abc);
}
private long getLCM(long a,long b){//求最小公倍数
long gcd=getGCD(a,b);
return a/gcd*b;
}
private long getGCD(long a,long b){
if(a<b){
long tmp=b;
b=a;
a=tmp;
}
while(b!=0){
long mod=a%b;
long chu=a/b;
a=b;
b=mod;
}
return a;
}
找到后直接计算
之前提到“找到这样的x后,x不一定是丑数。如果p是第n个丑数,x属于[p+min(a,b,c))(左闭右开的区间)都能满足x对应的cnt=n。”其实mid-min(mid%a,mid%b,mid%c)就能得到比mid小,离mid最近的满足条件的数。
public int nthUglyNumber(int n, int a, int b, int c) {
long lcm_ab=getLCM(a,b);
long lcm_ac=getLCM(a,c);
long lcm_bc=getLCM(b,c);
long lcm_abc=getLCM(lcm_ab,c);
//System.out.println(lcm_ab+" "+lcm_ac+" "+lcm_bc+" "+lcm_abc);
int min=Math.min(Math.min(a,b),c);
int left=min;
int right=2000000000;
int mid=0;
while(left<=right){
mid=((right-left)>>1)+left;
long cnt=getCnt(mid,lcm_ab,lcm_ac,lcm_bc,lcm_abc,a,b,c);
//System.out.println(mid+":"+cnt);
if(cnt<n){//右
left=mid+1;
}else if(cnt>n){//左
right=mid-1;
}else{//cnt==n
break;
}
}
mid=mid-Math.min(Math.min(mid%a,mid%b),mid%c);
return mid;
}