CC++ & Algorithm

数学知识辅助优化

较难4
语言版本:C++Python
概述:用数学公式或性质(比如最大公约数、平方根)简化计算,让程序既快又准。

用数学知识给程序加速:聪明地计算,而不是蛮力循环

写程序时,我们经常会遇到需要判断一个数是不是素数、求两个数的最大公约数这类问题。假如只用“暴力循环”——比如从1试到那个数本身——虽然也能得到答案,但遇到大数时程序就会慢得像蜗牛爬。可是如果我们借助数学公式或性质(比如平方根、辗转相除法),就能一下子跳过大量无意义的计算,让程序又快又准。这就好比你想知道一个西瓜是不是好瓜,不用把整个西瓜切开一点点尝,只需要切一小块看看就行了——数学知识就是那块“小切块”。

下面我们通过两个经典例子,看看数学是怎么帮我们“偷懒”的。


一、判断素数:从试到 n-1 到试到 sqrt(n)

什么是素数?
素数是只能被1和它本身整除的正整数,比如2、3、5、7、11……注意1不是素数。

最笨的办法:从2试到 n-1,看有没有能整除的。如果 n=1000000007(10亿多),就要循环10亿次,电脑也会累哭。

数学教我们:如果 n 不是素数,它一定有一个大于1并且小于等于 sqrt(n) 的因子。为什么呢?因为如果有两个因子 ab 使得 a*b=n,那么 ab 不可能都大于 sqrt(n)(否则乘积大于 n),所以至少有一个因子 ≤ sqrt(n)。于是我们只需要检查从2到 sqrt(n) 的数字就够了。

生活中的例子:老师有48颗糖果,要平均分给同学们,并且每人分到的颗数相同(每人至少2颗)。你怎么知道能不能正好分完?你不需要试所有人数从2到47,只需要试2到√48≈6.9,也就是2、3、4、5、6。因为如果48能被某个大于6的数整除,比如12,那么对应的另一个因子4已经在2到6之间了。

优化后的代码(保留你原有的代码,但加上详细注释):

#include <iostream>
#include <cmath>  // 使用 sqrt 函数
using namespace std;

// 判断一个整数 n 是否为素数
bool isPrime(int n) {
    if (n <= 1) return false;     // 0和1不是素数
    if (n == 2) return true;      // 2是唯一的偶素数
    if (n % 2 == 0) return false; // 其他偶数都不是素数,直接排除

    int limit = sqrt(n);          // 只需要检查到根号n
    for (int i = 3; i <= limit; i += 2) {  // 只检查奇数,步长2
        if (n % i == 0) return false;      // 找到了因子
    }
    return true;                  // 没有因子,是素数
}

int main() {
    int num = 1000000007;        // 一个比较大的素数(实际上1000000007确实是素数)
    cout << (isPrime(num) ? "是素数" : "不是素数") << endl;
    return 0;
}

运行一下:循环从3到31622(√10亿≈31623),步长2,只循环了约15000多次,而如果用暴力循环需要10亿次,速度提升了6万多倍!你学会这个“平方根魔法”了吗?


二、求最大公约数:从枚举到欧几里得算法

最大公约数(GCD):两个整数都能整除的最大整数。比如12和18,公约数有1、2、3、6,最大的是6。

最笨的办法:从1枚举到较小的那个数,找出所有能同时整除两个数的数,取最大值。如果两个数是100000和200000,就要枚举10万次。

数学教我们:欧几里得算法(辗转相除法)告诉我们:
gcd(a, b) = gcd(b, a % b),当 b=0 时,gcd(a,0)=a
比如求 gcd(48, 18)

  • 48 % 18 = 12 → gcd(18, 12)
  • 18 % 12 = 6 → gcd(12, 6)
  • 12 % 6 = 0 → gcd(6, 0) = 6
    三步就算出来了,就算数字很大也很快。

生活中的例子:你想要把24个苹果和36个橘子平均分给几个朋友,每个朋友分到的苹果数和橘子数分别相同(苹果不能切开,橘子也是),最多可以分给几个朋友?答案是求24和36的最大公约数——12个朋友。用辗转相除法:36%24=12,24%12=0,所以gcd=12。

代码实现(简洁而高效):

#include <iostream>
using namespace std;

// 求两个整数的最大公约数(欧几里得算法)
int gcd(int a, int b) {
    while (b != 0) {      // 当余数不为0时继续
        int temp = b;     // 保存当前的除数
        b = a % b;        // 新的余数作为下一轮的除数
        a = temp;         // 原来的除数变成被除数
    }
    return a;             // 最后a就是最大公约数
}

int main() {
    int x = 48, y = 18;  // 两个数
    cout << "48和18的最大公约数是: " << gcd(x, y) << endl;
    return 0;
}

