算法竞赛中的基本数论

初等数论

数论思维的引入

分苹果

(for循环可做)

P1372 又是毕业季I

(分析最大公约数,但实际用不到辗转相除法)

素数判定

知识点

题目练习

B3750 [信息与未来 2019] 幸运素数

(暴力判断素数)

#include<bits/stdc++.h>
using namespace std;
bool prime(int a){
        if (a <= 1) return 0;
        for(int i = 2;i * i <= a;i++){
                if(a % i == 0) return 0;
        }
        return 1;
}
int main(){
        int a,b;
        bool x = 1;
        cin >> a >> b;
        for(int i = a;i <= b;i++){
                int re = i;
                x = 1;
                while(re){
                        if(!prime(re)){
                                x = 0;
                                break;
                        } 
                        re /= 10;
                }
                if(x) cout << i << endl;
        }
                
        return 0;
}
P1304 哥德巴赫猜想

(暴力判断素数)

最大质因子个数【算法赛】

题目分析

求包含不同的质因子数最多的数。那就累乘所有的质因子呗,累乘的过程不能超过给定的n。因为有多次询问,我们可以把质数都预处理出来。

题目代码

import java.util.*;
public class Main
{
    public static void main(String[] args)
    {
        Scanner sc=new Scanner(System.in);
        int step=0;
        int[] a=new int[100005];
        for(int i=2;i<=10000;i++)//找出所有的质数
        {
            if(check(i))
            {
                a[++step]=i;
            }
        }
        int t=sc.nextInt();
        while(t-->0)
        {
            long n=sc.nextLong();
            double now=1;
            int ans=0;
            for(int i=1;i<=step;i++)
            {
                now=now*a[i];
                ans++;
                if(now>=n)
                {
                    
                    break;
                }
            }
            if(now==n)
            {
                System.out.println(ans);
            }else
            {
                System.out.println(ans-1);
            }
            
        }
    }
    public static boolean check(int i)
    {
        for(int j=2;j<=Math.sqrt(i);j++)
        {
            if(i%j==0)
                return false;
        }
        return true;
    }
}
小蓝的金牌梦【算法赛】

题目分析

既然对长度有要求,那么可以优先遍历子段的长度,然后遍历右端点,这样可以唯一的确定一个子段,然后使用前缀和快速求出子段的和,虽然是两层嵌套for循环,但是由于质数的个数很少,所以第二层循环执行的次数要比n小的多,所以可以通过。

题目代码

import java.util.Scanner;
// 1:无需package
// 2: 类名必须Main, 不可修改

public class Main {
    public static void main(String[] args) {
        Scanner scan = new Scanner(System.in);
        long n = scan.nextLong();
     long max = Integer.MIN_VALUE;
        long[]a = new long[(int)(n+1)];
        for (int i = 1; i <= n; i++) {
            a[i] = scan.nextLong();
            a[i] += a[i-1];
        }
        for(int i = 2; i <= n;i++)
        {
            if(isprime(i)){
                for(int j = i; j <= n; j++)
                {
                    max = Math.max(a[j]-a[j-i],max);
                }
            }
        }
        System.out.println(max);
        scan.close();
    }
    public static boolean isprime(long len)
    {

        for(int i = 2; i*i <= len; i++)
        {
            if(len % i == 0) return false;
        }
        return true;
    }
}

辗转相除法

辗转相除法引入

知识点介绍
#include<bits/stdc++.h>
using namespace std;
int gcd(int x,int y){
	if(y == 0) return x;
	return gcd(y,x%y);
}

signed main(){
	int x,y;
	cin >> x >> y;
	cout << gcd(x,y);
	return 0;
}

模板题目
B3736 [信息与未来 2018] 最大公约数

(最大公约数)

P4057 [Code+#1] 晨跑

(最小公倍数)

练习题目

[NOIP2014 普及组] 比例简化

(好在数据范围小,可以直接枚举)

P2651 添加括号III

(需要仔细想一下)

[NOIP2001 普及组] 最大公约数和最小公倍数问题

(还要了解到两个数的乘积就是这两个数最大公约数和最小公倍数的乘积)

P1170 兔八哥与猎人

(还挺难的,要画图进行模拟分析,最后是要确定两个数字是否互质)

P8469 [Aya Round 1 D] 文文的数学游戏

(数学推导)

题目分析:

M = g c d ( b 1 , b 2 . . . . . . b n ) M=gcd(b_1,b_2......b_n) M=gcd(b1,b2......bn),则 M < = m i n b M<=minb M<=minb,其中 m i n b minb minb表示所有的 b i b_i bi中的最小值。又因为 1 < b i < = a i 1<b_i<=a_i 1<bi<=ai,所以 m i n b minb minb能够取到的最大值是 a i a_i ai的最小值,所以 M M M可以取到的最大值是 a i a_i ai的最小值。什么情况下 M < = m i n a M<=mina M<=mina呢?即 b i b_i bi m i n a mina mina的倍数,那么对于 m i n a mina mina对应的 b i b_i bi必然等于 m i n a mina mina。其他值可以是 c ∗ m i n a c*mina cmina但是不能超过它对应的 a i a_i ai的值,因此每个位置可以取值的个数为 a i / m i n a a_i/mina ai/mina,这里下取整,可能的方案数是所有位置可以取值的个数的累乘。

题目代码:

#include <iostream>
using namespace std;
int a[100050],n,mgcd=1<<30;
const int mod=1e9+7;
int main()
{
    cin >> n;
    for (int i=1;i<=n;i++)
    {
        cin >> a[i];
        mgcd=min(mgcd,a[i]);
    }
    long long ans=1;
    for (int i=1;i<=n;i++)
        ans=(long long)ans*(a[i]/mgcd)%mod;
    cout << mgcd << " " << ans << endl;
    return 0;
}
既约分数(遍历,最大公约数)

题目分析

题目代码

import java.util.*;
public class Main {
    public static void main(String[] args) {
        int ans = 0;
        for(int i = 1 ; i <= 2020 ; i ++){
          for(int j = 1 ; j <= 2020 ; j ++){
            if(gcd(i , j) == 1) ans ++;
          }
        }
        System.out.println(ans);
    }
    static int gcd(int a,int b){
      if(b == 0) return a;
      else return gcd(b , a % b);
    }
}
寻找整数

题目分析

根据题目描述,其实是求解一元线性同余方程组
x ≡ a 1 ( m o d   m 1 ) x\equiv a_1(mod \, m_1) xa1(modm1)

x ≡ a 2 ( m o d   m 2 ) x\equiv a_2(mod \, m_2) xa2(modm2)

x ≡ a 3 ( m o d   m 3 ) x\equiv a_3(mod \, m_3) xa3(modm3)

. . . . . . ...... ......

x ≡ a n ( m o d   m n ) x\equiv a_n(mod \, m_n) xan(modmn)

如果 x x x是方程组的一个解,那么 x + k ∗ l c m ( m 1 , m 2 , m 3 . . . m n ) x+k*lcm(m_1,m_2,m_3...m_n) x+klcm(m1,m2,m3...mn)是方程组的一个通解。

因此我们可以先求第一个式子的解 x 1 x_1 x1,那么第二个式子的解应该是在 x 1 + k ∗ l c m ( x 1 , 1 ) x_1+k*lcm(x_1,1) x1+klcm(x1,1)里去找,这样才能保证得到的解依然是第一个式子的解,假设第一个式子和第二个式子的解是 x 2 x_2 x2,那么第三个式子的解应该是在 x 2 + k ∗ l c m ( x 1 , x 2 ) x_2+k*lcm(x_1,x_2) x2+klcm(x1,x2)里去找,这样才能保证得到的解依然是第一个式子和第二个式子的解。 依次类推即可。

题目代码

import java.util.*;
import java.io.*;
public class Main {
    //求数字a,b的最大公约数
    public static long gcd(long a, long b) {
        if (b == 0) return a;
        return gcd(b, a % b);
    }
    //求数字a,b的最小公倍数
    public static long lcm(long a, long b){
        return a / gcd(a, b) * b;
    }
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        long[] a = {0, 0,
            1,2,1,4,5,4,1,2,9,0,5,10,
            11,14,9,0,11,18,9,11,11,15,17,9,
            23,20,25,16,29,27,25,11,17,4,29,22,
            37,23,9,1,11,11,33,29,15,5,41,46};
        long x = 0, step = 1;
        for(int i = 2; i <= 49; i++)
        {
            //暴力求解满足x % i = a[i]
            while(x % i != a[i])
                x += step;
            step = lcm(step, i);
        }
        System.out.println(x);
    }
}

