算法笔记(数学知识篇)
- 第四章:数学知识:
- 4.1 试除法判定质数:[试除法判定质数](https://www.acwing.com/problem/content/868/)
- 4.2 试除法分解质因数:[试除法分解质因数](https://www.acwing.com/problem/content/869/)
- 4.3 筛质数:[筛质数](https://www.acwing.com/problem/content/870/)
- 4.4 试除法求约数:[试除法求约数](https://www.acwing.com/activity/content/problem/content/938/)
- 4.5 约数个数:[约数个数](https://www.acwing.com/activity/content/problem/content/939/)
- 4.6 约数之和:[约数之和](https://www.acwing.com/activity/content/problem/content/940/)
- 4.7 最大公约数:[最大公约数](https://www.acwing.com/activity/content/problem/content/941/)
- 4.8 欧拉函数:[欧拉函数](https://www.acwing.com/activity/content/problem/content/943/)
- 4.9 筛法求欧拉函数:[筛法求欧拉函数](https://www.acwing.com/activity/content/problem/content/943/)
- 4.10 快速幂:[快速幂](https://www.acwing.com/problem/content/877/)
- 4.11 快速幂求逆元:[快速幂求逆元](https://www.acwing.com/activity/content/problem/content/945/)
- 4.12 扩展欧几里得算法:[扩展欧几里得算法](https://www.acwing.com/activity/content/problem/content/946/)
- 4.13 线性同余方程:[线性同余方程](https://www.acwing.com/activity/content/problem/content/947/)
- 4.14 高斯消元(`O(n³)`):[高斯消元解线性方程组](https://www.acwing.com/problem/content/885/)
- 4.15 简单博弈论:[Nim游戏](https://www.acwing.com/activity/content/problem/content/961/)
第四章:数学知识:
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
,则将i
与x / 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)
,其中a1
为x
的第一个质因子的指数,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 = 1
,t1 = p + 1
,t2 = 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)]
其中Pi
为x
的每一个质因子。上述公式的除法均为整除(即下取整),上述公式的思想为: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^b
在java
中虽然有方法pow
可以使用,但是在计算过程中很容易就爆long
,而快速幂计算的每一步都mod p
,一般就不会爆long
。 -
其思想为先预处理出
a^(2^0), a^(2^1),… , a^(2^log(k))
的结果,这些数每一个都是前一个的平方。这一步显然是log(b)
复杂度的。 -
再将
a^b
分解为若干个前面预处理出来的值相乘,即将b
分解为前面预处理出来的值的指数相加,这一步可以使用二进制进行计算,例如:十进制中的a^5
,5
的二进制的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)
,则称x
为b
的模m
乘法逆元,记作b⁻¹ (mod m)
。b
存在乘法逆元的充分必要条件是b
与模数m
互质。
- 当
b
为m
的倍数时,很明显,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, b
的x, 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
,当b
为a, 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
行(r
从0
开始); - 将第
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");
}
}