算法中常见的数学问题

算法中常见的数学问题

在计算机世界里,我们通常还是要解决一些数学问题,这里对一些常见数学问题的算法进行总结,希望能够减慢遗忘的速度。

最大公约数和最小公倍数

最大公约数

正整数 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,这个结论可以当作递归边界。由此,递归的两个关键得到:

  1. 递归式:gcd(a, b) = gcd(b, a % b)
  2. 递归边界: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;
};

三项规则:

  1. 使 down 为非负数,如果分数为负,那么令分子 up 为负即可
  2. 如果该分数恰好为 0,那么规定其分子为 0,分母为 1
  3. 分子和分母没有除了 1 以外的公约数

分数的化简

化简主要使用使 Fraction 变量满足分数表示的三项规定,因此化简步骤分为以下三步:

  1. 如果分母 down 为负数,令分子和分母都变成其相反数
  2. 如果分子 up 为 0,那么令分母为 1
  3. 约分:求出分子绝对值和分母绝对值的最大公约数 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);
}

分数的输出

分数的输出需要根据题目的要求进行,大体有以下几点注意:

  1. 输出分数之前,需要对其先进行化简
  2. 如果分数 r 的分母 down 为 1,说明该分数是整数,一般会直接输出分子
  3. 如果分数 r 的分子 up 的绝对值大于分母 down,说明该分数是假分数,此时应该输出带分数的形式,即整数部分为 r.up / r.down,分子部分为 abs(r.up) % down
  4. 以上均不满足时说明分数 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. 枚举 1~sqrt(n) 范围内所有的质因子,判断 p 是否是 n 的因子
    1. 如果 p 是 n 的因子,那么给 fac 数组中增加质因子 p,并且初始化其个数为 0,然后只要 p 还是 n 的因子,就让 n 不断除以 p,每次操作令 p 的个数加 1,直到 p 不是 n 的因子
    2. 如果 p 不是 n 的因子,则直接跳过
  2. 如果上述步骤结束后 n 仍然大于 1,说明 n 有一个大于 sqrt(n)的因子,此时把这个因子加入 fac 数组中。

代码如下:

PAT A1059

/*
    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;
}
posted @ 2020-03-24 19:42  南风sa  阅读(...)  评论(...编辑  收藏