算法基础(数学知识篇)

第四章:数学知识:

4.1 试除法判定质数:试除法判定质数

算法思想:

判定一个数n是否是质数,主要判定2 ~ sqrt(n)之间是否有其约数,并且,所有小于2的数不是质数。

代码实现:
public static boolean isPrime(int x) {
    if (x < 2) return false;
    for (int i = 2; i <= x / i; i ++){ // 由于sqrt()比较慢,且i * i 可能爆int,所以采用此种写法
        if (x % i == 0)
            return false;
    }
    return true;
}

4.2 试除法分解质因数:试除法分解质因数

算法思想:
  • 想要对一个数x分解质因数,主要是通过从小到大枚举其约数,当枚举到一个约数i时,通过一个循环,将x反复更新为x = x / i,并记录更新了多少次,这个次数就是i这个质因子的指数。
  • 当上述过程结束后,需要判定一下最后的x是否被除成了1,如果不是,则说明被除后的这个x也是最开始的x的一个质因子,且该质因子不能再被分解。
代码实现:
public static void devide(int x) {
    for (int i = 2; i <= x / i; i ++){
        if (x % i == 0){ // 如果i是x的约数
            int cnt = 0;
            while (x % i == 0){ // 求出i的指数cnt,并且更新x
                x /= i;
                cnt ++;
            }
            System.out.println(i + " " + cnt); // 输出这个质因子和它的指数
        }
    }
    if (x > 1) System.out.println(x + " " + 1);
}

4.3 筛质数:筛质数

4.3.1 朴素筛法:
算法思想:
  • 筛质数的目的在于求出1 ~ n之间的质数。那么比较快速的办法就是将1 ~ n当中不是质数的数给筛出去。
  • 朴素筛法是利用已经确定了的质数进行筛除,其原理是将一个质数的所有小于等于n的倍数全部筛除
代码实现:
int cnt = 0; // 记录质数的个数
int[] primes = new int[n]; // 存储所有的质数
boolean[] st = new boolean[n + 10]; // st表示当前数是否被筛掉
for (int i = 2; i <= n; i ++) { //从2开始枚举每个数
    if (st[i]) continue; // 如果当前的数已经被筛掉,则跳过该次循环
    primes[cnt ++] = i; // 否则将其加到质数数组中,同时cnt ++
    for (int j = i + i; j <= n; j += i) { // 用当前质数筛掉其所有的倍数(如2i,3i都被筛掉)
        st[j] = true;
    }
}
4.3.2 线性筛法:
算法思想:
  • 线性筛法可以理解为是对朴素筛法的优化,因为朴素筛法里会多次筛除同一个数,而线性筛法中,每个合数只会被筛掉一次。
  • 其主要思想是通过每个合数的最小质因子将其筛掉。
代码实现:
int cnt = 0;
int[] primes = new int[n];
boolean[] st = new boolean[n + 10];
for (int i = 2; i <= n; i ++) {
    //if (st[i]) continue; 此处不能continue,因为还需要用i来筛掉后面的合数
    if (!st[i]) primes[cnt ++] = i;
    for (int j = 0; primes[j] <= n / i; j ++){ //每次筛掉的数为primes[j] * i,所以需要primes[j] * i <= n
        st[primes[j] * i] = true;
        if (i % primes[j] == 0) break; // 保证primes[j]为primes[j] * i的最小质因子
    }
}

4.4 试除法求约数:试除法求约数

算法思想:

与求质数差不多,从0 开始枚举到sqrt(x),对每个数i进行判断,如果是x的约数,且i != x / i,则将ix / i放进答案中,最后排序输出即可。

代码实现:
import java.util.*;
import java.io.*;

public class Main {
    public static void main(String[] args) throws IOException{
        Scanner sc = new Scanner(System.in);
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
        int n = sc.nextInt(); //求n个数的每个约数
        while (n -- > 0) {
            ArrayList<Integer> devisors = new ArrayList(); // 容器,用于存放答案
            int x = sc.nextInt();// 读入x
            for (int i = 1; i <= x / i; i ++) {
                if (x % i == 0) {
                    devisors.add(i);
                    if (x / i != i) devisors.add(x / i);
                }
            }
            Collections.sort(devisors); // 对容器中的数按从小到大排序
            for (int a : devisors){ // 遍历容器
                bw.write(a + " ");
            }
            bw.write("\n");
            bw.flush();
        }
    }
}

