丑数

定义

丑数的定义是指只包含质因数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乘积得到的结果都是最小值,这时候他们的指针都要后移。
CleanShot 2019-12-13 at 20.44.05@2x
比如上图,当已有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;
}
原文地址:https://www.cnblogs.com/FannyChung/p/chou-shu.html