【算法】数学相关知识总结

本文用于记录一些关于算法题中偶尔被使用到的数学相关知识。

gcd 和 lcm

gcd 和 lcm 分别是 最大公约数(Greatest common divisor) 和 最小公因数(Least Common Multiple)。

常用的 gcd 算法使用的是欧几里得算法,也就是辗转相除算法。原理如下:

1.有两个数a和b,我们把较大的数传给maxn,较小的数传给minx
2.用maxn对minx进行取余运算,如果余数为0,那么a,b的最大公约数为a,
3.若余数不为0,a,b的最大公约数为minx和余数的最大公约数,我们在循环到第一步进行计算。

将算法描述翻译成代码如下:

public int gcd(int a, int b) {
    if (a < b) return gcd(b, a);    // 确保 a 是较大的数字
    while (a % b != 0) {	// 一直相除到余数为 0
        int t = a % b;		// 求余数
        a = b;	// 
        b = t;	// 
    }
    return b;
}

推荐写法
经过精简之后可以写成如下形式(原理是一样的,只不过从迭代改成了递归的形式,代码会更短一些。)(或者可以这样来理解这种写法基于的一个事实:对于任何两个整数a和b,gcd(a, b)和gcd(b, a % b)是相同的

public int gcd(int a, int b) {
    return b != 0? gcd(b, a % b): a;
}

此外还有一种 if + while + 位运算的写法:

public int gcd(int a, int b) {
    if (b != 0) {
        while ((a %= b) != 0 && (b %= a) != 0);
    }
    return a + b;
}

通过使用 System.currentTimeMillis() 来测算程序运行的时间,可以发现后两种写法的耗时要短一些。(后两种的速度差不多都是第一种 while 循环的两倍左右

最大公约数与质数的关系:通过判断两个或者多个整数之间的公约数只有1,就可以说它们是互质的。


lcm 的写法在 gcd 的基础之上,即两个数相乘然后除最大公约数即为最小公倍数

public int lcm(int a, int b) {
    return a * b / gcd(a, b);
}

参考资料:
gcd和lcm(最大公约数,最小公倍数)
【C++】gcd函数的写法
D351周赛复盘:美丽下标对数目(互质/数学运算)+数组划分若干子数组

取模运算 %

如果让你计算 1234 ∗ 6789 1234 * 6789 12346789个位数,你会如何计算?
由于只有个位数会影响到乘积的个位数,因此 4 ∗ 9 = 36 4 * 9 = 36 49=36 的个位数 6 就是答案。

将这个结论抽象成数学等式如下:

( a + b )   m o d   m = ( ( a   m o d   m ) + ( b   m o d   m ) )   m o d   m (a + b) \bmod m = ((a \bmod m) + (b \bmod m)) \bmod m (a+b)modm=((amodm)+(bmodm))modm
( a ∗ b )   m o d   m = ( ( a   m o d   m ) ∗ ( b   m o d   m ) )   m o d   m (a * b) \bmod m = ((a \bmod m) * (b \bmod m)) \bmod m (ab)modm=((amodm)(bmodm))modm

在这里插入图片描述
参考资料:
https://leetcode.cn/problems/movement-of-robots/solution/nao-jin-ji-zhuan-wan-pai-xu-tong-ji-pyth-we55/

求一个点和一片矩形区域之间的最短距离

以一道题目为例:https://leetcode.cn/problems/circle-and-rectangle-overlapping/

在这里插入图片描述
在这里插入图片描述

class Solution {
    public boolean checkOverlap(int radius, int xCenter, int yCenter, int x1, int y1, int x2, int y2) {
        double dist = 0;
        if (xCenter < x1 || xCenter > x2) {
            dist += Math.min(Math.pow(x1 - xCenter, 2), Math.pow(x2 - xCenter, 2));
        }
        if (yCenter < y1 || yCenter > y2) {
            dist += Math.min(Math.pow(y1 - yCenter, 2), Math.pow(y2 - yCenter, 2));
        }
        return dist <= radius * radius;
    }
}

求1~n中的所有质数

在这里插入图片描述

思路

在这里插入图片描述

解法1——最朴素的筛法

从小到大进行枚举,每次枚举到一个数字,就把数据范围内它的所有倍数都删掉。
这样剩下的没有被筛掉的就都是质数了。

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

int getPrime(int n) {
    int f[n + 1] = {1};
    for (int i = 0; i <= n; ++i) f[i] = 1;
    int res = 0;
    for (int i = 2; i <= n; ++i) {
        if (f[i] == 1) {
            ++res;
        }
        for (int j = i + i; j <= n; j += i) {
            f[j] = 0;
        }
    }
    return res;
}

int main()
{
    int n;
    scanf("%d", &n);
    printf("%d\n", getPrime(n));
    return 0;
}

时间复杂度:

在这里插入图片描述
可以记成 O ( n log ⁡ 2 n ) O(n\log_{2}{n}) O(nlog2n)

解法2——解法1的优化(埃氏筛法 O(nloglogn))

一种由希腊数学家埃拉托斯特尼所提出的一种简单检定素数的算法。要得到自然数n以内的全部素数,必须把不大于根号n的所有素数的倍数剔除,剩下的就是素数。
筛的时候,只需要筛掉所有质数的倍数就好了。

质数定理

1~n 中有 n ln ⁡ n \frac{n}{\ln{n}} lnnn 个质数。

从小到大进行枚举,每次枚举到一个质数,就把数据范围内它的所有倍数都删掉。

从代码的角度来看,和解法一的区别其实只在于:将筛质数的过程放在了判断当前数字是否为质数的 if 中,即只需要筛掉质数的倍数即可

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

int getPrime(int n) {
    int f[n + 1];
    for (int i = 0; i <= n; ++i) f[i] = 1;	// 初始化成1,表示没被筛掉/是质数
    int res = 0;						// 记录有多少个质数
    for (int i = 2; i * i <= n; ++i) {	// 枚举2~n
        if (f[i] == 1) {				// 没被筛掉
            for (int j = i * i; j <= n; j += i) {	// 将i的所有倍数筛掉
                f[j] = 0;
            }
        }
    }
    for (int i = 2; i <= n; ++i) if (f[i]) res++;
    return res;
}

int main()
{
    int n;
    scanf("%d", &n);
    printf("%d\n", getPrime(n));
    return 0;
}

代码中的 for (int j = i * i; j <= n; j += i) 是为了对于每个质数 i ,筛掉它的所有倍数 j,因为是 j += i
在自己做的时候,初始化写成了 int j = 2 * i,从结果上看当然没有问题,但是可以优化成 int j = i * i,因为从 i * 2、i * 3、… 一直到 i * (i - 1) ,其实都已经在之前的大循环中筛过了(即对应 i = 2、i = 3、… i = i - 1时的大循环)。

时间复杂度:
在这里插入图片描述
在这里插入图片描述
参考资料:通俗易懂的埃氏筛时间复杂度分析

解法3——线性筛法(O(n))

线性筛法的核心:n只会被它的最小质因子筛掉(也就是说它不会被重复筛多次)

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

int getPrime(int n) {
    int f[n + 1], prime[n];   // f[i]=1表示i是质数;prime[i]是从0~n第i个质数
    memset(prime, 0, sizeof prime);
    for (int i = 0; i <= n; ++i) f[i] = 1;
    int cnt = 0;							// cnt记录质数的总数
    for (int i = 2; i <= n; ++i) {			// 枚举从2~n是不是质数
        if (f[i] == 1) prime[cnt++] = i;    // 没被筛掉,那就是质数,i是质数,记录在数组中
        for (int j = 0; prime[j] * i <= n; ++j) {	// 筛掉目前所有质数的i倍数字
            f[prime[j] * i] = 0;            // 筛掉 prime[j]*i
            if (i % prime[j] == 0) break;   // prime[j]一定是i的最小质因子,退出筛的过程,防止重复筛
        }
    }
    return cnt;
}

int main()
{
    int n;
    scanf("%d", &n);
    printf("%d\n", getPrime(n));
    return 0;
}

与前面两种方法不同的是,前面两种方法,每次循环 j 时是为了筛掉 i 的所有倍数,即 j 的意义是 i 的倍数。
但是这种方法,每次循环 j 是为了筛掉所有已经发现的质数的 i 倍,即 prime[j] 的意义是目前已经发现的第 j 个质数。

关于 if (i % prime[j] == 0) break;
这是为了防止重复去筛某个数字,确保每个数字都只会被它的最小质因子筛掉。
举个例子:
当枚举到 i = 15 时,有 15 % 3 == 0,这时候就 break 掉了,只会筛到 15 * 3 = 45 这个数字,而不许继续去筛掉 15 * 5 = 75 这个数字。因为 75 = 25 * 3,它会在枚举到 i = 25 时被 3 这个最小质因子筛掉,所以不让 5 这个非最小质因子提前筛掉 75。


在代码中,
for (int j = 0; prime[j] * i <= n; ++j) 这句可能有同学认为需要改成 for (int j = 0; j < cnt && prime[j] * i <= n; ++j)

但是实际上是不需要 j < cnt 的判断的,因为

  • 当 i 是合数的时候,枚举到 i % prime[j] == 0 时就会 break 出去了
  • 当 i 是质数的时候,枚举到 primes[j] = i 时,i % i == 0,它也会 break 出去。(注意当 i 是质数的时候一定会有某个 j ,primes[j] = i 的,因为上面已经将质数 i 放入 primes 了)

总之就是它一定会 break 出去!


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

三种筛法的速度实测:

在这里插入图片描述

参考资料:
C++使用O(n)的算法找到1~n的所有素数——欧拉筛法的模板代码及原理

组合数求法

见:【力扣周赛】第 112 场双周赛(统计一个字符串的 k 子序列美丽值最大的数目(贪心+计数+组合数学)

    long comb(long n, long k) {
        long res = n;
        for (int i = 2; i <= k; ++i) {
            res = res * --n / i;
        }
        return res % MOD;
    }

相关题目练习

见:
【LeetCode周赛】2022上半年题目精选集——数学

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Wei *

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值