唯一分解定理

1.内容

任何一个大于1的整数n都可以分解成若干个质数的连乘积,如果不计各个质数的顺序,那么这种分解是惟一的,即若n>1,则有
n = ∏ p i j n=\prod{p^j_i} n=pij
这里的 p i p_i pi是质数。

可以进行简单证明,假设 p i p_i pi是合数,那么它可以接着分解为两个数相乘的形式,所以最后 p i p_i pi一定是质数。

2.唯一分解定理模板

模板代码其实也是唯一分解定理的直接应用,给一个整数n,问有多少个质数是n的约数。这里就需要进行分解,也就是用到了唯一分解定理,我们直接上代码,然后逐一解释难懂的地方。

import java.util.Scanner;

public class Main {
public static void main(String[] args) {
    Scanner scanner = new Scanner(System.in);
    long n = scanner.nextLong();
    int ans = 0;//
    //分解n的质因子 n%i==0   n/i
    for (int i = 2; i*i <= n; i++) {
        if(n%i==0) ans++;
        while(n%i == 0) {//36   6 i=6 n肯定不会有6  2和3
            n = n / i;
        }
    }
    if(n > 1) {
        ans++;
    }
    System.out.println(ans);
}
}
  1. 一般n可能给的很大,注意最好用long类型

  2. 我们如果要求一个数的因数,会从1开始遍历进行试除,那么应该遍历到哪里呢?是n吗?其实遍历到 n \sqrt{n} n 就可以了。因为如果找到了一个因子为a,那么大于 n \sqrt{n} n 的另一个因子就是n/a

  3. 很明显这个for循环我们是在采用试除法来求n的因子,但是我们如何保证求到的因子是质因子呢?这就是里面的while循环的作用了。给大家举个例子,n=36,6是它的因子,但是不是质因子,那么我们会不会遍历到它呢?i=2时,在while循环里就把36里的所有2都除没了,此时n=9。i=3时,在while循环里就把36里的所有3都除没了,此时n=1。那么此时的n里面已经不包含6了,因为6是由2和3构成的,在遍历到6之前,n里的所有的2和3都没有了,自然也就没有6了。这就是while循环的作用,他保证了我们找到的因子一定是质数。

    抽象一点证明,假设q是合数且是n的因子,因为q是合数所有可以被表示成 q = p 1 ∗ p 2 q=p_1*p_2 q=p1p2,而 p 1 p_1 p1 p 2 p_2 p2一定比q小,那么他们一定会在while里面被除去,因此遍历到q时不再包含它们,自然也不会有q。

  4. 最后一个地方,为什么在代码的最后要加一个if判断呢?还是给大家举一个例子,比如n=396,i=2时,n=99。n=3时,n=11。当i=4时i> n \sqrt{n} n ,所以for循环退出了。但是你也可以发现,11也是n的一个质因子,所以我们在最后要判断一下,防止把这种情况漏了。

质因数个数

题目分析

唯一分解定理的直接应用

约数????因数,因子

n = p 1 2 + p 2 3 n=p_1^2+p_2^3 n=p12+p23

求n的所有因子的个数

36 = 2 2 ∗ 3 2 36=2^2*3^2 36=2232

1( 2 0 ∗ 3 0 2^0*3^0 2030) 36( 2 2 ∗ 3 2 2^2*3^2 2232)

2( 2 1 ∗ 3 0 2^1*3^0 2130) 18( 2 1 ∗ 3 2 2^1*3^2 2132)

3 12

4 9

6

2 i ∗ 3 j 2^i*3^j 2i3j

0-2 3种

0-2 3种

36的所有的因子个数

题目代码

#include <iostream>
using namespace std;
int main() {
    long long n;
    cin >> n; // 输入n 分解为多个质因子相乘的形式
    long long sum = 0;   
    for (long long i = 2; i * i <= n; i++) {
        if (n % i == 0) {
            sum++;  // i 是 n 的一个质因子
        }
        while (n % i == 0) {
            n = n / i;  // 一直除以 i,直到不能被整除
        }
    }
    // 如果n大于1,说明剩下的n是质数
    if (n > 1) {
        sum++;
    }
    cout << sum << endl;  // 输出质因子个数
    return 0;
}
import java.util.Scanner;

public class Main {
 public static void main(String[] args) {
  Scanner scanner = new Scanner(System.in);
  long n = scanner.nextLong();
  long sum = 0;
  for (long i = 2; i * i <= n; i++) {
     if (n % i == 0) sum++;
     while (n % i == 0)n = n / i;
  }
  if(n > 1) sum++;
  System.out.println(sum);
 }
}

3.应用

(1)求正整数n的正因子个数

假设n的分解公式如下,
n = p 0 j 0 ∗ p 1 j 1 ∗ p 2 j 2 . . . ∗ p k j k n=p^{j_0}_0*p^{j_1}_1*p^{j_2}_2...*p^{j_k}_k n=p0j0p1j1p2j2...pkjk
则n的因子个数为 ( j 0 + 1 ) ∗ ( j 1 + 1 ) ∗ ( j 2 + 1 ) . . . ∗ ( j k + 1 ) (j_0+1)*(j_1+1)*(j_2+1)...*(j_k+1) (j0+1)(j1+1)(j2+1)...(jk+1)个。

简单理解一下,对于 2 3 ∗ 3 2 ∗ 5 3 2^{3}*3^{2}*5^{3} 233253来说,可以选择一个2,其余质因子不选,则2是其因子,也可以选择两个2和一个3,则12是其因子,总的来说,假设n包含m个质因子p,则对于p我有m+1中选择,即[0,m]。

做个类比,我有红球3个,绿球5个,他们共有多少种不同的组合,肯定是4*6吧,那么n的因子个数为 ( j 0 + 1 ) ∗ ( j 1 + 1 ) ∗ ( j 2 + 1 ) . . . ∗ ( j k + 1 ) (j_0+1)*(j_1+1)*(j_2+1)...*(j_k+1) (j0+1)(j1+1)(j2+1)...(jk+1)同理。

模板代码如下,

import java.util.Scanner;
public class 约数个数 {
public static void main(String[] args) {
    Scanner scanner = new Scanner(System.in);
    long n = 1200000;
    int ans = 1;
    for (int i = 2; i <= Math.sqrt(n); i++) {
        int cnt = 0;
        while(n%i == 0) {
            n = n / i;
            cnt++;
        }
        ans *=(cnt+1);
    }
    if(n > 1) {
        ans *= 2;
    }
    System.out.println(ans);
}
}

约数个数

题目分析:

题目代码:

#include <iostream>
#include <cmath>
using namespace std;

