5.1-5.2高效解决常见数论问题

5.1 如何高效寻找素数

5.1.1 常规解法

  • 素数的定义:如果一个数只能被1和它本身整除,那么这个数就称为素数,

输入一个正整数n,函数返回区间[2,n)中素数的个数

  • 一种常见的写法如下
    public int countPrimes(int n) {
        int ans = 0;
        for(int i = 2;i<n;i++){
            if(isPrime(i)){
                ans++;
            }
        }
        return ans;
    }

    private boolean isPrime(int n){
        for(int i = 2;i*i<=n;i++){
            if(n%i == 0){
                //有其他整除因子
                return false;
            }
        }
        return true;
    }

这种解法下,i不需要遍历到你n,只需要到sqrt(n)即可。假设n=12时

12 = 2 * 6
12 = 3 * 4
12 = sqrt(12) * sqrt(12)
12 = 4 * 3
12 = 6 * 2
  • 可以看到,后两个因素就是前面两个因素反过来而已,反转临界点就是在sqrt(n)。所以说,如果在[2,sqrt(n)]这个区间之内没有发现可整除的因子,就可以直接断定n是素数了,如果在区间[sqrt(n),n]也一定不会发现可整除的因子

5.1.2 高效解法(筛数法)

首先从2开始,我们知道2是一个素数,那么2的所有倍数:2X2=4,3X2=6,4X2=…都不可能是素数了

然后我们发现3也是素数,那么所有3的倍数:3X2=6,3X3=9,3X4=12都不可能是素数了

    public int countPrimes(int n) {
        boolean[] isPrime = new boolean[n];
        //将数组都初始化为true
        Arrays.fill(isPrime,true);
        //素数从2开始算起
        for(int i = 2;i<n;i++){
            if(isPrime[i]){
                //i的倍数不可能再是素数了
                for(int j = 2*i;j<n;j+=i){
                    isPrime[j]=false;
                }
            }
        }
        int count = 0;
        for(int i = 2;i<n;i++){
            if(isPrime[i]){
                count++;
            }
        }
        return count;
    }

简要阐明一下上述算法的思路,从2开始,开始寻找2的倍数,一旦发现2的倍数,就把它标记成false,表示这个数肯定不是素数,然后从[2,n)中取数不断枚举,直到把在n以内的所有的、有可能的数都给他标记完了,那么就可以开始O(n)的遍历了

  • 优化解法

回想刚才判断一个数是否是素数的isPrime函数,由于乘法因子的对称性,其中的for循环只要遍历[2,sqrt(n)]就可以了,这样也是一样的原因,外层的for循环也只需要遍历到sqrt(n)就可以了

 for(int i = 2;i*i<=n;i++)

同时,内层for循环也可以优化,回顾之前的做法

//i的倍数不可能再是素数了
for(int j = 2*i;j<n;j+=i){
    isPrime[j]=false;
}
  • 这样做的话可以保证i的整数倍都标记为false,但是依然存在计算冗余

比如说n=25,i=4的时候,算法会标记4X2=8,4X3=12等数字,但是8和12这两个数字已经被i=2i=3的2X4和3X4给标记过了,所以可以优化一下,让内层的j从i的平方开始遍历,而不是从2*i开始

因为当x>2的时候,x肯定已经被2给选过了,但是肯定没有给x选过,因此从(x^2)开始的话就能保证它是最小的未被其他数所过滤的数

最终解法

    public int countPrimes(int n) {
        boolean[] isPrime = new boolean[n];
        //将数组都初始化为true
        Arrays.fill(isPrime,true);
        //素数从2开始算起
        for(int i = 2;i*i<=n;i++){
            if(isPrime[i]){
                //i的倍数不可能再是素数了
                for(int j = i*i;j<n;j+=i){
                    isPrime[j]=false;
                }
            }
        }
        int count = 0;
        for(int i = 2;i<n;i++){
            if(isPrime[i]){
                count++;
            }
        }
        return count;
    }

5.2 如何高效进行模幂运算

你的任务是计算 (a^b)1337 取模,a 是一个正整数,b 是一个非常大的正整数且会以数组形式给出。

比如输入a=2,b=[1,2]就会让你返回2^121337求模的结果,也就是4096%1337=85

5.2.1 问题分析

  • **如何处理用数组表示的指数?**现在b是一个数组,也就是说b可以相当大,没办法转成整形,可能会产生溢出,怎么把这个数组作为指数进行运算呢?
  • **如何得到求模之后的结果?**按道理,应该要先把幂结果求出来,如何才能和1337求模取余,但问题是,指数运算的真实结果肯定会非常大,也就是说,算出来真实结果也没办法表示,肯定会溢出报错
  • 如何进行高效的幂运算?

