ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

蓝桥杯竞赛必备:O(√n)算法高效求解数字真因子和

蓝桥杯竞赛必备:O(√n)算法高效求解数字真因子和 1. 项目概述与核心价值最近在带学生准备蓝桥杯竞赛发现很多同学在基础算法题上尤其是关于数字因子、约数这类看似简单的数学问题上经常栽跟头。题目“输出数字除本身的所有因子和”就是一个典型的例子。乍一看这不就是求一个数的真因子和吗但实际编码时从暴力枚举的优化到边界条件的处理再到时间复杂度的控制每一步都藏着不少细节。很多同学要么是算法效率太低在数据量稍大时就超时要么是忽略了特殊情况比如输入为1或质数时导致结果错误。这个题目虽然基础但它完美地串联了循环控制、条件判断、数学思维和算法优化是检验编程基本功和逻辑严谨性的绝佳试金石。对于正在备战蓝桥杯、CCF-CSP或者任何编程入门考试的同学来说彻底搞懂这道题其价值远不止于AC一道题。它背后蕴含的“如何高效求约数”、“如何避免重复计算”、“如何处理边界值”等思想会反复出现在数论、动态规划甚至图论的题目中。今天我就以一个过来人和教练的双重身份带大家从头到尾、由浅入深地拆解这道题。我们不只追求一个正确的答案更要弄明白每一个步骤背后的“为什么”以及在实际竞赛中如何快速、稳健地写出bug-free的代码。无论你是刚接触循环的萌新还是已经有一定基础但总在细节上失分的同学相信这篇详尽的拆解都能让你有所收获。2. 问题本质与数学原理拆解2.1 核心需求解析什么是“除本身的所有因子和”首先我们必须把题目描述翻译成精确的数学和编程语言。题目要求是“输出数字除本身的所有因子和”。这里有几个关键点需要明确因子约数指能够整除给定整数的正整数。例如6的因子有1, 2, 3, 6。除本身这意味着我们需要排除数字本身这个因子。所以对于6我们需要考虑的因子是1, 2, 3。和将上述所有因子相加。因此对于输入n我们需要计算的是真因子之和即sum (所有能整除n的正整数之和) - n。在数论中这个值有一个专门的名字叫做真约数和。注意这里有一个极其重要的边界情况——数字1。1的因子只有它自己。根据“除本身”的原则我们需要排除1本身那么1就没有任何因子了。所以数字1的真因子和应该是0。很多初学者会忽略这一点导致程序在输入为1时输出错误结果比如错误地输出1。2.2 从暴力法到优化算法思路演进最直观的想法是暴力枚举。既然要找所有能整除n的数那就让一个变量i从1循环到n判断n % i 0是否成立。如果是就把i累加起来。最后再从累加和中减去n得到结果。# 最朴素的暴力法 (不推荐) def divisor_sum_naive(n): total 0 for i in range(1, n 1): # 循环n次 if n % i 0: total i return total - n # 减去本身这个方法逻辑清晰但效率是O(n)。当n很大时比如10^9循环次数巨大在竞赛中必然会导致超时Time Limit Exceeded, TLE。因此我们必须优化。优化的核心基于一个简单的数学事实如果i是n的因子那么n/i也一定是n的因子并且这两个因子在i ! sqrt(n)时是不同的。例如n12。当i1时n/i12是一对因子(1,12)。当i2时n/i6是一对因子(2,6)。当i3时n/i4是一对因子(3,4)。我们发现因子是成对出现的。我们不需要遍历到n只需要遍历到sqrt(n)n的平方根即可。在遍历过程中当我们找到一个因子i我们就把i和n/i都加起来。这样就一次性找到了两个因子。这里有三个需要特别注意的细节避免重复累加当i恰好等于n/i时即i*i n这意味着n是一个完全平方数i是它的平方根。此时因子i和n/i是同一个数我们只能加一次。排除数字本身根据题目要求我们需要排除n本身。在成对累加时因子对(i, n/i)中n/i可能就是n本身当i1时。我们不能将n本身加入总和。循环范围遍历i从1到int(sqrt(n))。注意因为我们要判断i*i n的情况所以循环条件通常设为i * i n。基于以上分析我们可以将算法优化到O(sqrt(n))效率提升巨大。2.3 算法流程图与步骤规划为了让思路更清晰我们可以用文字描述一下优化后的算法步骤初始化设总和total 0。输入正整数n。特判如果n 1直接返回0。循环遍历令变量i从1开始循环直到i * i n。 a.判断是否为因子如果n % i 0则进入步骤b和c。 b.累加较小因子将i加入total。 c.累加较大因子计算j n // i。 - 如果j ! i且j ! n则将j加入total。条件j ! i避免了完全平方数的重复累加条件j ! n排除了数字本身 - 如果j i说明是完全平方根且在上一步已加过一次不再重复加。 - 如果j n说明i是1j就是n本身根据规则排除不加。返回结果循环结束total即为所求的“除本身的所有因子和”。这个步骤规划已经考虑到了所有边界情况和优化点是编写代码的可靠蓝图。3. 核心代码实现与逐行解析理解了算法思想接下来我们将其转化为具体的代码。这里我提供Python和C两种竞赛常用语言的实现并附上详细的注释。3.1 Python 实现详解Python代码以其简洁清晰著称非常适合快速实现算法逻辑。import math def sum_of_proper_divisors(n): 计算正整数n的真因子之和即除本身之外的所有因子之和。 参数: n (int): 输入的正整数。 返回: int: n的真因子之和。 # 边界情况处理n为1时没有真因子和为0 if n 1: return 0 total 0 # 初始化总和 # 只需遍历到 sqrt(n)利用因子成对出现的性质 limit int(math.isqrt(n)) # math.isqrt() 是Python3.8引入的整数平方根函数效率高且无浮点误差 # 使用 for i in range(1, limit 1): 也可以 i 1 while i * i n: # 循环条件等价于 i sqrt(n) if n % i 0: # 如果i是n的因子 total i # 将较小的因子i加入总和 j n // i # 计算对应的较大因子j # 需要添加较大因子j的条件 # 1. j ! i (避免完全平方数时平方根被加两次) # 2. j ! n (排除数字本身这个因子) if j ! i and j ! n: total j i 1 return total # 测试用例 if __name__ __main__: test_cases [1, 2, 6, 12, 28, 496, 9] for num in test_cases: result sum_of_proper_divisors(num) print(f数字 {num} 的真因子之和为: {result})代码关键点解析math.isqrt(n)这是求整数平方根的最佳方式。相比于int(math.sqrt(n))或int(n**0.5)isqrt直接返回整数结果完全避免了浮点数精度可能带来的问题例如对于很大的完全平方数浮点运算可能产生细微误差导致取整错误。while i * i n这是另一个常用的循环条件完全在整数域运算同样安全可靠。它比计算sqrt(n)再比较更直观。核心逻辑块if n % i 0total i无条件加上较小的因子i。为什么因为只要i是因子它就有资格被加入除非它是n本身但i从1开始in的情况只会在n1时发生而我们已经特判了。j n // i使用整数除法得到配对的因子。if j ! i and j ! n这个条件判断是精华所在。j ! i处理完全平方数。例如n9当i3时j也等于3。如果不加这个判断因子3会被加两次。j ! n排除数字本身。当i1时jn。这个条件确保n本身不会被加入total。测试用例包含了1边界、2质数、6普通合数、12有多个因子、28和496完全数即真因子和等于它本身、9完全平方数。覆盖了所有典型情况。3.2 C 实现详解C在竞赛中以其运行速度快而受欢迎。实现逻辑与Python一致但需要注意数据类型和输入输出。#include iostream #include cmath // 用于 sqrt 函数 using namespace std; long long sumOfProperDivisors(int n) { // 处理边界情况n1 if (n 1) return 0; long long total 0; // 使用long long防止求和溢出 int limit sqrt(n); // 计算遍历上限 for (int i 1; i limit; i) { if (n % i 0) { // i是n的因子 total i; // 加上较小因子i int j n / i; // 对应较大因子j // 添加较大因子的条件不是同一个数且不是n本身 if (j ! i j ! n) { total j; } } } return total; } int main() { // 测试 int test_numbers[] {1, 2, 6, 12, 28, 496, 9}; for (int num : test_numbers) { long long result sumOfProperDivisors(num); cout 数字 num 的真因子之和为: result endl; } return 0; }C代码注意事项数据类型total使用了long long。这是非常重要的因为因子之和可能很大例如对于较大的n其因子和可能超过int的范围约21亿。使用long long可以避免溢出错误。sqrt函数sqrt(n)返回double类型赋值给int类型的limit时会自动截断小数部分得到不大于平方根的最大整数这正好符合我们的需求。循环条件for (int i 1; i limit; i)。这里i limit等价于i * i n。两种写法都可以但使用预先计算好的limit可能在某些编译器优化下稍快一点。条件判断if (j ! i j ! n)逻辑与Python版本完全一致。3.3 算法复杂度与性能分析让我们定量分析一下优化带来的巨大提升。朴素暴力法时间复杂度为O(n)。空间复杂度为O(1)。开方优化法时间复杂度为O(√n)。空间复杂度为O(1)。性能对比示例假设 n 1,000,000,000 (10亿)暴力法需要循环1,000,000,000次。优化法只需要循环sqrt(1e9) ≈ 31,623次。优化后的循环次数仅为暴力法的约0.003%效率提升了数万倍。这在竞赛的严格时间限制通常1秒或2秒下是AC与TLE的天壤之别。实操心得在竞赛中遇到涉及因子、约数、质数判断的问题首先要本能地想到O(√n)的优化思路。这几乎是一个条件反射。同时要特别注意数据范围如果题目给出的n最大可能是10^12甚至更大那么O(√n)的算法约10^6次循环也依然在安全范围内而O(n)的算法则完全不可行。4. 边界条件与特殊案例深度剖析一道题能否AC往往取决于对边界和特殊情况的处理是否周全。下面我们系统性地梳理一下这道题的所有“坑点”。4.1 数字1的处理这是最容易出错的地方。许多初学者会写出这样的循环total 0 for i in range(1, n): # 注意这里是 range(1, n)不包含n if n % i 0: total i return total当n1时range(1,1)是一个空区间循环直接跳过total保持为0。这段代码看似能正确处理n1。但是它牺牲了算法的统一性和效率使用了range(1,n)是O(n)复杂度。而我们优化后的算法循环条件是i*i n当n1时i11*1 1成立会进入循环。此时i1是因子j n//i 1如果不加特判根据if j ! i and j ! n的判断ji且jntotal不会被加任何数最终返回0。逻辑上似乎也对但这里有一个更隐蔽的问题在我们的优化算法中我们总是先total i。当n1i1时total先被加了1。然后计算j1因为j i所以不会执行total j。最终total1返回1这就错了因此必须在函数开始时显式特判n1的情况。这是最安全、最清晰的做法。它明确了“1没有真因子”这一数学定义避免了任何循环内的复杂判断。结论对于n1直接返回0。4.2 质数的处理质数是只有1和它本身两个因子的数。例如n7因子为1和7。根据题意排除本身后真因子只有1。所以对于任何质数p其真因子和都是1。我们的优化算法能正确处理吗以n7为例limit int(sqrt(7)) ≈ 2循环i从1到2。i1: 7%10,total1(total1)。j7。判断j!i(7!1) 且j!n(77)? 第二个条件不成立所以不执行totalj。i2: 7%2!0不执行。返回total1。正确。算法之所以能正确排除质数本身的因子“7”正是依靠了条件j ! n。对于质数在i1时j就等于n本身被成功过滤。4.3 完全平方数的处理完全平方数如n9, 16, 25等有一个重复的平方根因子。例如9的因子是1,3,9。真因子是1,3。我们的算法中条件j ! i就是为它们准备的。以n9为例limit int(sqrt(9)) 3i1: total1; j9; j!i成立但jn不添加。i2: 9%2!0。i3: 9%30, total3 (total4); j3; 此时j ! i不成立(33)所以不会执行totalj避免了因子3被重复累加。返回total4 (13)。正确。如果去掉j ! i这个条件当n9i3时total会先加3然后j也是3又会加一次3导致结果错误地变成7。4.4 大数与溢出问题这是一个隐含的“坑”。题目可能不会明确说n的范围有多大但我们要有防范意识。因子之和可能增长得很快。例如一个数的因子和可能接近甚至超过它本身对于“富足数”而言因子和大于本身。在C/C、Java等语言中使用int类型来存储总和可能溢出。例如假设n是一个较大的数其因子和超过了2^31-1用int存储就会变成负数导致结果错误。解决方案在不确定数据范围时使用更大范围的数据类型如C的long long(通常是64位)Java的longPython的intPython的int是任意精度无需担心。避坑技巧在竞赛中养成一个好习惯——阅读题目时首先关注数据范围。如果题目说“1 n 10^9”你就要估算一下因子和的最大可能值。一个粗略的估计是一个数的因子和不会超过它本身的几倍实际上对于大的n因子和的上限大约是 n * log(log(n)) 级别。对于n10^9因子和可能达到数十亿用int最大值约21亿是危险的用long long是稳妥的。在Python中则无需担心。5. 测试与调试实战指南写完代码不等于完事全面的测试是保证AC率的最后一道防线。我建议建立一个系统的测试流程。5.1 设计全面的测试用例集一个好的测试集应该覆盖所有路径和边界。针对本题我建议至少包含以下测试用例输入 (n)预期输出测试目的10最小输入边界情况21小质数31质数43完全平方数 (因子1,2,4 - 真因子1,2)66第一个完全数 (因子1,2,3,6 - 真因子1,2,3)1216普通合数 (因子1,2,3,4,6,12 - 真因子1,2,3,4,6)2828完全数94完全平方数 (因子1,3,9 - 真因子1,3)100117稍大的数验证计算正确性素数素数 (如10110310403)1101103205半质数因子较少你可以编写一个简单的测试函数来批量运行这些用例。def test_function(): test_cases [ (1, 0), (2, 1), (3, 1), (4, 3), (6, 6), (12, 16), (28, 28), (9, 4), (100, 117), (10403, 205) # 101*103 ] passed 0 failed 0 for n, expected in test_cases: result sum_of_proper_divisors(n) # 调用你的函数 if result expected: passed 1 print(f✓ PASS: n{n}, result{result}) else: failed 1 print(f✗ FAIL: n{n}, expected{expected}, got{result}) print(f\n测试结果通过 {passed}失败 {failed}) if __name__ __main__: test_function()5.2 常见错误与调试方法即使思路正确编码时也可能出现一些典型错误。下面列出几个我学生常犯的错循环条件错误for i in range(1, int(math.sqrt(n))):这里range的结束值是不包含的如果sqrt(n)恰好是整数就会漏掉这个平方根因子。应该用range(1, int(math.sqrt(n)) 1)或while i*i n。调试用完全平方数如4,9,16测试看结果是否正确。重复累加平方根忘记了if j ! i的判断导致完全平方数的平方根被加两次。调试测试n4预期结果是3(12)。如果得到4就是重复加了2。未能排除数字本身忘记了if j ! n的判断或者在累加i时没有考虑in的情况在暴力法中常见。调试测试任意一个数比如n6结果应该是6(123)。如果得到12就是把6本身也加进去了。整数溢出C/Java使用int存储总和对于大数输出负数或奇怪的值。调试用一个较大的、因子丰富的数测试比如n 几十万看结果是否合理。或者直接检查代码中的变量类型。输入处理错误题目可能是多组数据输入而你的代码只读了一组。或者没有处理输入结束标志。调试仔细阅读题目输入格式说明。如果是多组数据通常使用while循环读取直到文件结束(EOF)。可以本地用多行数据测试。调试心法当程序出错时不要漫无目的地看代码。首先构造一个最小的、能复现错误的测试用例。然后在关键位置打印中间变量比如每次找到因子i和j时打印它们的值以及当前的total。对比你的手动计算过程很快就能定位逻辑错误在哪里。对于边界情况主动去测试它比如n1而不是假设它“应该”是对的。6. 算法扩展与相关题型链接掌握了这道题的核心——O(√n)求一个数的所有因子你就解锁了一类题目的通用解法。下面我列举几个可以直接应用或稍作变形的相关题型帮助大家举一反三。6.1 判断完数、盈数、亏数完数Perfect Number一个数等于它的真因子之和。例如612328124714。我们的函数sum_of_proper_divisors(n)返回值如果等于n那n就是完数。盈数Abundant Number真因子之和大于它本身。判断条件是sum_of_proper_divisors(n) n。亏数Deficient Number真因子之和小于它本身。判断条件是sum_of_proper_divisors(n) n。很多基础题会要求判断一个数属于哪一类或者在一定范围内找出所有完数。6.2 求最大公约数(GCD)与最小公倍数(LCM)虽然求两个数的gcd有更高效的辗转相除法欧几里得算法但理解因子的概念是理解gcd的基础。两个数的gcd就是它们公共因子中最大的那个。而lcm(a, b) a * b / gcd(a, b)。6.3 判断两数是否互质如果两个数的最大公约数是1则它们互质。这可以通过检查它们是否没有公共的质因子除了1来实现本质上还是因子问题。6.4 求一个数的所有因子对有时题目要求列出所有因子对或者基于因子对进行某种计算。我们算法中(i, j)的生成过程就是在遍历所有因子对。你可以轻松地将它们存储到一个列表里。def get_factor_pairs(n): pairs [] i 1 while i * i n: if n % i 0: j n // i pairs.append((i, j)) i 1 return pairs6.5 更进一步的挑战预处理与筛法当题目要求对一个区间内所有数都求真因子和时比如“求1到N之间所有盈数的个数”如果对每个数都调用一次O(√n)的函数总复杂度是O(N√N)对于N10^5或10^6可能就有点吃力了。这时可以用类似埃拉托斯特尼筛法的思路进行预处理。我们换一个角度思考不是“找每个数的因子”而是“确定每个因子是谁的因子”。例如数字1是所有数的因子数字2是所有偶数的因子……我们可以初始化一个数组sum_div长度为N1全部为0因为真因子和至少从0开始。然后让i从1遍历到N/2因为一个大于N/2的数不可能是任何小于等于N的数的真因子除了它自己对于每个i把所有i的倍数jj 2*i, 3*i, ...且j N的sum_div[j]都加上i。这样循环结束后sum_div[n]里存储的就是n的真因子和。这种方法的时间复杂度是O(N log N)调和级数当N很大且需要查询很多次时比单独计算每个数更优。def precompute_sum_proper_divisors(limit): 预处理1到limit所有数的真因子和 sum_div [0] * (limit 1) for i in range(1, limit // 2 1): # i是因子 for j in range(i * 2, limit 1, i): # j是i的倍数 sum_div[j] i # i是j的真因子 return sum_div # 使用示例 N 10000 precomputed_sums precompute_sum_proper_divisors(N) # 查询n的真因子和直接访问 precomputed_sums[n] 即可时间复杂度O(1)这个筛法思路非常强大是解决密集区间数论问题的利器。理解了这个你对因子相关问题的认识就又深了一层。从一道简单的求因子和题目出发我们不仅学会了优化算法处理边界还延伸到了数论分类和筛法预处理这才是刷题锻炼思维的意义所在。下次再看到类似的题目希望你能够一眼看穿本质快速写出稳健高效的代码。
返回列表