int main() {
    long long n = 1200000;
    int ans = 1;

    // 从2开始判断每个可能的因子
    for (int i = 2; i <= sqrt(n); i++) {
        int cnt = 0;

        // 除尽所有 i 的倍数
        while (n % i == 0) {
            n = n / i;
            cnt++;
        }

        // 计算该质因子的因子个数
        ans *= (cnt + 1);
    }

    // 如果n大于1,说明剩下的n是质数
    if (n > 1) {
        ans *= 2;
    }

    cout << ans << endl;  // 输出结果
    return 0;
}
import java.util.Scanner;

public class Main{
public static void main(String[] args) {
    Scanner scanner = new Scanner(System.in);
    long n = 1200000;
    int ans = 1;
    for (int i = 2; i <= Math.sqrt(n); i++) {
        int cnt = 0;
        while(n%i == 0) {
            n = n / i;
            cnt++;
        }
        ans *=(cnt+1);
    }
    if(n > 1) {
        ans *= 2;
    }
    System.out.println(ans);
}
}
(2)求正整数n的所有正因子之和

假设n的分解公式如下,
n = p 0 j 0 ∗ p 1 j 1 ∗ p 2 j 2 . . . ∗ p k j k n=p^{j_0}_0*p^{j_1}_1*p^{j_2}_2...*p^{j_k}_k n=p0j0p1j1p2j2...pkjk
则n的所有正因子之和为 s u m = ( p 0 0 + p 0 1 + . . . + p 0 j 0 ) ∗ ( p 1 0 + p 1 1 + . . . + p 1 j 1 ) ∗ . . . ∗ ( p k 0 + p k 1 + . . . + p k j k ) sum=(p_0^0+p_0^1+...+p_0^{j_0})*(p_1^0+p_1^1+...+p_1^{j_1})*...*(p_k^0+p_k^1+...+p_k^{j_k}) sum=(p00+p01+...+p0j0)(p10+p11+...+p1j1)...(pk0+pk1+...+pkjk)个。

这里如果正向推导的话不是很简单,其实对于 p 0 0 p_0^0 p00而言,能够和它相乘成为n的因子的数为 p 1 p_1 p1的幂次里选一个幂次, p 2 p_2 p2的幂次里选一个幂次… p k p_k pk的幂次里选一个幂次,当然对于 p i p_i pi的幂次都会被选择,只是不是同时被选择,既然不是同时,就把他们写成加法,式子 p 0 0 ∗ ( p 1 0 + p 1 1 + . . . + p 1 j 1 ) p_0^0*(p_1^0+p_1^1+...+p_1^{j_1}) p00(p10+p11+...+p1j1)表示的是从 p 1 p_1 p1的所有幂次里进行选择与 p 0 0 p_0^0 p00组成一个因子,扩展到其它质数就是 p 0 0 ∗ ( p 1 0 + p 1 1 + . . . + p 1 j 1 ) ∗ . . . ∗ ( p k 0 + p k 1 + . . . + p k j k ) p_0^0*(p_1^0+p_1^1+...+p_1^{j_1})*...*(p_k^0+p_k^1+...+p_k^{j_k}) p00(p10+p11+...+p1j1)...(pk0+pk1+...+pkjk),然后再找对于 p 0 1 p_0^1 p01能够和它相乘成为n的因子的数,可以表示为 p 0 1 ∗ ( p 1 0 + p 1 1 + . . . + p 1 j 1 ) ∗ . . . ∗ ( p k 0 + p k 1 + . . . + p k j k ) p_0^1*(p_1^0+p_1^1+...+p_1^{j_1})*...*(p_k^0+p_k^1+...+p_k^{j_k}) p01(p10+p11+...+p1j1)...(pk0+pk1+...+pkjk),把 p 0 p_0 p0的所有幂次都考虑到就是 ( p 0 0 + p 0 1 + . . . + p 0 j 0 ) ∗ ( p 1 0 + p 1 1 + . . . + p 1 j 1 ) ∗ . . . ∗ ( p k 0 + p k 1 + . . . + p k j k ) (p_0^0+p_0^1+...+p_0^{j_0})*(p_1^0+p_1^1+...+p_1^{j_1})*...*(p_k^0+p_k^1+...+p_k^{j_k}) (p00+p01+...+p0j0)(p10+p11+...+p1j1)...(pk0+pk1+...+pkjk)

4.进阶题目

阶乘约数

问题描述
定义阶乘 n! = 1 × 2 × 3 × ··· × n。

请问 100! (100 的阶乘)有多少个约数。

答案提交
这是一道结果填空的题,你只需要算出结果后提交即可。
本题的结果为一个整数,在提交答案时只填写这个整数,填写多余的内容将无法得分。

题目分析

这是蓝桥杯国赛的一道原题。求一个数的约数个数可以用唯一分解定理在 O ( n ) O(\sqrt{n}) O(n )的时间复杂度内求解。但是100的阶乘确实有点大了,你要是把100的阶乘求出来再去求会超时,并且这个数的存储也是一个问题,当然用java的大整数是可以存的。

这里只能舍弃掉求100的阶乘再求它的约数个数的思路,那应该怎么求呢?回顾一下利用唯一分解定理求解约数个数的过程,先对数字n进行质因数分解,得到式子 n = p 0 j 0 ∗ p 1 j 1 ∗ p 2 j 2 . . . ∗ p k j k n=p^{j_0}_0*p^{j_1}_1*p^{j_2}_2...*p^{j_k}_k n=p0j0p1j1p2j2...pkjk,然后可以求得n的因子个数为 ( j 0 + 1 ) ∗ ( j 1 + 1 ) ∗ ( j 2 + 1 ) . . . ∗ ( j k + 1 ) (j_0+1)*(j_1+1)*(j_2+1)...*(j_k+1) (j0+1)(j1+1)(j2+1)...(jk+1)。其实我们只需要知道数字n里面包含几个 p i p_i pi即可。对于 100 ! = 1 ∗ 2 ∗ 3 ∗ 4... ∗ 100 100!=1*2*3*4...*100 100!=1234...100,我们可以计算出2里面包含数字2的个数为 a 1 a_1 a1,3里面包含数字2的个数为 a 2 a_2 a2,4里面包含数字2的个数为 a 3 a_3 a3,5里面包含数字2的个数为 a 4 a_4 a4,以此类推直到求到100,那么100!里面包含数字2的个数就是 a 1 + a 2 + a 3 . . . a 99 a_1+a_2+a_3...a_{99} a1+a2+a3...a99。综上我们可以依次对1到100里面的每一个数进行质因数分解,得到的值累加就可以了,最终就可以求出来100进行质因子分解的结果。然后再按照求因子个数的方法进行求解就可以了。

题目代码

public class Main {
public static void main(String[] args) {
	int p[] = new int[105];
	for(int i = 2;i <= 100;i++) {
		int n = i;
		for(int j = 2;j * j <= n;j++) {
			while(n%j==0) {
				p[j]++;
				n/=j;
			}
		}
		if(n > 0) p[n]++;
	}
	long ans = 1;
	for(int i = 2;i <= 100;i++) {
		ans *= (p[i]+1);
	}
	System.out.println(ans);
}
}
#include <iostream>
using namespace std;
int p[100];
int main()
{
    for (int i = 2; i <= 100; i ++)
    {
        int n = i;
        for (int j = 2; j <= n / j; j ++)
            while(n % j == 0)
            {
                p[j] ++;
                n /= j;
            }
        if(n > 1) p[n] ++;    
    }
    
    long ans = 1;
    for (int i = 2; i <= 100; i ++)
        if(p[i]) ans *= (p[i] + 1);

    cout << ans << endl;
    return 0;    
}
序列求和