更简洁的写法(用递归):
int gcd(int a, int b) { return b == 0 ? a : gcd(b, a % b); }
不过递归对初学者可能有点绕,用循环更容易理解。


三、新手容易犯的错误

  1. 忘记处理边界情况

    • 素数判断中:n <= 1 都返回 false,但漏掉1的判断会导致误判。
    • 最大公约数中:如果 ab 是负数?通常我们只求非负数的gcd,需要取绝对值,或者先处理 abs
  2. 平方根函数返回 double,直接当整型用可能精度不够

    • 比如 sqrt(9) 返回 3.0,赋值给 int limit 没问题;但 sqrt(10) 返回 3.162...,截断后变成 3,正好够了。
    • 但有些极端情况(比如大数的浮点舍入误差),可能会导致少检查一个数。保险的做法:int limit = (int)sqrt(n) + 1,多检查一次无害。
  3. 循环条件写成 i <= n-1 而不是 i <= sqrt(n)

    • 这是没应用数学优化的常见错误,初学者很容易忘记。
  4. 在欧几里得算法中搞错 ab 的赋值顺序

    • 比如写成 a = b; b = a % b; 会先覆盖 a 导致后面取模出错。一定要先用临时变量保存原来的 b
  5. 忘记包含头文件 <cmath>

    • 使用 sqrt 需要 #include <cmath>,否则编译会报错。

四、完整可运行的综合示例

下面我们写一个完整程序,先判断一个数是不是素数,如果是素数,再输出它的最大公约数(和另一个数)。程序包含输入提示,适合初学者测试。

#include <iostream>
#include <cmath>   // 用 sqrt
using namespace std;

// 判断素数(优化版)
bool isPrime(int n) {
    if (n <= 1) return false;       // 0、1不是素数
    if (n == 2) return true;        // 2是素数
    if (n % 2 == 0) return false;   // 偶数不是素数(除了2)

    int limit = (int)sqrt(n) + 1;   // 多加1避免精度问题
    for (int i = 3; i <= limit; i += 2) {
        if (n % i == 0) return false;
    }
    return true;
}

// 求最大公约数(欧几里得算法)
int gcd(int a, int b) {
    while (b != 0) {
        int temp = b;      // 保存除数
        b = a % b;         // 新的余数
        a = temp;          // 原来的除数成为新被除数
    }
    return a;
}

int main() {
    int number, other;
    cout << "请输入一个整数:";
    cin >> number;
    cout << "请输入另一个整数:";
    cin >> other;

    if (isPrime(number)) {
        cout << number << " 是素数。" << endl;
    } else {
        cout << number << " 不是素数。" << endl;
    }

    int result = gcd(number, other);
    cout << number << " 和 " << other << " 的最大公约数是:" << result << endl;

    return 0;
}

运行示例

请输入一个整数:1000000007
请输入另一个整数:100
1000000007 是素数。
1000000007 和 100 的最大公约数是:1

你还可以自己尝试输入不同数字,感受优化带来的乐趣。


五、还想了解更多?相关知识点指引

  • 埃拉托色尼筛法:一次找出很多个素数(比如1到1000000的所有素数),比逐个判断快得多。
  • 快速幂:计算 a^b mod m 时,用二分法把复杂度从 O(b) 降到 O(log b)
  • 欧几里得算法的扩展:可以用来解不定方程(裴蜀定理),或者求模逆元。
  • 数学归纳法:在编程中证明算法正确性时常用,尤其是递归算法。

数学就像一把万能钥匙,能帮你打开很多问题的“锁”。下次遇到需要大量循环的问题,先停下来想一想:有没有数学公式或性质可以派上用场?试试看,你会爱上这种聪明的写法!

例题精讲

1单选题

下列哪种方法求两个正整数的最大公约数(GCD)的效率最高?

A从1开始逐个枚举到较小的数,找出最大公约数
B使用欧几里得算法(辗转相除法)
C使用更相减损术(反复相减)
D对两个数分别进行质因数分解,取公共质因数乘积
2判断题

判断一个正整数n是否为素数时,只需检查从2到sqrt(n)之间的整数能否整除n,如果都没有整除,则n是素数。这种方法是正确的。

3填空题
以下函数用于判断正整数n是否为素数,请补充循环条件,使得算法复杂度最优。

bool isPrime(int n) {
    if (n < 2) return false;
    for (int i = 2; ___ ; i++) {
        if (n % i == 0) return false;
    }
    return true;
}
4单选题

计算前n个正整数的平方和(即1²+2²+...+n²),当n=10⁶时,以下哪种做法效率更高?

A使用for循环累加,每次计算平方并累加
B使用数学公式 n*(n+1)*(2n+1)/6 直接计算
C使用递归函数求和
D使用等比数列求和公式变形
5填空题
补全下面递归函数,使用欧几里得算法求两个整数a和b的最大公约数。

int gcd(int a, int b) {
    if (b == 0) return a;
    return ___;
}