文章目录
本文用于记录一些关于算法题中偶尔被使用到的数学相关知识。
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
1234∗6789 的个位数,你会如何计算?
由于只有个位数会影响到乘积的个位数,因此
4
∗
9
=
36
4 * 9 = 36
4∗9=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
(a∗b)modm=((amodm)∗(bmodm))modm
求一个点和一片矩形区域之间的最短距离
以一道题目为例: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;
}