问题描述
学习了约数后,小明对于约数很好奇,他发现,给定一个正整数 t,总是可
以找到含有 t 个约数的整数。小明对于含有 t 个约数的最小数非常感兴趣,并
把它定义为 S t S_t St
例如 S 1 S_1 S1 = 1, S 2 S_2 S2 = 2, S 3 S_3 S3 = 4, S 4 S_4 S4 = 6,···。
现在小明想知道,前 60 个 S i S_i Si 的和是多少?即 S 1 + S 2 + ⋅ ⋅ ⋅ + S 60 S_1 + S_2 + ··· + S_{60} S1+S2+⋅⋅⋅+S60 是多少?
答案提交
这是一道结果填空的题,你只需要算出结果后提交即可。本题的结果为一
个整数,在提交答案时只填写这个整数,填写多余的内容将无法得分。

题目分析1

这也是蓝桥杯国赛的一道真题。参考题解

考虑一下i=12时怎么求解 S i S_i Si

12 = 6 ∗ 2 12=6*2 12=62 S i = 2 5 ∗ 3 1 S_i=2^5*3^1 Si=2531

12 = 3 ∗ 2 ∗ 2 12=3*2*2 12=322 S i = 2 3 ∗ 3 1 ∗ 5 1 S_i=2^3*3^1*5^1 Si=233151

12 = 4 ∗ 3 12=4*3 12=43 S i = 2 3 ∗ 3 2 S_i=2^3*3^2 Si=2332

12 = 2 ∗ 2 ∗ 3 12=2*2*3 12=223 S i = 2 1 ∗ 3 1 ∗ 5 2 S_i=2^1*3^1*5^2 Si=213152

假设数字n的因数个数有12个,那么根据12的分解结果,可以推出来 S i S_i Si的值(每个乘数间1就是要求的数分解后的质数的幂次),但是这个值并不是唯一的,不同的分解结果有不同的值,同一个分解结果不同的幂次分配方式也对应不同的值。比如 12 = 2 ∗ 2 ∗ 3 12=2*2*3 12=223这种分解结果,如果想要n的值比较小,那么就把这几个数字分配到前3小的质数上,即 S i = 2 1 ∗ 3 1 ∗ 5 2 S_i=2^1*3^1*5^2 Si=213152。但是这不是最理想的分配方式。我们将3,2,2降序排序,再减一为2,1,1,那么显然 S i = 2 2 ∗ 3 1 ∗ 5 1 S_i=2^2*3^1*5^1 Si=223151要比之前求的 S i = 2 1 ∗ 3 1 ∗ 5 2 S_i=2^1*3^1*5^2 Si=213152更小。

总结一下这道题的解题步骤,对于 S i S_i Si,我们dfs搜出i所有的分解情况,然后按照刚刚的办法即对分解降序后求出 S i S_i Si。这样一种分解对应一个值,所有的分解对应的值里面求最小值就是 S i S_i Si

另外,特判因子数为质数,比如因子数是13,减一是12,这个幂次全部分配给2就是我们要找的最小数。

题目代码1

import java.math.BigInteger;
public class Main {
    static int n=10000;
    static int  prime[]=new int[n];
    static int index=0;
    static BigInteger endBigInteger=new BigInteger("99999999999999999999999999999999999");
    public static void main(String[] args) {
        int vis[]=new int[n];
        for (int i = 2; i <n; i++) {
            if (vis[i]==0) {
                prime[index]=i;
                index++;
                for (int j = i*i; j <n; j+=i) {
                    vis[j]=1;
                }
            }
        }

    BigInteger sumBigInteger=new BigInteger("0");
    for (int i = 1; i <=60; i++) {
         int vis1[]=new int[i+1];
         dfs(i,i,0,vis1);
         sumBigInteger=sumBigInteger.add(endBigInteger);
            endBigInteger=new BigInteger("99999999999999999999999999999999999");
    }
    System.out.println(sumBigInteger);
    }
    static void dfs(int Snum,int mid,int start,int vis[]) {
        if (mid==1) {
            if (endBigInteger.compareTo(loop(Snum, vis))==1) {
                endBigInteger=loop(Snum, vis);
            }
            return;
        }
        for (int i = 2; i <=mid; i++) {
            if (mid%i==0) {
                vis[i]++;
                dfs(Snum,mid/i, start, vis);
                dfs(Snum,mid/i, start+1, vis);
                vis[i]--;
            }
        }
    }
    static BigInteger loop(int num,int vis[]) {
        int vis2[]=new int[vis.length];
        for (int i = 0; i < vis.length; i++) {
            vis2[i]=vis[i];
        }
        int index2=0;
        BigInteger sumBigInteger=new BigInteger("1");
        for (int i =vis2.length-1; i>0; i--) {
            if (vis2[i]>0) {
                sumBigInteger=sumBigInteger.multiply(new BigInteger(prime[index2++]+"").pow(i-1));
                vis2[i]--;
                i=i+1;
            }
        }
        return sumBigInteger;
    }
}

题目分析2

刚刚的分析是比较正规但是也比较麻烦的思路,这道题还有另外一种讨巧的思路。 S i S_i Si最多由4个质数构成,要使值最小那么这4个质数必然是2,3,5,7。我只需要枚举2,3,5,7对应的幂次就可以了。在枚举的过程中记录当前有t个约数的值,和之前记录的值取一个最小。最后求和输出就行。

题目代码2

import java.util.*;
public class Main {
    static int testCount=60;
    static int ii=100;
    static long result[]=new long[61];
    public static void main(String[] args) {
        for (int a4 = 0; a4 <= ii; a4++) {
            for (int a3 = 0; a3 <= ii; a3++) {
                for (int a2 = 0; a2 <= ii; a2++) {
                    for (int a1 = 0; a1 <= ii; a1++) {
                        int t=(a1+1)*(a2+1)*(a3+1)*(a4+1);
                        if(t<=60) {
                            long single=(long) (Math.pow(2, a1)*Math.pow(3, a2)*Math.pow(5, a3)*Math.pow(7, a4));
                            if(single<result[t] || result[t]==0) {
                                result[t]=single;
                            }
                        }
                    }
                }
            }
        }
        long sum=0;
        for (int i = 1; i <= testCount; i++) {
            sum+=result[i];
        }
        System.out.println(sum);
    }
}
阶乘的约数和

题目分析

ans表示答案(约数和)

(1)求n!内包含的质数个数

阶乘末尾包含0的个数,n!包含5的个数 (通过while循环求)

(2)n以内的质数有谁都求出来 (欧拉筛)

1 ∗ 2 ∗ 3 ∗ 4... ∗ n 1*2*3*4...*n 1234...n

(3)对于质数p,有m个

a n s ∗ = ( 1 − p m + 1 ) / ( 1 − p ) ans*=(1-p^{m+1})/(1-p) ans=(1pm+1)/(1p)

和阶乘约数一样,把阶乘求出来再进行分解不现实,我们只需要知道阶乘里面含有质因子的个数就可以了。一个数的阶乘里面含有某个数的个数,在题目里有遇见过,那里求的是n的阶乘含有5的个数,但是换成其他数字也是同样的道理。

首先把n以内的所有质数找出来,然后遍历n以内的所有质数,统计n!含有该质数的个数。

