数学知识辅助优化
较难4用数学知识给程序加速:聪明地计算,而不是蛮力循环
写程序时,我们经常会遇到需要判断一个数是不是素数、求两个数的最大公约数这类问题。假如只用“暴力循环”——比如从1试到那个数本身——虽然也能得到答案,但遇到大数时程序就会慢得像蜗牛爬。可是如果我们借助数学公式或性质(比如平方根、辗转相除法),就能一下子跳过大量无意义的计算,让程序又快又准。这就好比你想知道一个西瓜是不是好瓜,不用把整个西瓜切开一点点尝,只需要切一小块看看就行了——数学知识就是那块“小切块”。
下面我们通过两个经典例子,看看数学是怎么帮我们“偷懒”的。
一、判断素数:从试到 n-1 到试到 sqrt(n)
什么是素数?
素数是只能被1和它本身整除的正整数,比如2、3、5、7、11……注意1不是素数。
最笨的办法:从2试到 n-1,看有没有能整除的。如果 n=1000000007(10亿多),就要循环10亿次,电脑也会累哭。
数学教我们:如果 n 不是素数,它一定有一个大于1并且小于等于 sqrt(n) 的因子。为什么呢?因为如果有两个因子 a 和 b 使得 a*b=n,那么 a 和 b 不可能都大于 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); }
不过递归对初学者可能有点绕,用循环更容易理解。
三、新手容易犯的错误
-
忘记处理边界情况
- 素数判断中:
n <= 1都返回false,但漏掉1的判断会导致误判。 - 最大公约数中:如果
a或b是负数?通常我们只求非负数的gcd,需要取绝对值,或者先处理abs。
- 素数判断中:
-
平方根函数返回
double,直接当整型用可能精度不够- 比如
sqrt(9)返回3.0,赋值给int limit没问题;但sqrt(10)返回3.162...,截断后变成3,正好够了。 - 但有些极端情况(比如大数的浮点舍入误差),可能会导致少检查一个数。保险的做法:
int limit = (int)sqrt(n) + 1,多检查一次无害。
- 比如
-
循环条件写成
i <= n-1而不是i <= sqrt(n)- 这是没应用数学优化的常见错误,初学者很容易忘记。
-
在欧几里得算法中搞错
a和b的赋值顺序- 比如写成
a = b; b = a % b;会先覆盖a导致后面取模出错。一定要先用临时变量保存原来的b。
- 比如写成
-
忘记包含头文件
<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)。 - 欧几里得算法的扩展:可以用来解不定方程(裴蜀定理),或者求模逆元。
- 数学归纳法:在编程中证明算法正确性时常用,尤其是递归算法。
数学就像一把万能钥匙,能帮你打开很多问题的“锁”。下次遇到需要大量循环的问题,先停下来想一想:有没有数学公式或性质可以派上用场?试试看,你会爱上这种聪明的写法!
例题精讲
下列哪种方法求两个正整数的最大公约数(GCD)的效率最高?
判断一个正整数n是否为素数时,只需检查从2到sqrt(n)之间的整数能否整除n,如果都没有整除,则n是素数。这种方法是正确的。
以下函数用于判断正整数n是否为素数,请补充循环条件,使得算法复杂度最优。
bool isPrime(int n) {
if (n < 2) return false;
for (int i = 2; ___ ; i++) {
if (n % i == 0) return false;
}
return true;
}计算前n个正整数的平方和(即1²+2²+...+n²),当n=10⁶时,以下哪种做法效率更高?
补全下面递归函数,使用欧几里得算法求两个整数a和b的最大公约数。
int gcd(int a, int b) {
if (b == 0) return a;
return ___;
}