zoukankan      html  css  js  c++  java
  • 算法中常见的数学问题

    算法中常见的数学问题

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

    最大公约数和最小公倍数

    最大公约数

    正整数 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;
    }
    
  • 相关阅读:
    Web前端开发中的各种CSS规范
    SVN简明课程
    使用django-compressor压缩静态文件
    今日头条视频Url嗅探
    python 异常类型
    抓包分析工具备注
    电子签章盖章之jQuery插件jquery.zsign
    程序员读书雷达
    在csdn里markdown感受
    如何在无趣的世界里,做一个有趣的人?
  • 原文地址:https://www.cnblogs.com/veeupup/p/12561242.html
Copyright © 2011-2022 走看看