至于约数和,根据(2)的介绍,有
s u m = ( p 0 0 + p 0 1 + . . . + p 0 j 0 ) ∗ ( p 1 0 + p 1 1 + . . . + p 1 j 1 ) ∗ . . . ∗ ( p k 0 + p k 1 + . . . + p k j k ) sum=(p_0^0+p_0^1+...+p_0^{j_0})*(p_1^0+p_1^1+...+p_1^{j_1})*...*(p_k^0+p_k^1+...+p_k^{j_k}) sum=(p00+p01+...+p0j0)(p10+p11+...+p1j1)...(pk0+pk1+...+pkjk)
对于每一个乘积项其实是关于质数的等比数列求和,如下所示
p i j i + 1 − 1 p i − 1 \frac{p_i^{j_i+1}-1}{p_i-1} pi1piji+11
因为题目有取余操作,所以需要求 p i − 1 p_i-1 pi1的逆元。

等比数列求和公式
a 1 ∗ ( 1 − q n ) 1 − q \frac{a_1*(1-q^n)}{1-q} 1qa1(1qn)

题目代码

import java.util.Arrays;
import java.util.Scanner;

public class Main {
    static Scanner in = new Scanner(System.in);
    static int n, k, zu;
    static int N = (int) (2e5 + 10);
    static int prime[]=new int[N];
    static boolean st[]=new boolean[N];
    static int cnt=0;
    static int mod=998244353;
    static void solve() {
        n=in.nextInt();
        long ans=1;
        for(int i=0;prime[i]<=n;i++){
            long res=0;
            int x=n;
            while(x!=0){
                res=res+x/prime[i];//直接求n!里面含有prime的个数
                x/=prime[i];
            }
            ans=(ans*((ksm(prime[i],res+1,mod)-1)*ksm(prime[i]-1,mod-2,mod)%mod)+mod)%mod;
        }
        System.out.println(ans);
    }
    static void getPrime(int n) {
        cnt = 0;
        for (int i = 2; i <= n; i++) {
            if (!st[i]) prime[cnt++] = i;
            for (int j = 0; j < cnt; j++) {
                if ((long) i * prime[j] > n) break;
                st[i * prime[j]] = true;
                if (i % prime[j] == 0) break;
            }
        }
    }
    static long ksm(long a, long b, long p) {
        long res = 1;
        for (; b != 0; b >>= 1, a = a * a % p) {
            if ((b & 1) == 1) {
                res = res * a % p;
            }
        }
        return res;
    }
    public static void main(String[] args) {
        Arrays.fill(prime,0x3f3f3f3f);
        getPrime(200000);
        int t = 1;
        for (zu = 1; zu <= t; zu++) {
            solve();
        }
    }
}
#include <iostream>
#include <vector>
#include <cmath>
#include <cstring>

using namespace std;

const int N = 2e5 + 10;
const int mod = 998244353;

int n, k, zu;
int prime[N];
bool st[N];
int cnt = 0;

long long ksm(long long a, long long b, long long p) {
    long long res = 1;
    while (b != 0) {
        if (b & 1) {
            res = res * a % p;
        }
        a = a * a % p;
        b >>= 1;
    }
    return res;
}

void getPrime(int n) {
    cnt = 0;
    for (int i = 2; i <= n; i++) {
        if (!st[i]) prime[cnt++] = i;
        for (int j = 0; j < cnt; j++) {
            if ((long long)i * prime[j] > n) break;
            st[i * prime[j]] = true;
            if (i % prime[j] == 0) break;
        }
    }
}

void solve() {
    cin >> n;
    long long ans = 1;
    for (int i = 0; prime[i] <= n; i++) {
        long long res = 0;
        int x = n;
        while (x != 0) {
            res += x / prime[i];  // 直接求n!里面含有prime的个数
            x /= prime[i];
        }
        ans = (ans * ((ksm(prime[i], res + 1, mod) - 1) * ksm(prime[i] - 1, mod - 2, mod) % mod) + mod) % mod;
    }
    cout << ans << endl;
}

int main() {
    fill(prime, prime + N, 0x3f3f3f3f);
    getPrime(200000);
    int t = 1;
    for (zu = 1; zu <= t; zu++) {
        solve();
    }
    return 0;
}

素数筛

素数筛的作用是筛选出[2,N]范围内的所有素数,本次主要讲解两种方法,分别是埃氏筛和欧拉筛。证明时会提到唯一分解定理,如果不知道的小伙伴可以先去学一学,那我们开始啦!

1.埃氏筛

主要思想:当找到一个素数时,利用该素数把该素数的所有倍数筛掉。

时间复杂度: O ( n l o g ( l o g ( n ) ) ) O(nlog(log(n))) O(nlog(log(n)))

上代码,

    //每个数的最小质因子
    //pre[i]表示i的最小质因子 book[i]=1表示i不是素数,book[i]=0表示i是素数
    //pre[i]表示i的最小质因子
    book[1] = 1;//记录是否为素数,1表示不是素数
    book[0] = 1;
    for(int i =2;i<book.length;i++) {//book[i]=0怎么保证的i一定是素数,万一是合数没有被筛掉
        //i是合数,i=a*b,       
        if(book[i]==0) {// i是素数,筛掉素数的倍数 i=2 6 = 2+2+2
            pre[i]=i;
            for(int j = i+i;j<book.length&&j>0;j+=i) {
                if(book[j]==0) pre[j]=i;//j是被i的倍数筛掉的,能说明i是j的质因子,不能说明是最小质因子    
                book[j] = 1;
                
            } 
        }
    }
 // 埃氏筛法,求最小质因子
    book[0] = book[1] = 1; // 0 和 1 不是质数
    for (int i = 2; i < MAX_N; ++i) {
        if (book[i] == 0) { // 如果 i 是质数
            pre[i] = i; // i 的最小质因子是它本身
            for (int j = i + i; j < MAX_N; j += i) { // 筛掉 i 的倍数
                if (book[j] == 0) {
                    pre[j] = i;
                }
                book[j] = 1; // 标记 j 不是质数
            }
        }
    }

问题:

  1. 为什么遍历到i时,若i没有被标记为合数(也就是没有被i前面的数筛掉),则一定是素数?

  2. 为什么for循环遍历到sqrt(N)就可以了?

先自己想一想哦,提示是唯一分解定理。

答案:

  1. 还记得唯一分解定理吗?一个正整数可以用若干个质数表示,假设当前正整数是n,它可以用质数 p 1 , p 2 . . . p k p_1,p_2...p_k p1,p2...pk表示, p 1 , p 2 . . . p k p_1,p_2...p_k p1,p2...pk一定比q小。假设q是合数,那么遍历到q,q一定会被 p 1 , p 2 . . . p k p_1,p_2...p_k p1,p2...pk筛掉。如果q是质数呢?他只能写出1*q的形式,它会被自己筛掉。
  2. 其实也就是证明sqrt(N)后面的合数一定会被小于sqrt(N)的数筛掉。设 N < n < N \sqrt{N}<n<N N <n<N a ∗ b = n a*b=n ab=n,若a<b,则 a < n < N a<\sqrt{n}<\sqrt{N} a<n <N ,若a是素数,则n会被a筛掉,若a是合数,则a可以继续分解为更小的素数,而a和n都会被这个更小的素数筛掉,所以即便 N < n \sqrt{N}<n N <n,但是仅用小于 N \sqrt{N} N 的数就可以把n筛掉,所以可以遍历到sqrt(N)。

2.欧拉筛

主要思想:埃氏筛的一部分时间耗在了重复的筛某些合数,比如18会被2和3筛掉。欧拉筛保证每个合数只被筛一次,因此也保证了 O ( n ) O(n) O(n)的时间复杂度。

时间复杂度: O ( n ) O(n) O(n)