4.5 约数个数:约数个数

算法思想:
  • 该算法求解的是一个数x的所有2 ~ x - 1中的约数个数
  • 该算法基于约数个数公式(a₁ + 1) * (a₂ + 1) * … * (ak + 1),其中a1x的第一个质因子的指数,a₂x的第二个质因子的指数,依次类推。
代码实现:
import java.util.*;

// 求n个数的乘积,再求这个数的约数个数
public class Main {
    static final int mod = (int)1e9 + 7;
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        HashMap<Integer, Integer> primes = new HashMap<>();
        while (n -- > 0) {
            int x = sc.nextInt();
            for (int i = 2; i <= x / i; i ++){  // 想求这n个数的乘积的约数个数,可以对每个数分解质因数,再求积,就等同于求乘积后再分解质因数
                while (x % i == 0) {
                    x /= i; //更新x,此处同分解质因数
                    primes.put(i, primes.getOrDefault(i, 0) + 1); //更新每个质因数的指数,其中primes.getOrDefault(i, 0)表示如果primes.get(i)不存在则对其赋初值0,再get
                }
            }
            if (x > 1) {
                primes.put(x, primes.getOrDefault(x, 0) + 1);
            }
        }
        long res = 1;
        for (int value : primes.values()) { // 遍历primes的所有value
            res = res * (value + 1) % mod; //公式
        }
        System.out.println(res);
    }
}

4.6 约数之和:约数之和

算法思想:
  • 4.5节是求一个数的约数个数,而4.6则是求这些约数的和。
  • 该算法同样基于公式,(p1^0 + p1^1 + … + p1^a1) * … * (pk^0 + pk^1 + … + pk^ak)。其中,p1是第一个质因子,a1是第一个质因子的指数。
  • 上式中,可以利用t = t * p + 1求解每一项,例如:t1 = 1t1 = p + 1t2 = p^2 + p + 1,ta = p^a1 + …… + 1
代码实现:
import java.util.*;

// 求n个数的乘积,再求这个数的所有约数之和
public class Main {
    static final int mod = (int)1e9 + 7;
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        HashMap<Integer, Integer> primes = new HashMap<>();
        while (n -- > 0) {
            int x = sc.nextInt();
            for (int i = 2; i <= x / i; i ++){
                while (x % i == 0) {
                    x /= i; // 同4.5节
                    primes.put(i, primes.getOrDefault(i, 0) + 1);
                }
            }
            if (x > 1) {
                primes.put(x, primes.getOrDefault(x, 0) + 1);
            }
        }
        long res = 1;
        for (Map.Entry prime : primes.entrySet()) { // 遍历容器中的每一项
            long t = 1;
            int key = (int)prime.getKey(), value = (int)prime.getValue();
            while (value -- > 0) t = (t * key + 1) % mod; // 求解公式的每一项
            res = res * t % mod; // 将公式的每一项相乘
        }
        System.out.println(res);
    }
}
补充map遍历方式:
for (int key : map.keySet()) { } // 遍历每项key
for (int value : map.values()) { } // 遍历每项value
for (Map.Entry<Integer, Integer> p : entrySet()) { } // 遍历map的每个对象

4.7 最大公约数:最大公约数

算法思想:

如果一个数d能整除a,且能整除b,那么d一定能整除c1 * a + c2 * b。所以d也能够整除a - c * b,令c = (a / b)向下取整,则a - c * b = a mod b,所以d也能整除a mod b,故a, b两个数的最大公约数等于b, a mod b这两个数的最大公约数。这就是欧几里得算法的核心之处。

代码实现:
public static int gcd(int a, int b) {
	return b > 0 ? gcd(b, a % b) : a; // 如果b等于0,那么最大公约数就是a,否则就是gcd(b, a % b)
}

4.8 欧拉函数:欧拉函数