5.2.2 处理数组指数

首先明确问题:现在b是一个数组,不能表示成整形,而且数组的特点是随机访问,删除最后一个元素是比较高效的(只需要O(1)的运算时间),不考虑求模的要求,以b=[1,5,6,4]来举例,结合指数运算的法则,可以发现:

a ( [ 1 , 5 , 6 , 4 ] ) a^([1,5,6,4]) a([1,5,6,4])

= a 4 ∗ a [ 1 , 5 , 6 , 0 ] =a^4*a^[1,5,6,0] =a4a[1,5,6,0]

= a 4 ∗ ( a [ 1 , 5 , 6 ] ) ( 10 ) =a^4*(a^[1,5,6])^(10) =a4(a[1,5,6])(10)

superPow(a,[1,5,6,4])
=> superPow(a,[1,5,6])
  • 从函数调用的角度来看,这是一种递归的结构,因为通过不断的函数调用,可以发现问题的规模缩小了
    public int superPow(int a, int[] b) {
        return superPow(a,b,b.length-1);
    }

    private int superPow(int a,int[] b,int index){
        //递归的base-case
        if(index == -1){
            return 1;
        }
        //取出最后一个数
        int last = b[index];
        //将原问题化简,缩小规模求解
        int part1 = myPow(a,last);
        int part2 = myPow(superPow(a,b,index-1),10);
        return part1*part2;
    }

    private int myPow(int a,int k){
        return 0;
    }

5.2.3 处理mod运算

形如(a*b)%base这样的运算,乘法的结果可能会导致溢出,我们希望找到一种技巧,能够化简这种表达式,避免溢出同时还能得到结果(就比如在二分搜索中将索引(l+r)/2转化为l+(r-l)/2),在避免溢出的同时也能得到正确的结果

下面首先来说一下模运算的技巧

(a*b)%k=(a%k)*(b%k)%k;
证明:
a=Ak+B,b=Ck+D(A,B,C,D为任意常数),:
ab = ACk^2+ADK+BCk+BD
得:ab%k=BD%k
又:a%k=B;b%k=D
所以(a%k)*(b%k)%k=BD%k
  • 简单地说,对乘法的结果求模,等价于先对每个因子求模,然后对因子相乘的结果再求模
  • 那么扩展到这道题,求一个数的幂不就是对这个数连乘吗?所以只需要简单扩展刚才的思路。
    public static int MOD = 1337;
    public int superPow(int a, int[] b) {
        return superPow(a,b,b.length-1);
    }

    private int superPow(int a,int[] b,int index){
        //递归的base-case
        if(index == -1){
            return 1;
        }
        //取出最后一个数
        int last = b[index];
        //将原问题化简,缩小规模求解
        int part1 = myPow(a,last);
        int part2 = myPow(superPow(a,b,index-1),10);
        return (part1%MOD)*(part2%MOD)%MOD;//凡是可能存在溢出的地方都应该进行取模
    }

    private int myPow(int a,int k){
        int res = 1;
        //连乘k次
        for(int _ = 0;_<k;_++){
            res = (res%MOD) * (a%MOD)%MOD;
        }
        return res;
    }

5.2.4 高效求幂(快速幂)

//要求a^b
int res = 0;
if(b%2 == 1){//b是奇数
    res = pow(a,b-1)*a
}else{
    res = pow(pow(a,b/2),2);
}
  • 这个思想肯定会被k个a连乘的要高效,因为有机会直接把问题的规模b的大小直接减少一半了
    public static int MOD = 1337;
    public int superPow(int a, int[] b) {
        return superPow(a,b,b.length-1);
    }

    private int superPow(int a,int[] b,int index){
        //递归的base-case
        if(index == -1){
            return 1;
        }
        //取出最后一个数
        int last = b[index];
        //将原问题化简,缩小规模求解
        int part1 = myPow(a,last);
        int part2 = myPow(superPow(a,b,index-1),10);
        return (part1%MOD)*(part2%MOD)%MOD;//凡是可能存在溢出的地方都应该进行取模
    }

    private int myPow(int a,int k){
        if(k==0){
            return 1;
        }
        if(k%2 == 1){//k是奇数
            return ((a%MOD) * (myPow(a,k-1)%MOD)%MOD);
        }else{
            //k是偶数
            int sub = myPow(a,k/2);
            return  (sub%MOD) * (sub%MOD) %MOD;
        }
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值