上代码,

 int count = 0;
 for (int i = 2; i < 20000005; i++) {//线性
 if (!visit[i]) {//如果i是一个质数
     prime[count++] = i;//记录当前已经找出来的所有的质数
 }
 for (int j = 0; j < count && i * prime[j] < 20000005; j++) {
     visit[i * prime[j]] = true;//用prime[j]筛掉了i * prime[j]。
     if (i % prime[j] == 0) break;//保证每个合数只被最小的质因子筛掉
  }
 }

问题:

  1. 为什么if语句满足后可以提前退出循环?
  2. 两个for循环嵌套如何实现的线性复杂度?
  3. j < count是否可以不加?

先自己想一想哦,提示是prime[j]是i的因子,你可以把式子写出来看看。

再讲答案之前先来捋一捋欧拉筛的结构,因为它不像埃氏筛那么直接。

首先一个for循环,接着如果当前的i是素数,则用另一个数组prime存一下,这个数组只存素数。

再来一个for循环,这个for循环就是用来筛合数的,遍历之前找到的所有素数,然后筛掉 p r i m e [ j ] ∗ i prime[j]*i prime[j]i。当满足if语句时,这一轮的筛合数可以提前退出了。

答案:

  1. 若此时if语句条件满足了,则prime[j]是i的因子,因此有 i = k ∗ p r i m e [ j ] i=k*prime[j] i=kprime[j]。如果此时没有退出for循环,会有 p r i m e [ j + 1 ] ∗ i prime[j+1]*i prime[j+1]i p r i m e [ j + 1 ] prime[j+1] prime[j+1]筛掉。 p r i m e [ j + 1 ] ∗ i = p r i m e [ j + 1 ] ∗ k ∗ p r i m e [ j ] = k ‘ ∗ p r i m e [ j ] prime[j+1]*i=prime[j+1]*k*prime[j]=k^`*prime[j] prime[j+1]i=prime[j+1]kprime[j]=kprime[j],这说明了什么?说明被 p r i m e [ j + 1 ] prime[j+1] prime[j+1]筛掉的 p r i m e [ j + 1 ] ∗ i prime[j+1]*i prime[j+1]i也会被 p r i m e [ j ] prime[j] prime[j]筛掉,这就重复筛了,怎么办?我们让每个数都被其最小的质因子筛掉,那么这里 p r i m e [ j ] prime[j] prime[j]就是 p r i m e [ j + 1 ] ∗ i prime[j+1]*i prime[j+1]i最小的质因子,因此j就不继续增大了,直接退出该循环。
  2. 因为保证了每个数只被筛一次,第二个for循环总共被执行n次,所有的数被筛完代码也就结束了。
  3. 可以不加,如果i是质数,则prime[j]一定会被遍历到值为i的时候,最终满足i % prime[j] == 0,然后退出for循环。如果i是合数,则i可以被分解为多个质数相乘,这些质数一定被存在了prime数组里面,当遍历到某个质因子时就会满足i % prime[j] == 0,然后退出for循环。

证明:p[j]是i*p[j]的最小质因子

  1. 如果i是素数,则i*p[j]里i和p[j]都是质数,又因为p[j]<i,则p[j]就是i*p[j]的最小质因子
  2. 如果i不是素数,则i可以被分解为多个质数相乘的形式,假设i=a*b,假设其中a<p[j],则a是i*p[j]的最小质因子,但是如果a是小于p[j]的质数,在for循环里面,a会比p[j]先遍历到,并且满足i%p[j]==0,此时for循环就退出了,不会遍历到p[j],所以a或者b都大于p[j](也可以说i分解出来的所有质因子都比p[j]大),则p[j]一定是i*p[j]的最小质因子。

利用欧拉筛求约数个数和约数和

[线性筛 约数个数/约数和]_线性筛约数和-CSDN博客

例题

埃氏筛——最小质因子之和

参考代码:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StreamTokenizer;
import java.util.Scanner;

public class 最小质因子之和Easy {
public static void main(String[] args) throws IOException{
    //进行预处理
    f();//求2-n每个数对应的最小质因子
    sum();//求前缀和数组
    StreamTokenizer sc = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in))); 
    Scanner scanner = new Scanner(System.in);
    sc.nextToken();
    int t = (int)sc.nval;
    while(t-- >0) {
        sc.nextToken();
        int n = (int)sc.nval;
        System.out.println(res[n]);
    }
}
static int book[] = new int[4000000];
static int pre[] = new  int[4000000];
private static void f() {//埃氏筛模板
    //每个数的最小质因子
    //pre[i]表示i的最小质因子
    book[1] = 1;//记录是否为素数,1表示不是素数
    book[0] = 1;
    for(int i =2;i<book.length;i++) {
        if(book[i]==0) {// i是素数,筛掉素数的倍数 i=2 6 = 2+2+2
            pre[i] = i;//求的是质数的最小质因子
            for(int j = i+i;j<book.length&&j>0;j+=i) {
                if(book[j]==0) {
                    pre[j] =i;
                }
                book[j] = 1;
            }
            
        }
    }
    
}
static long res[] = new long[4000000];
private static void sum() {
    //一次求出i 2- n
    // 2-i的最小质因子之和,前缀和数组可以在O(n)
    for(int i=2;i<res.length;i++) {
        res[i] = res[i-1]+pre[i];
    }
}
}
#include <iostream>
#include <vector>
using namespace std;

const int MAX_N = 4000000;

int book[MAX_N]; // book[i] = 0 表示 i 是质数,1 表示 i 不是质数
int pre[MAX_N];  // pre[i] 表示 i 的最小质因子
long long res[MAX_N]; // res[i] 表示 2 到 i 的最小质因子之和

void f() {
    // 埃氏筛法,求最小质因子
    book[0] = book[1] = 1; // 0 和 1 不是质数
    for (int i = 2; i < MAX_N; ++i) {
        if (book[i] == 0) { // 如果 i 是质数
            pre[i] = i; // i 的最小质因子是它本身
            for (int j = i + i; j < MAX_N; j += i) { // 筛掉 i 的倍数
                if (book[j] == 0) {
                    pre[j] = i;
                }
                book[j] = 1; // 标记 j 不是质数
            }
        }
    }
}

void sum() {
    // 计算前缀和,res[i] 表示 2 到 i 的最小质因子之和
    for (int i = 2; i < MAX_N; ++i) {
        res[i] = res[i - 1] + pre[i];
    }
}

int main() {
    // 进行预处理
    f();
    sum();
    
    int t;
    cin >> t;  // 输入测试用例数
    
    while (t--) {
        int n;
        cin >> n; // 输入 n
        cout << res[n] << endl; // 输出结果
    }
    
    return 0;
}
欧拉筛——最小质因子之和困难版

参考代码:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class 最小质因子之和Hard {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int[] prime = new int[20000005];
        int[] f = new int[20000005];
        boolean[] visit = new boolean[20000005];
        int count = 0;
        for (int i = 2; i < 20000005; i++) {//线性
            if (!visit[i]) {//如果i是一个质数
                prime[count++] = i;
                f[i] = i;
            }
            for (int j = 0; j < count && i * prime[j] < 20000005; j++) {
                visit[i * prime[j]] = true;
                f[i * prime[j]] = prime[j];
                if (i % prime[j] == 0) break;
            }
        }
        long[] sum = new long[20000005];//前缀和数组
        for (int i = 2; i < f.length; i++) {
        	//System.out.println(f[i]);
            sum[i] = sum[i - 1] + f[i];
        }
        int t = Integer.parseInt(br.readLine());
        while (t-- > 0) {
            int n = Integer.parseInt(br.readLine());
            System.out.println(sum[n]);
        }
    }
}
#include <iostream>
#include <vector>
using namespace std;

const int MAX_N = 20000005;

int prime[MAX_N];   // 存储质数
int f[MAX_N];       // 存储每个数的最小质因子
bool visit[MAX_N]; // 用于标记已访问的数
long long sum[MAX_N]; // 存储前缀和

void sieve() {
    int count = 0;
    for (int i = 2; i < MAX_N; i++) {
        if (!visit[i]) { // 如果 i 是质数
            prime[count++] = i;
            f[i] = i;
        }
        for (int j = 0; j < count && i * prime[j] < MAX_N; j++) {
            visit[i * prime[j]] = true;
            f[i * prime[j]] = prime[j];
            if (i % prime[j] == 0) break;
        }
    }
}

void compute_prefix_sum() {
    for (int i = 2; i < MAX_N; i++) {
        sum[i] = sum[i - 1] + f[i]; // 计算前缀和
    }
}

int main() {
    // 执行预处理
    sieve();
    compute_prefix_sum();
    int t;
    cin >> t; // 输入测试用例数
    while (t--) {
        int n;
        cin >> n; // 输入 n
        cout << sum[n] << endl; // 输出前缀和结果
    }

    return 0;
}

快速幂与快速乘

快速幂原理

以倍增的形式快速扩大,例如3的5次方,普通的求法会是 3 ∗ 3 ∗ 3 ∗ 3 ∗ 3 3*3*3*3*3 33333,那么快速幂是如何求的? 3 4 ∗ 3 1 3^4*3^1 3431,那么他是怎么来的呢?5的二进制为0101,这里的右边第一个1就是 3 1 3^1 31,第二个1就是 3 4 3^4 34,如下所示。那么我们这样做能够表示所有的数吗?也就是能够把所有的幂次都表示了吗?这里的1,2,4,8,16其实可以看成二进制里的 2 0 , 2 1 , 2 2 , 2 3 , 2 4 2^0,2^1,2^2,2^3,2^4 20,21,22,23,24,二进制可以表示所有的数字,那么这里也同理。
3 16 , 3 8 , 3 4 , 3 2 , 3 1 3^{16}, 3^{8},3^{4},3^{2},3^{1} 316,38,34,32,31

16 : 2 4 , 8 : 2 3 , 4 : 2 2 , 2 : 2 1 , 1 : 2 0 16:2^4,8:2^3,4:2^2,2:2^1,1:2^0 16:24,8:23,4:22,2:21,1:20

3 16 , 3 8 , 3 4 , 3 2 , 3 1 3^{16}, 3^{8},3^{4},3^{2},3^{1} 316,38,34,32,31,是怎么求的?每次自己乘以自己就可以了。假设我要求a的p次幂,则每次查看p&1是否等于1,如果等于则将当前的权值累计到答案中,否则的话就不累计,并且p要右移一位。

快速幂模板代码

static int fastPower(long a, long b) {
        long res = 1;//存快速幂的结果  3^5   a=3 res*=3;a=9,5->2(0101->010);a=81(3^4),1(01),res*=3^4;a=3^8,b=0
    //res=3^1*3^4
        while (b != 0) {//a的b次方 对 mod 取模
            if ((b & 1) == 1) {//每次查看p&1是否等于1,如果等于则将当前的权值累计到答案中
                res = (res * a);
            }
            a = a * a;//每次自己乘以自己
            b >>= 1;//p要右移一位
        }
        return res;
    }

快速幂模板题目

快速幂

#include<bits/stdc++.h>
using namespace std;
long long b,a,p,k,ans=1,c;
int main()
{
//    scanf("%d%d%d",&b,&p,&k);
    cin>>b>>p>>k;
    a=b;c=p;
    while(p>0)//快速幂
    {
        if(p%2!=0)
            ans=ans*b%k;//如果p为单数,乘到ans里面去,然后取模
        b=b*b%k;//每次运算都取模
        p=p>>1;    //用位运算除2,可能会快一点
    }
    ans %= k;
//    printf("%d^%d mod %d=%d",a,c,k,ans);//输出
    cout<<a<<"^"<<c<<" mod "<<k<<"="<<ans;
    return 0;
}

矩阵快速幂模板代码

原理是一样的,只不过换成矩阵了,要用到矩阵乘法。

static class mat{
		public mat(int i, int j, int k, int l) {
			// TODO Auto-generated constructor stub
			this.m[0][0]=i;
			this.m[0][1]=j;
			this.m[1][0]=k;
			this.m[1][1]=l;
		}
		public void putall(mat a) {
			this.m[0][0]=a.m[0][0];
			this.m[0][1]=a.m[0][1];
			this.m[1][0]=a.m[1][0];
			this.m[1][1]=a.m[1][1];
		}
		public mat() {
			// TODO Auto-generated constructor stub
		}
		long m[][] = new long[2][2];
	}
	static mat E=new mat(1,0,0,1);
static mat mul(mat a,mat b,long mod){
	    mat c = new mat();
	    int i,j,k;
	    for(i=0;i<2;++i){
	        for(j=0;j<2;++j){
	            c.m[i][j]=0;
	            for(k=0;k<2;++k){
	                    c.m[i][j]+=a.m[i][k]*b.m[k][j];
	                    c.m[i][j]%=mod;
	            }
	        }
	    }
	    return c;
	}
static long f(long n,long mod){
	if(n<3)return 1;
	mat r=qpow(B,n-1,mod);
	long res=(r.m[0][0]+r.m[1][0]);
	return res;   
}

快速乘

原因:计算机中的加法运算会比乘法快得多,且做乘法运算往往会溢出。快速乘能高效完成乘法运算且不会溢出。

快速乘与快速幂原理相似,也是将运算转换为二进制处理。

如x*13,13的二进制是 1101 1101 1101,那么可以写成 x ∗ ( 2 0 + 2 2 + 2 3 ) x * (2^0+2^2+2^3) x(20+22+23)的形式,对比一下快速幂,如果是 x 13 x^{13} x13次方应该写成什么形式呢?应该是 x 2 0 ∗ x 2 2 ∗ x 2 3 x^{2^0}*x^{2^2}*x^{2^3} x20x22x23,可以看到快速幂和快速乘是非常相似的,只不过快速幂 2 i 2^i 2i是在幂次上,快速乘是在乘积上,那就来看快速乘的代码吧。

static long mm(long x,long y){
	    if(x>y){ 
	        long t=x;
	        x=y;
	        y=t; 
	    }
	    long r=0;
	    while(y>0){
	        if((y&1)==1){
	            r=(r+x); 
	        }
	        x=(x*2);
	        y>>=1;
	    }
	    return r;
	}

扩展欧几里得定理

欧几里得与扩展欧几里得算法解析-CSDN博客

首先除了了解过欧几里得算法,还要知道裴属定理:

  • ax+by=gcd(a , b)

即如果a、b是整数,那么一定存在整数x、y使得ax+by=gcd(a,b)。

换句话说,如果ax+by=m有解,那么m一定是gcd(a,b)的若干倍。(可以来判断一个这样的式子有没有解)

// 求x, y,使得ax + by = gcd(a, b)
int exgcd(int a, int b, int &x, int &y)
{
    if (!b)
    {
        x = 1; y = 0;
        return a;   //到达递归边界开始向上一层返回
    }
    int d = exgcd(b, a % b, x, y);
    int temp=y;    //推出这一层的x,y
    y=x-(a/b)*y;
    x=temp;
    return d;
}

首先当递归到达边界时,余数b==0,除数a就是最大公约数,此时可以得出来方程的一组解

x=1,y=0; ——> a*1+b*0=gcd(a,b);

注意在递归中永远都是先得到上一层的结果状态

假设栈中当前层得到的解是x1,y1;那么 就要求下一层的结果状态了;代入方程得:

b*x1 + (a%b)*y1 = gcd(a,b) 

—》b*x1+(a-(a/b)*b)*y1 = gcd(a,b)

—》a*y1+ b*(x1-(a/b)*y1) = gcd(a,b)

显而易见由上一层的解 x1,y1,可以推出下一层x,y的状态

x=y1 y= x1-(a/b)*y1

于是相邻的两个层之间的状态关系就出来了,ey~

高精度运算

高精度加法

大整数加法

题目分析:

题目代码:

#include<bits/stdc++.h>
using namespace std;
string a,b,c;
vector<int> add(vector<int>A,vector<int>B){
	if(A.size()<B.size())return add(B,A);
	int t=0;
	vector<int>C;
	for(int i=0;i<A.size();i++){
		t+=A[i];
		if(i<B.size())t+=B[i];
		C.push_back(t%10);
		t/=10;
	}
	if(t)C.push_back(t);
	return C;
}
int main (){
	int n=1;
	//cin>>n;
	for(int i=0;i<n;i++){
		vector<int>A,B;
		cin>>a>>b;
		for(int i=a.size()-1;i>=0;i--){
			A.push_back(a[i]-'0');
		}
		for(int i=b.size()-1;i>=0;i--){
			B.push_back(b[i]-'0');
		}
		vector<int>C=add(A,B);
		for(int i=C.size()-1;i>=0;i--){
		cout << C[i];
		}
        	cout<<endl;
	}	
	return 0;
}

字符数组写法

#include <iostream>
#include<cstring>
using namespace std;
const int maxn = 100;
char a1[maxn],b1[maxn];
int a[maxn],b[maxn],c[maxn];
int main() {
    cin>>a1;
    cin>>b1;
    int lena = strlen(a1);
    int lenb = strlen(b1);
    for(int i=0,j=lena-1;i<lena;i++,j--)
       a[j]=a1[i]-'0';
    for(int i=0,j=lenb-1;i<lenb;i++,j--)
	   b[j]=b1[i]-'0';
	int lenc = lena>lenb?lena:lenb;
	for(int i = 0;i < lenc;i++){
		c[i]+=a[i]+b[i];
		if(c[i] >= 10){
			c[i+1]=c[i]/10;
			c[i]=c[i]%10;
		}
	}
	if(c[lenc]>0) lenc++;
	for(int i = lenc-1;i>=0;i--)
	  cout<<c[i];
    return 0;
}

高精度减法

大整数减法

题目分析:

题目代码:

#include <bits/stdc++.h>
using namespace std;
typedef vector<int> vi;
#define pb push_back
vi sub(vi a,vi b){
	vi res;
	int t=0;
	for (int i=0;i<a.size();i++){
		t+=a[i];
		if (i<b.size()) t-=b[i];
		res.pb((t+10)%10);
		if (t<0) t=-1;
		else t=0;
	}
	while (res.size()>1 && res.back()==0) res.pop_back();
	return res;
}
int main(){
	int n=1;
	string q[51],p[51];
	//cin>>n;
	for (int i=1;i<=n;i++) cin>>q[i]>>p[i];
	for (int j=1;j<=n;j++){
		vi a,b;
		if (q[j].size()<p[j].size() || q[j].size()==p[j].size() and q[j]<p[j]){
			cout<<"-";
			swap(q[j],p[j]);
		}
		for (int i=q[j].size()-1;i>=0;i--) a.pb(q[j][i]-48);
		for (int i=p[j].size()-1;i>=0;i--) b.pb(p[j][i]-48);
		auto c=sub(a,b);
		for (int i=c.size()-1;i>=0;i--) cout<<c[i];
		cout<<endl;
	}
	return 0;	
}

高精度乘

高精度乘法

题目分析:

题目代码:

#include<bits/stdc++.h>
using namespace std;
vector<int> a,b;
string x,y;
vector<int> mul(vector<int> a,vector<int> b){
    vector<int> res;
    for(int i = 0;i < a.size();i++){
        for(int j = 0;j < b.size();j++){
            if(i+j<res.size()) res[i+j]+=a[i]*b[j];
            else res.push_back(a[i]*b[j]);
        }
    }
    //处理进位
    int t = 0;
    for(int i = 0;i < res.size();i++){
        t += res[i];//这个t里面可能携带进位
        res[i]=t%10;//留下的结果
        t /= 10;//计算当前位的进位
    }
    //特殊处理最后一位的进位
    while(t){
        res.push_back(t%10);
        t/=10;
    }
    //处理前导0
    while(res.size()>1&&res.back()==0) res.pop_back();
    //返回结果
    return res;
}
int main(){
	cin>>x>>y;
	for(int i = x.size()-1;i>=0;i--) a.push_back(x[i]-'0');
	for(int i = y.size()-1;i>=0;i--) b.push_back(y[i]-'0');
	vector<int> c = mul(a,b);
	for (int i=c.size()-1;i>=0;i--) cout<<c[i];
	return 0;
}

#include <bits/stdc++.h>
using namespace std;
int res[202];
void m(int a[],int b[],int &j){
	for(int i = 0;i < 100;i++){
		for(int j = 0;j < 100;j++){
			res[i+j] += a[i] * b[j];
		}
	}
	int t = 0;
	for(int i = 0;i <= j;i++){
		t += res[i];
		res[i] = t % 10;
		t /= 10;
	}
	while(t){
		res[++j] = t % 10;
		t /= 10;
	}
	while(j > 0 && res[j] == 0){
		j--;
	}
	return ;
}
int main(){
	char x[101],y[101];
	cin >> x >> y;
	int j = strlen(x)+strlen(y)-2;//计算错误
	int a[101]={0},b[101]={0};//初始化错误
	int k = 0;
	for(int i = strlen(x)-1;i >= 0;i--){
		a[k++] = x[i] - '0';
		//cout<<x[i]-'0'<<" ";
	}
	//cout<<endl;
	k = 0;
	for(int i = strlen(y)-1;i >= 0;i--){
		b[k++] = y[i] - '0';
		//cout<<y[i]-'0'<<" ";
	}
	//cout<<endl;
	m(a,b,j);//未调用函数
	for(int i = j;i >= 0;i--){
		cout << res[i];
	}
	return 0;
}

高精度除

#include<bits/stdc++.h>
using namespace std;
vector<int> a;
string x;
long long b,mod;
vector<int> div(vector<int> a,long long b){
    vector<int> res;
    long long r = 0;
    for(int i = 0;i < a.size();i++){
        r=r*10+a[i];
        if(r>=b)
            res.push_back(r/b),r%=b;
        else res.push_back(0);
    }
    mod = r;
    reverse(res.begin(),res.end());
    while(res.size()>1&&res.back()==0) res.pop_back();
    reverse(res.begin(),res.end());
    return res;
}
int main(){
	cin>>x>>b;
	for(int i = 0;i<x.size();i++) a.push_back(x[i]-'0');

	auto c = div(a,b);
	for(int i = 0;i<c.size();i++) cout<<c[i];
    cout<<endl;
	cout<<mod;
	return 0;
}

高精度除法2(余数高精度)

#include<iostream>
using namespace std;
int main(){
	int a, b, n;
	cin >> a >> b >> n;
	cout << a / b; // 先计算整数部分。 
	cout <<"."; // 输出小数点
	// 计算小数部分 
	for(int i = 0;i < n; i++){
		int c = a % b; //余数
		a = c * 10;      //余数乘以10 得到当前位置的被除数
		cout << a / b; //当前位置的商
	}
	 
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值