算法思想:
  • 欧拉函数就是指:对于一个正整数x,小于或等于x的正整数中与x互质的正整数个数(包括1)的个数,记作 `φ ( n ) 。

  • 欧拉函数的公式推导大致为:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jcLX473j-1673159657623)(E:\学习类\md文件\md插图\欧拉公式.png)]

其中Pix的每一个质因子。上述公式的除法均为整除(即下取整),上述公式的思想为:1 ~ x中总共有x个数,减去与x有相同质因子的数后,剩下的数均与x互质,而每次会重复减去相同的数,所以再加回来(容斥原理)。

代码实现:
public static int getEuler(int x) {
    int res = x;
    for (int i = 2; i <= x / i; i ++){
        if (x % i == 0) {
            res = res - res / i; // 公式,这种写法是避免出现小数,等价于res = res *(1 - 1 / i);
            while (x % i == 0) x /= i; // 把i除干净
        }
    }
    if (x > 1) res = res - res / x;
    return res;
}

4.9 筛法求欧拉函数:筛法求欧拉函数

算法思想:

主要思想还是基于4.8中的公式,此算法适用于题目要求求解1 ~ x中的每一个数的欧拉函数值。

代码实现:
static final int N = 1000010;
static int[] primes = new int[N];
static boolean[] st = new boolean[N];
static int[] phi = new int[N]; // 用于存放每个数的欧拉函数值
public static void getEuler(int x) {
    int cnt = 0;
    phi[1] = 1; // 对于phi[],由于其实际意义,i从1开始
    for (int i = 2; i <= x; i ++) {
        if (!st[i]) {
            primes[cnt ++] = i;
            phi[i] = i - 1;
        }
        for (int j = 0; primes[j] <= x / i; j ++){
            st[primes[j] * i] = true;
            if (i % primes[j] == 0){
                phi[i * primes[j]] = primes[j] * phi[i]; // 公式1
                break;
            }
            // 不需要else,因为如果执行了if,就会break
            phi[i * primes[j]] = (primes[j] - 1) * phi[i]; // 公式2
        }
    }
}
公式推导:
  • 公式1: 由于此时i % primes[j] == 0,说明primes[j]i的最小质因子,则在计算phi[i * primes[j]]时,(1 - 1 / primes[j])已经在求解phi[i]时被乘过一次,所以此时不需要乘这一项。

在这里插入图片描述

  • 公式2: 由于此时i % primes[j] != 0,说明primes[j]不是i的质因子,则在计算phi[i * primes[j]]时,需要乘(1 - 1 / primes[j])这一项,化简即可。

在这里插入图片描述

4.10 快速幂:快速幂

算法思想:
  • 快速幂可以快速求解a^b % p的结果。a^bjava中虽然有方法pow可以使用,但是在计算过程中很容易就爆long,而快速幂计算的每一步都mod p,一般就不会爆long

  • 其思想为先预处理出a^(2^0), a^(2^1),… , a^(2^log(k))的结果,这些数每一个都是前一个的平方。这一步显然是log(b)复杂度的。

  • 再将a^b分解为若干个前面预处理出来的值相乘,即将b分解为前面预处理出来的值的指数相加,这一步可以使用二进制进行计算,例如:十进制中的a^55的二进制的101,则5可以写为2^0 + 2^2 ,那么a^5就被分解为a^(2^0) * a^(2^2),此时就可以用预处理出来的值相乘得到。而这一步显然也是log(b)的,因此时间复杂度为log(b)

代码实现:
// 该模板相当精妙,在每次while循环时,算出a^(2^i),同时判断这一个预处理出来的值需不需要乘进去,并达到了更新a和b的效果
public static int qmi(int a, int b, int p) {
    int res = 1 % p; // 防止p=1,当p=1时,答案为0
    while(b > 0) {
        if ((b & 1) == 1) res = (int)((long)res * a % p); // (b & 1)要加括号,否则&会被当作逻辑运算符
        a = (int)((long)a * a % p); // 将a更新为a^2(java为强类型语言,比c++严格,必须最后手动强转为int再复制给a)
        b >>= 1; // 删除b的二进制中最后一位数
    }
    return res;
}

4.11 快速幂求逆元:快速幂求逆元

算法思想:
  • 乘法逆元定义:若整数b, m互质,并且对于任意的整数a,如果满足b|a,则存在一个整数x,使得a/b ≡ a × x (mod m),则称xb的模m乘法逆元,记作b⁻¹ (mod m)b存在乘法逆元的充分必要条件是b与模数m互质。

在这里插入图片描述

  • bm的倍数时,很明显,b % m = 0,不存在逆元;当b不是m的倍数时,b的逆元为b^(m-2) % m
代码实现:
public static int qmi(int a, int b, int p) {
    int res = 1 % p; // 防止p=1
    while (b != 0) {
        if ((b & 1) == 1) res = (int)((long)res * a % p);
        a = (int)((long)a * a % p);
        b >>= 1;
    }
    return res;
}

int a = sc.nextInt(), p = sc.nextInt();
int res = qmi(a, p - 2, p);
if (b % m != 0) System.out.println(res);
else System.out.println("impossible");

4.12 扩展欧几里得算法:扩展欧几里得算法

算法思想:
  • 想了解扩展欧几里得算法,先引入**裴属定理:**若 a, b 是整数,且 gcd(a , b) = d ,那么对于任意的整数x, y , ax + by都一定是d的倍数,特别地,一定存在整数 x, y,使ax + by = d成立。而扩展欧几里得算法则可以很方便的求解任意正整数a, bx, y这两个系数。即通过函数exgcd(a, b, x, y)求得系数x, y。值得注意:x, y并不唯一。

  • 由欧几里得算法知,gcd(a, b) = gcd(b, a % b),而a % b = a - a / b * b ,那么在递归求gcd(b, a % b, y, x)时有by + (a % b)x = d,化简得ax + (y - a / b * x)b = d,说明在递归时系数x不用更新(这里的x是指exgcd函数里的x,因为在每次进行递归时,会将实参交换后再复制给形参),只需要更新y

  • java中没有类似C ++的引用类型,可以用数组进行代替

代码实现:
static int[] x = new int[1];
static int[] y = new int[1];
// 形式上是把gcd拆开写
public static int exgcd(int a, int b, int[] x, int[] y) {
    if (b == 0) {
        x[0] = 1;
        y[0] = 0;
        return a;
    }
    int d = exgcd(b, a % b, y, x);
    y[0] -= a / b * x[0]; // 核心,更新系数,这里实际上不只是更新y,只是变量名统一为y了,实际上是交替更新x, y
    return d;
}

4.13 线性同余方程:线性同余方程

算法思想:
  • 可以通过扩展欧几里得算法求解线性同余方程ax ≡ b (mod m)。从取模的定义出发,可以根据ax ≡ b (mod m)构造出ax = my' + b,令y = -y',整理得ax + my = b,当ba, m的最小公倍数的倍数时,可以利用扩展欧几里得算法进行求解,而当b不是其倍数时,则无解。

  • 当用扩展欧几里得求出一组x0, y0后,此时的x0, y0满足的是ax0 + my0 = gcd(a, m),此时,我们将等式两边同时乘以b / gcd(a, m),得到ax0(b / gcd(a, m)) + my0(b / gcd(a, m)) = b,令x = x0(b / gcd(a, m))y = y0(b / gcd(a, m)),则此时的x, y即为原线性同余方程的一组解。

代码实现:
static int[] x = new int[1];
static int[] y = new int[1];
public static int exgcd(int a, int b, int[] x, int[] y) {
    if (b == 0) {
        x[0] = 1;
        y[0] = 0;
        return a;
    }
    int d = exgcd(b, a % b, y, x);
    y[0] -= a / b * x[0];
    return d;
}

int d = exgcd(a, m, x, y);
if (b % d != 0) bw.write("impossible\n"); // 若b不为d的倍数,则原线性同余方程无解
else bw.write((long)x[0] * (b / d) % m + "\n"); // mod m是为了将其转换为int范围内的解
bw.flush();

4.14 高斯消元(O(n³)):高斯消元解线性方程组

算法思想:

高斯消元的原理是将线性方程组的增广矩阵进行初等行变换,使之成为上三角矩阵,再通过上三角矩阵倒着解出未知数。

步骤如下:

  • 依次遍历每一列c;
  • 找到这一列中绝对值最大的元素行号,若最大元素为0,则不需要处理此列;
  • 将其交换到第r行(r0开始);
  • 将第r行第c列元素化为1(初等行变换);
  • 最后通过上三角矩阵判断解的情况。
代码实现:
import java.util.*;
import java.io.*;

public class Main {
    static final int N = 110;
    static int n;
    static double[][] a = new double[N][N];
    static final double eps = 1e-6; // 精度
    public static void swap(int x1, int y1, int x2, int y2) { // 便于交换数组中的元素
        double t = a[x1][y1];
        a[x1][y1] = a[x2][y2];
        a[x2][y2] = t;
    }
    public static int gauss() {
        int c, r; // col row
        for (c = 0, r = 0; c < n; c ++) {
            int t = r; // 标记当前列中最大元素行号
            for (int i = r; i < n; i ++)  // 找到最大元素行号
                if (Math.abs(a[i][c]) > Math.abs(a[t][c])) 
                    t = i;
            
            if(Math.abs(a[t][c]) < eps) continue; // 若最大的一个元素为0,则该列不需要再进行处理
            
            for (int i = c; i <= n; i ++)  swap(t, i, r, i);// 将找到的这行换到第r行,从第c列开始(c列之前的全为零且能与之交换的也是0)
            
            for (int i = n; i >= c; i --) a[r][i] /= a[r][c]; // 将这一行所有的数除以这一行的第c个数(将第c个数化为1)
            
            for (int i = r + 1; i < n; i ++) { // 将该列在该行一下的元素化为0,初等行变换(某一行减去某一行的若干倍)
                if (Math.abs(a[i][c]) > eps) // 若该列r行一下有元素本身是0,则元素所在行不需要进行处理
                for (int j  = n; j >= c; j --)  
                    a[i][j] -= a[r][j] * a[i][c];// 初等行变换
            }
            
            r ++; // 此处r也可以理解为增广矩阵的秩,可以通过秩判定唯一解/无穷多解/无解
        }
        
        if (r < n) {
            for (int i = r; i < n; i ++){
                if (Math.abs(a[i][n]) > eps) return 2; // 如果系数为0,但多项式和不为0,则说明无解
            }
            return 1; // 否则为无穷多解
        }
        else { // 唯一解的情况
            for (int i = n - 2; i >= 0; i --) { // 从倒数第二行往上解方程
                for (int j = i + 1; j < n; j ++) // i为行数,j为列数
                    a[i][n] -= a[i][j] * a[j][n]; // 系数乘以未知数的值(值在该行的下一行(第j行)已经解出,并存放在a[][n])
            }
            return 0;
        }
    }
    
    public static void main(String[] args) throws IOException{
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        n = Integer.parseInt(br.readLine());
        
        for (int i = 0; i < n; i ++) { // 读入增广矩阵
            String[] s = br.readLine().split(" ");
            for (int j = 0; j <= n; j ++) { // 每行会多一个多项式的和
                a[i][j] = Double.parseDouble(s[j]);
            }
        }
        
        int ans = gauss();
        if (ans == 0)  // 唯一解
            for (int i = 0; i < n; i ++)
                if(Math.abs(a[i][n]) < eps)
                    System.out.printf("0.00\n");// 避免数据存储时精度误差(例如存的答案为-0.00000000000001,如果保留两位小数会输出-0.00,答案应为0.00)
                else
                    System.out.printf("%.2f\n", a[i][n]);
        else if (ans == 1) System.out.println("Infinite group solutions"); // 无穷多解
        else System.out.println("No solution"); // 无解
    }
}

4.15 简单博弈论:Nim游戏

算法思想:

博弈论又被称为对策论(Game Theory),既是现代数学的一个新分支,也是运筹学的一个重要学科。博弈论主要研究已公式化的激励结构间的相互作用,是研究具有斗争或竞争性质现象的数学理论和方法。博弈论考虑游戏中的个体的预测行为和实际行为,并研究它们的优化策略。

Nim游戏为例:给定n堆石子,两位玩家轮流操作,每次操作可以从任意一堆石子中拿走任意数量的石子(可以拿完,但不能不拿),最后无法进行操作(即没有石子可拿)的人视为失败。

问如果两人都采用最优策略,先手是否必胜?

结论:

  • 每堆石子的异或为0,则先手必败
  • 每堆石子的异或为1,则先手必胜
代码实现:
import java.util.*;
import java.io.*;

public class Main {
    public static void main(String[] args) throws IOException{
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int n = Integer.parseInt(br.readLine());
        String[] s = br.readLine().split(" ");
        int[] a = new int[100010];
        int res = 0;
        for (int i = 0; i < n; i ++) {
            a[i] = Integer.parseInt(s[i]);
            res ^= a[i];
        }
        if (res == 0) System.out.print("No");
        else System.out.print("Yes");
    }
}
  • 19
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值