算法中常见的数学问题
在计算机世界里,我们通常还是要解决一些数学问题,这里对一些常见数学问题的算法进行总结,希望能够减慢遗忘的速度。
最大公约数和最小公倍数
最大公约数
正整数 a 与 b 的最大公约数是指 a 与 b 的所有公约数中最大的那个公约数,一般用 gcd(a, b)
来表示 a 和 b 的最大公约数,而求解最大公约数常用欧几里得算法(辗转相除法)。
欧几里得算法基于下面这个定理:
设a、b 均为正整数,则 gcd(a, b) = gcd(a, a % b)
证明:设 a = kb + r, 其中 k 和 r 分别为 a 除以 b 得到的商和余数
则有 r = a - kb 成立
设 d 为 a 和 b 的一个公约数,
那么由 r = a - kb,得 d 也是 r 的一个约数
因此 d 是 b 和 r 的公约数
而 r = a % b,得 d 为 b 和 a % b 的一个公约数
因此 d 既是 a 和 b 的公约数,也是 b 和 a % b 的公约数
由于 d 的任意性,得 a 和 b 的公约数都是 b 和 a % b 的公约数
由 a = kb + r,同理可证 b 和 a % b 的公约数都是 a 和 b 的公约数
因此 a 和 b 的公约数与 b 和 a % b 的公约数全部相等,故其最大公约数也相等
即 gcd(a, b) = gcd(b, a % b)
如果 a < b ,结果就是将 a 和 b 交换;如果 a > b,那么通过这个定理可以很快的将数据规模变小。那么递归边界呢?众所周知,0 和任意一个整数 a 的最大公约数都是 a,这个结论可以当作递归边界。由此,递归的两个关键得到:
- 递归式:gcd(a, b) = gcd(b, a % b)
- 递归边界:gcd(a, 0) = a
于是可以得到下面的代码:
int gcd(int a, int b) {
if(b == 0) return a;
else return gcd(b, a % b);
}
最小公倍数
一般用 lcm(a, b) 来代表 a 和 b 的最小公倍数。
最小公倍数的求解在最大公约数的基础上进行。当得到 a 和 b 的最大公约数 d 后,可以马上得到最小公倍数是 ab / d,由于 **ab **在实际运算时可能溢出,所以更恰当的写法是 a/db。
a 和 b 的最大公约数即集合 a 与集合 b 的交集,而最小公倍数为 a 和 b 的并集,由于 ab 会使公因子部分多计算一次,故需要除掉一次公因子
分数的四则运算
分数的四则运算是指给定两个分数的分子和分母,求它们加减乘除的结果,下面先介绍如何表示和化简一个分数。
分数的表示和化简
分数的表示
对一根分数来说,最简洁的写法就是写成假分数的形式,即无论分子比分母大或者小,都保留其原数,因此可以使用结构体来保存:
struct Fraction{
int up, down;
};
三项规则:
- 使 down 为非负数,如果分数为负,那么令分子 up 为负即可
- 如果该分数恰好为 0,那么规定其分子为 0,分母为 1
- 分子和分母没有除了 1 以外的公约数
分数的化简
化简主要使用使 Fraction 变量满足分数表示的三项规定,因此化简步骤分为以下三步:
- 如果分母 down 为负数,令分子和分母都变成其相反数
- 如果分子 up 为 0,那么令分母为 1
- 约分:求出分子绝对值和分母绝对值的最大公约数 d,然后令分子分母同时除以 d
代码如下:
Fraction reduction(Fraction result) {
if(result.down < 0) { // 分母为负数,令分子分母都变味相反数
result.up = -result.up;
result.down = -result.down;
}
if(result.up == 0) { // 如果分子为 0,令分母为 1
result.down = 1;
}else {
int d = gcd(abs(result.up), abs(result.down)); // 分子分母的最大公约数
result.up /= d;
result.down /= d; // 约去最大公约数
}
return result;
}
分数的四则运算
分数的加减法都需要进行约分,所以需要约分出公式,然后针对公式进行计算即可。
// 加法
Fraction add(Fraction f1, Fraction f2) {
Fraction result;
result.up = f1.up * f2.down + f2.up * f1.down;
result.down = f1.down * f2.down;
return reduction(result);
}
// 减法
Fraction minu(Fraction f1, Fraction f2) {
Fraction result;
result.up = f1.up * f2.down - f2.up * f1.down;
result.down = f1.down * f2.down;
return reduction(result);
}
// 乘法
Fraction mul(Fraction f1, Fraction f2) {
Fraction result;
result.up = f1.up * f2.up;
result.down = f2.down * f2.down;
return reduction(result);
}
// 除法
Fraction divide(Fraction f1, Fraction f2) {
Fraction result;
result.up = f1.up * f2.down;
result.down = f1.down * f2.up;
return reduction(result);
}
分数的输出
分数的输出需要根据题目的要求进行,大体有以下几点注意:
- 输出分数之前,需要对其先进行化简
- 如果分数 r 的分母 down 为 1,说明该分数是整数,一般会直接输出分子
- 如果分数 r 的分子 up 的绝对值大于分母 down,说明该分数是假分数,此时应该输出带分数的形式,即整数部分为 r.up / r.down,分子部分为 abs(r.up) % down
- 以上均不满足时说明分数 r 时真分数,按原样输出
以下是一个输出案例:
void showResult(Fraction r) {
r = reduction(r);
if(r.down == 1) printf("%lld", r.up);
else if(abs(r.up) > r.down) {
printf("%d %d/%d", r.up / r.down, abs(r.up) % r.down, r.down);
}else {
printf("%d %d", r.up, r.down);
}
}
素数
素数又称为质数,是指除了 1 和本身外,不能被其他整数除的一类数。即对于给定的正整数 n,如果对于任意的正整数 a(1 < a < n),都有 n % a != 0
,那么称 n 时素数,否则为合数。特别注意的是,1 既不是素数,也不是合数。
素数的判断
判断一个数字 n 是否为素数,需要判断 n 能否被 2,3,……,n-1 整除,只有都不能整除的时候,才能判断是素数。但是我们发现,如果一个数字有大于 sqrt(n) 的约数,那么其肯定有一个小于 sqrt(n) 的约数,所以我们只需要从 2 判断到 sqrt(n)。
代码如下:
bool isPrime(int n) {
if(n <= 1) return false;
int sqr = (int)sqrt(n); // 根号 n
for(int i = 2; i <= sqr; i++) {
if(n % i == 0) return false;
}
return true; // n 是素数
}
如果 n 没有接近 int 变量的范围的上界,有更简单的写法:
bool isPrime(int n) {
if(n <= 1) return false;
for(int i = 2; i * i <= n; i++) {
if(n % i == 0) return false;
}
return true;
}
这样写的问题就是当接近 int 型变量的范围上界时导致 i * i 溢出(当然在 109 以内都会是安全的),解决的办法是将 i 定义为 long long 型,就不会溢出来。
素数表的获取
如果我们需要在很大范围内去寻找所有的素数,有没有快速一点的方法呢?
“筛选法” 是很多筛选法中很容易理解的一类,核心思想是:
算法从小到大枚举所有数,对每一个素数,筛去它的所有倍数,剩下的就都是素数了,在这个过程中,我们唯一需要给定的开始条件就是 2 是一个素数,之后的过程通过循环即可得到。
从小到大到达某个数 a 时,如果 a 没有被前面的步骤筛去,那么 a 一定是素数,这是因为如果 a 不是素数,a一定有小于 a 的素因子,所以一定被筛掉
我们可以使用一个 bool 数组来模拟筛,如果 a[i] 为 true,则说明是质数,如果 a[i] 为false,则为非质数。
bool isPrime[maxn];
vector<int> prime;
void getPrime() {
for(int i = 0; i < maxn; i++) {
isPrime[i] = true; // 开始认为所有的都是素数
}
isPrime[0] = isPrime[1] = false;
for(int i = 2; i < maxn; i++) {
if(!isPrime[i]) {
continue; // 如果是非质数则跳过
}
prime.push_back(i);
for(int j = i * i; j < maxn; j+=i) {
isPrime[j] = false;
}
}
return;
}
分解质因数
所谓质因子分解是指将一个正整数 n 写成一个或多个质数乘积的形式。
但是最后都要写成若干不同质数的乘积,因此我们不妨先把素数表打出来。由于每个质因子出现可能不止一次,所以我们不妨定义结构体来存放质因子和其个数:
struct factor{
int x, cnt;
}fac[10];
对于一个正整数 n 来说,如果它存在 [2, n] 范围内的质因子,要么这些因子都小于 sqrt(n),那么至多有一个大于 sqrt(n),所以思路出现:
- 枚举 1~sqrt(n) 范围内所有的质因子,判断 p 是否是 n 的因子
- 如果 p 是 n 的因子,那么给 fac 数组中增加质因子 p,并且初始化其个数为 0,然后只要 p 还是 n 的因子,就让 n 不断除以 p,每次操作令 p 的个数加 1,直到 p 不是 n 的因子
- 如果 p 不是 n 的因子,则直接跳过
- 如果上述步骤结束后 n 仍然大于 1,说明 n 有一个大于 sqrt(n)的因子,此时把这个因子加入 fac 数组中。
代码如下:
/*
Author: Veeupup
*/
#include <cstdio>
#include <climits>
#include <vector>
#include <cmath>
#include <algorithm>
using namespace std;
const long maxn = 1e7;
vector<long> primes; // 保存质数
bool isPrime[maxn]; // 质数判断
// 质数表初始化
void Initial() {
fill(isPrime, isPrime + maxn, true); // 所有的数字都初始化为 true,为质数,随后筛
isPrime[0] = isPrime[1] = false;
for(long i = 2; i < maxn; i++) {
if(!isPrime[i])
continue; // 如果 i 不是质数,则跳过
primes.push_back(i);
for(long j = i * i; j < maxn; j += i) {
isPrime[j] = false; // i 的倍数被标记为合数
}
}
return;
}
struct Factor {
long x, cnt;
}fac[10]; // 保存质因子
int main()
{
Initial(); // 初始化质数表
long n, num = 0; // num 为不同质因子的个数
scanf("%ld", &n);
if(n == 1) {
printf("1=1");
}else {
printf("%ld=", n);
long sqr = (long)sqrt(n);
// 枚举根号 n 以内的质因子
for (int i = 0; i < primes.size() && primes[i] < sqr; i++)
{
if(n % primes[i] == 0) {
fac[num].x = primes[i];
fac[num].cnt = 0;
while (n % primes[i] == 0)
{
fac[num].cnt++;
n /= primes[i];
}
num++;
}
if(n == 1)
break;
}
if(n != 1) {
fac[num].x = n;
fac[num++].cnt = 1;
}
// 按照格式输出
for (int i = 0; i < num; i++)
{
if(i > 0) printf("*");
printf("%ld", fac[i].x);
if(fac[i].cnt > 1) {
printf("^%d", fac[i].cnt);
}
}
}
return 0;
}