1. 约数基础概念与试除法实现
约数(Divisor)是数论中最基础也最重要的概念之一。简单来说,如果一个整数a能被另一个整数b整除(即a÷b的余数为0),那么b就是a的约数。例如,6的约数有1、2、3、6,因为6能被这些数整除。
1.1 约数的数学定义
在数学表达上,我们记作b|a,表示b整除a。约数有几个重要性质:
- 每个正整数至少有1和它本身两个约数
- 1是所有正整数的约数
- 如果a是质数,那么它只有1和a两个约数
理解这些性质对后续约数相关算法的实现至关重要。比如在判断一个数是否为质数时,本质上就是在检查它的约数个数是否为2。
1.2 试除法求约数
试除法是最直观的求约数方法,其核心思想是:遍历所有可能的候选数,检查是否能整除目标数。以下是基本实现步骤:
- 初始化一个空列表用于存储找到的约数
- 从1到n遍历每个整数i
- 如果n能被i整除(n%i == 0),则将i加入约数列表
- 同时,如果i≠n/i,也将n/i加入约数列表(避免重复添加平方数的情况)
- 最后对约数列表进行排序
这个算法的时间复杂度是O(√n),因为当i超过√n后,n/i就会小于i,这时我们已经找到了所有约数对。
注意:在实现时,要特别注意完全平方数的情况(如16=4×4),避免将同一个约数添加两次。
1.3 试除法代码实现
cpp复制vector<int> getDivisors(int n) {
vector<int> divisors;
for (int i = 1; i <= n / i; ++i) {
if (n % i == 0) {
divisors.push_back(i);
if (i != n / i) divisors.push_back(n / i);
}
}
sort(divisors.begin(), divisors.end());
return divisors;
}
这段代码有几个关键点:
- 循环条件i <= n/i避免了使用sqrt函数带来的浮点数精度问题
- 检查i != n/i防止重复添加平方根
- 最后排序保证约数按升序排列
在实际应用中,比如解决"给定n个正整数,输出每个数的所有约数"这类问题时,这个算法非常有效。对于输入规模不大的情况(如n≤100),这种方法的性能完全足够。
2. 约数个数的高效计算
2.1 约数个数的数学原理
约数个数的计算基于数的质因数分解。根据数论基本定理,任何大于1的正整数都可以唯一表示为质数的幂次乘积:
n = p₁^a₁ × p₂^a₂ × ... × pₖ^aₖ
其中pᵢ是质数,aᵢ是对应的指数。那么n的正约数的总数d(n)可以通过以下公式计算:
d(n) = (a₁+1) × (a₂+1) × ... × (aₖ+1)
这个公式的原理是:对于每个质因数pᵢ,在约数中它的指数可以从0取到aᵢ,共aᵢ+1种选择。所有质因数的选择组合起来就是总的约数个数。
2.2 质因数分解实现
要实现约数个数的计算,首先需要实现质因数分解。以下是分步过程:
- 初始化一个空的质因数计数器(可以用map或unordered_map)
- 从最小的质数2开始尝试整除n
- 每当找到一个能整除n的质数p时,不断除以p并计数,直到不能整除为止
- 处理完2后,继续用奇数3,5,7...尝试,直到√n
- 如果最后剩下的n>1,说明它本身就是一个质数
cpp复制map<int, int> primeFactors(int n) {
map<int, int> factors;
for (int i = 2; i <= n / i; ++i) {
while (n % i == 0) {
factors[i]++;
n /= i;
}
}
if (n > 1) factors[n]++;
return factors;
}
2.3 计算约数个数的完整实现
结合质因数分解和约数个数公式,完整的实现如下:
cpp复制int countDivisors(int n) {
auto factors = primeFactors(n);
int count = 1;
for (auto [p, exp] : factors) {
count *= (exp + 1);
}
return count;
}
对于多个数的乘积的约数个数问题(如给定n个数,求它们乘积的约数个数),我们可以先分别分解每个数,然后合并所有质因数的指数,最后应用同样的公式计算。
实际应用时,如果需要对结果取模(如10^9+7),记得在每次乘法后立即取模,防止溢出。
3. 约数和的快速计算
3.1 约数和的数学公式
约数和的计算同样基于质因数分解。对于n = p₁^a₁ × p₂^a₂ × ... × pₖ^aₖ,约数和σ(n)的公式为:
σ(n) = (1+p₁+p₁²+...+p₁^a₁) × (1+p₂+p₂²+...+p₂^a₂) × ... × (1+pₖ+pₖ²+...+pₖ^aₖ)
这个公式可以理解为:每个质因数的幂次和代表了该质因数在约数中所有可能的贡献,所有质因数的贡献相乘就是总的约数和。
3.2 等比数列求和优化
直接计算每个括号内的和效率不高,我们可以使用等比数列求和公式来优化:
1 + p + p² + ... + p^a = (p^(a+1) - 1)/(p - 1)
这可以将计算复杂度从O(a)降到O(log a),如果配合快速幂算法的话。
3.3 约数和的代码实现
cpp复制const int MOD = 1e9 + 7;
int qpow(int a, int b) {
int res = 1;
while (b) {
if (b & 1) res = 1LL * res * a % MOD;
a = 1LL * a * a % MOD;
b >>= 1;
}
return res;
}
int sumDivisors(int n) {
auto factors = primeFactors(n);
int sum = 1;
for (auto [p, exp] : factors) {
int numerator = (qpow(p, exp + 1) - 1 + MOD) % MOD;
int denominator = qpow(p - 1, MOD - 2); // 费马小定理求逆元
sum = 1LL * sum * numerator % MOD;
sum = 1LL * sum * denominator % MOD;
}
return sum;
}
对于多个数的乘积的约数和问题,处理方式与约数个数类似:先合并所有质因数的指数,然后应用约数和公式。
注意:当p-1与MOD不互质时(即p≡1 mod MOD),这种求逆元的方法会失效。在实际编程竞赛中,通常题目会保证不会出现这种情况,或者可以使用其他方法处理。
4. 最大公约数与最小公倍数
4.1 欧几里得算法原理
最大公约数(GCD)的计算最常用的是欧几里得算法,基于以下关键性质:
gcd(a, b) = gcd(b, a mod b)
这个性质可以递归应用,直到b为0时,a就是GCD。证明这个性质的关键在于观察到:
- 任何能整除a和b的数,必然能整除a mod b
- 反过来,任何能整除b和a mod b的数,也必然能整除a
因此,a和b的公约数集合与b和a mod b的公约数集合完全相同。
4.2 GCD和LCM的实现
cpp复制int gcd(int a, int b) {
return b ? gcd(b, a % b) : a;
}
int lcm(int a, int b) {
return a / gcd(a, b) * b; // 先除后乘避免溢出
}
几个实现细节:
- 使用递归形式简洁,但迭代形式可能效率更高
- 对于负数,可以先取绝对值再计算
- LCM计算时先进行除法可以避免中间结果溢出
4.3 扩展欧几里得算法
扩展欧几里得算法不仅能计算GCD,还能找到满足ax + by = gcd(a,b)的整数x和y。这在解决线性同余方程等问题时非常有用。
cpp复制int exgcd(int a, int b, int &x, int &y) {
if (!b) {
x = 1, y = 0;
return a;
}
int d = exgcd(b, a % b, y, x);
y -= a / b * x;
return d;
}
这个算法在密码学、模逆元计算等方面有重要应用。例如,当a和m互质时,可以使用扩展欧几里得算法找到a在模m下的乘法逆元。
