蓝桥杯算法题解:贪心策略与质因数分解求最大分解和

发布时间:2026/8/28 21:52:09
蓝桥杯算法题解:贪心策略与质因数分解求最大分解和 1. 项目概述与问题引入“最大分解”这个问题乍一看标题可能会让人联想到数学里的质因数分解或者整数拆分。但当你点开题目看到“ALGO-994”这个编号再结合“蓝桥杯集训”这个背景你就知道这绝对不是一道简单的数学题而是一道典型的算法竞赛题目考察的是选手对贪心算法、数论基础以及问题转化能力的掌握。我参加过不少算法竞赛也带过一些学生备赛这类题目往往是区分选手水平的关键——思路对了代码可能就十几行思路卡住了可能调试一整天都过不了。这道题的核心描述通常是给定一个正整数n你需要对其进行一系列操作。每次操作是找到n的最大的、不等于它自身的因数x然后用x替换n并将这个x累加到一个总和sum中。重复这个过程直到n变为 1。题目要求的就是这个累加和sum的最大值。举个例子如果n 12它的最大真因数是 6所以第一步变成 6sum66的最大真因数是 3第二步变成 3sum6393的最大真因数是 1第三步变成 1sum9110。所以对于12这个和是10。但问题没那么简单因为题目要求的是最大可能的和。这意味着在寻找最大真因数时我们可能不是每次都选“数学上”的最大真因数而是为了最终的总和最大可能需要做出不同的选择。这立刻就把问题从一个简单的模拟提升到了一个需要策略和证明的优化问题。它适合所有正在准备算法竞赛尤其是蓝桥杯、ACM的同学无论是想巩固贪心思想还是想挑战一下自己的思维严密性这道题都是一个绝佳的练手材料。接下来我们就一层层剥开这道题的外壳看看它到底在考什么以及如何系统地解决它。2. 核心思路解析与贪心策略证明拿到这道题第一反应可能是暴力搜索或者动态规划。但仔细分析操作过程你会发现它有一个非常特殊的性质每次操作后新的n值即我们找到的因数x一定是旧n值的约数并且比旧n小。整个操作序列构成了一条从初始n下降到 1 的路径路径上的每个点都是前一个点的约数。那么如何让路径上所有点的和最大呢直觉告诉我们应该让路径“下降得尽可能慢”也就是每次选择的因数x要尽可能大这样x本身的值大被加到总和里贡献就大而且后续操作的起点也更高。这个“每次选最大”的直觉就是贪心算法的核心。我们需要证明对于本题每次选择当前数字的最大真因数能得到全局最优解即总和最大。我们来尝试证明一下。假设当前数字是a它的真因数集合为{d1, d2, ..., dk}且d1 d2 ... dk那么dk就是最大真因数。设S(a)表示从a开始按最优策略操作能得到的总和。我们声称最优策略就是选择dk即S(a) dk S(dk)。用反证法。假设存在一个更优的策略第一步不选dk而是选了另一个较小的因数di (i k)。那么这条路径的总和是di S(di)。由于di dk并且di和dk都是a的因数那么di也一定是dk的因数吗不一定但它们之间存在关系因为di和dk都是a的因数所以gcd(di, dk)也是a的因数。更关键的是从di开始的路径其每一步得到的数都必然小于等于di。而从dk开始的路径第一步就得到了一个比di更大的数dk。由于整个求和过程是单调递减的数字越来越小从更大的起点dk开始其后续所有可能的数字都不会小于从di开始路径中对应位置的数字因为起点更高且每次操作都是找自己的因数下降空间更大。更严谨地说可以构造从S(dk)到S(di)的映射证明S(dk) S(di)。因此dk S(dk) di S(di)。这说明选择最大真因数dk不会比选择任何其他真因数di更差。因此贪心选择是安全的。这个证明的关键在于理解“从更大数字开始其后续最优和也不小于从较小数字开始的最优和”。在竞赛中我们通常不需要在代码里写这么严格的证明但必须想通这个道理否则写代码心里没底。实际上对于这类“每次操作将数变为它的一个因数”的问题贪心取最大因数往往就是最优的因为它最“延缓”了数变小的过程让大的数值尽可能多地被加入总和。3. 算法实现与细节剖析思路清晰了接下来就是实现。实现可以分为两个层面一是模拟贪心过程二是高效地寻找最大真因数。3.1 模拟贪心过程框架这个过程非常直接就是一个循环。def max_decomposition_sum(n): total_sum 0 while n 1: # 找到 n 的最大真因数 x x find_max_proper_divisor(n) total_sum x n x # 用因数替换当前数 return total_sum核心函数find_max_proper_divisor(n)负责找到除了n本身之外的最大因数。3.2 寻找最大真因数的高效算法最笨的方法是遍历从n-1down to 2看哪个数能整除n。当n很大时比如n10^9这显然是行不通的。我们必须利用因数的性质。一个数n的最大真因数是多少如果n是质数那么它的最大真因数就是 1因为质数只有 1 和它本身两个因数。如果n是合数那么它的最大真因数可能是n / p其中p是n的最小质因数。为什么因为对于任意一个因数d都有对应的n/d也是因数。为了让n/d最大就需要d最小。而最小的正因数除了1就是最小质因数。因此算法可以优化为如果n是质数直接返回 1。否则找到n的最小质因数p返回n // p。现在问题转化为如何高效地找到一个数的最小质因数对于单次查询我们可以用试除法。试除法找最小质因数def find_min_prime_factor(n): if n % 2 0: return 2 # 只需检查到 sqrt(n) 即可 i 3 while i * i n: if n % i 0: return i i 2 # 跳过偶数 # 如果没找到说明 n 本身是质数 return n在贪心循环中n会不断变小。但即使这样如果初始n很大且是一个质数的平方之类循环次数虽然不多但每次寻找最小质因数仍需 O(√n) 的时间在极端情况下可能超时例如n是一个接近10^9的大质数第一次找就要循环约3万次但之后很快变成1。有没有更高效的方法注意到我们的操作序列n - n/p1 - (n/p1)/p2 - ...。这其实就是对n进行质因数分解并按照从大到小的顺序重组因子的过程。我们最终的和其实就是所有非1的中间状态的和。我们可以直接通过质因数分解来计算这个和而无需模拟。更进一步的优化思路 设n的质因数分解为n p1^a1 * p2^a2 * ... * pk^ak其中p1 p2 ... pk。 贪心过程实际上是第一步找到最小质因数p1n变为n / p1将n / p1加入总和。新的n为n / p1其最小质因数可能是p1(如果a1 1) 或者是p2。这个过程持续下去每次都是除以当前数的最小质因数。那么最终的总和S等于什么呢我们可以通过观察发现S (n / p1) (n / (p1^2)) ... (n / (p1^a1)) (n / (p1^a1 * p2)) ...。换句话说就是n不断除以它的最小质因子直到除尽该因子然后继续除以下一个最小质因子将每次除后的商累加起来直到商为1。这给了我们一个极其高效的算法对n进行质因数分解得到质因子列表可以重复如[2, 2, 3, 5]。将质因子按从小到大排序。初始化current ntotal_sum 0。遍历排序后的质因子列表对于每个质因子pcurrent current // ptotal_sum current遍历结束后total_sum即为答案。为什么因为每次除以最小的质因子就相当于模拟了贪心过程中“用最大真因数替换”的操作。current // p就是当前n的最大真因数因为p是最小质因子。这个算法的时间复杂度取决于质因数分解的速度最优可以做到 O(√n)但实际因为n下降很快效率比直接模拟贪心每次试除要高得多。3.3 代码实现与对比我们来实现上述两种方法并对比其效率。方法一模拟贪心 每次试除找最小质因子def max_sum_simulation(n): total 0 while n 1: # 找最小质因数 p n if n % 2 0: p 2 else: i 3 while i * i n: if n % i 0: p i break i 2 # 计算最大真因数并更新 x n // p total x n x return total方法二质因数分解后直接计算def max_sum_fast(n): factors [] temp n # 分解质因数 i 2 while i * i temp: while temp % i 0: factors.append(i) temp // i i 1 if i 2 else 2 # 2之后只检查奇数 if temp 1: factors.append(temp) # 剩余的大质数 factors.sort() # 其实按分解顺序添加自然就是从小到大除了最后的大质数 total 0 current n for p in factors: current // p total current return total我们来测试一下n 12方法一过程n12, p2, x6, total6, n6 - n6, p2, x3, total9, n3 - n3, p3, x1, total10, n1结束。方法二分解12得到[2, 2, 3]。current初值12。p2: current12//26, total6p2: current6//23, total9p3: current3//31, total10 结果一致。对于n17(质数)方法一p17, x1, total1, n1结束。方法二分解17得到[17]。current17//171, total1。 结果一致。效率对比当n是一个很大的质数时方法一在第一次循环中while i*i n这个循环要执行大约 √n 次然后x n // n 1总共就一次循环。方法二在分解质因数时同样要执行 √n 次循环才能确定它是质数。看起来一样。但当n是一个合数且具有很多小的质因子时比如n 2^10 * 3^5 248832方法一在每次循环中都要重新从2开始试除尽管n在快速变小但前期仍然有开销。方法二则是一次性分解完毕然后进行简单的除法累加后者通常更稳定、更快。在竞赛中推荐使用方法二思路更清晰代码更简洁且不易出错。4. 边界条件与常见陷阱即使算法正确忽略边界条件和一些隐藏陷阱也会导致丢分。下面是我在实战和教学中总结的几个关键点。4.1 输入范围与数据类型首先一定要明确题目给定的n的范围。蓝桥杯的题目通常会在描述里给出比如1 n 10^6或者1 n 10^9。这直接影响我们的算法选择和数据类型。如果n 10^6那么使用 O(√n) 的试除法完全没问题。如果n 10^9O(√n) 在最坏情况下n是质数大约是 3万多次循环在时间限制内通常也是可以接受的C/Java肯定可以Python需要稍加优化比如只循环到int(sqrt(n))且步长为2。如果n可能更大比如10^12那么就需要更高效的分解算法如 Pollard-Rho但这超出了本题的一般范围。数据类型在 Python 中整数可以自动处理大数不用担心溢出。但在 C/Java 中要注意n和累加和total_sum的范围。total_sum可能比n大吗我们来看每次加的都是n的真因数所以每次加的数都小于当前的n。最坏情况是n每次只减少一点点比如n是质数总和就是1。如果n是像2^k这样的数总和是n/2 n/4 ... 1 n - 1。所以total_sum的最大值小于n。因此用和n相同的数据类型如int或long long存储total_sum是安全的。4.2 对质数和非质数的处理这是最容易出错的地方之一。一定要理解当n为质数时它的最大真因数是 1。在我们的质因数分解算法中如果n是质数那么factors列表里就只有n本身。循环current // p就会得到current // n 1总和就是1。这是正确的。但在模拟贪心的方法中找最小质因数的函数如果写得不小心可能会陷入死循环或返回错误值。例如# 错误示例未处理质数情况 def find_max_divisor_wrong(n): for i in range(n-1, 1, -1): # 从大到小遍历 if n % i 0: return i # 如果循环结束都没找到即n是质数函数没有返回值正确的做法是在循环结束后返回 1。def find_max_divisor_correct(n): for i in range(n-1, 1, -1): if n % i 0: return i return 1 # 质数情况4.3 循环终止条件在模拟贪心过程中循环条件是while n 1。当n变为 1 时停止。这里要确保你的更新逻辑能让n最终变为 1。对于质数n会一步变为 1。对于合数每次n都会变成它的一个真因数所以n严格递减最终必然会达到 1。在质因数分解直接计算的算法中循环遍历所有质因子即可结束后current必然为 1。4.4 算法正确性验证暴力打表对拍对于贪心算法虽然我们进行了推理证明但在竞赛中对于不确定的题目一个非常实用的技巧是暴力打表对拍。即写一个暴力搜索所有可能路径的程序通常用 DFS求出小范围n比如 1 到 1000的确切最优解然后与你贪心算法的结果进行对比。如果全部一致你就能大大增加对贪心策略正确性的信心。暴力搜索的代码效率很低仅用于小范围验证def brute_force(n, memo): if n 1: return 0 if n in memo: return memo[n] max_sum 0 # 找出所有真因数 for d in range(1, int(n**0.5)1): if n % d 0: if d ! 1 and d ! n: # d是真因数 max_sum max(max_sum, d brute_force(d, memo)) other n // d if other ! 1 and other ! n and other ! d: # other也是真因数 max_sum max(max_sum, other brute_force(other, memo)) # 如果n是质数只有真因数1 if max_sum 0: max_sum 1 brute_force(1, memo) # 其实就是 1 0 memo[n] max_sum return max_sum # 测试对比 for i in range(2, 50): bf brute_force(i, {}) gf max_sum_fast(i) if bf ! gf: print(f不一致: n{i}, 暴力{bf}, 贪心{gf})运行这段代码你会发现对于测试范围内的所有i贪心算法的结果都和暴力搜索的最优解一致。这给了我们实践上的信心。5. 性能优化与代码技巧在算法竞赛中正确性只是第一步性能往往决定能否通过所有测试点。针对这道题我们可以从以下几个方面进行优化。5.1 质因数分解的优化这是整个算法性能的关键。标准的试除法是从 2 遍历到 √n。def factorize(n): factors [] d 2 while d * d n: while n % d 0: factors.append(d) n // d d 1 if n 1: factors.append(n) return factors这个代码可以优化单独处理 2先判断并除尽 2这样后续循环可以只遍历奇数步长变为 2。使用int(n**0.5)作为上限在循环条件中重复计算d*d不如先计算limit int(n**0.5)但要注意n在循环中是变化的所以每次迭代还是需要重新判断d*d n。不过对于 Pythond*d的计算开销很小影响不大。提前终止当d大于当前n的平方根时n一定是质数可以提前结束。优化后的版本def factorize_optimized(n): factors [] # 处理因子2 while n % 2 0: factors.append(2) n // 2 # 处理奇数因子 d 3 while d * d n: while n % d 0: factors.append(d) n // d d 2 # 如果剩余部分大于1则为质数 if n 1: factors.append(n) return factors对于像n10^9-1这样的数优化后的版本能减少近一半的循环次数。5.2 避免不必要的排序在我们的“质因数分解直接计算”算法中我们需要质因子按从小到大排列。但注意我们分解的过程我们先除尽所有 2然后从 3 开始每次加 2。这样得到的factors列表自然就是从小到大排列的除了最后可能有一个大质数。例如n60分解过程除尽2得到[2,2]n15然后从3开始15%30得到[2,2,3]n55%50得到[2,2,3,5]。列表已经有序。因此我们不需要显式调用factors.sort()这可以节省 O(k log k) 的时间虽然 k质因子个数通常很小但也是一个好的习惯。5.3 整合计算过程节省空间我们甚至不需要显式存储质因子列表。可以在分解质因数的同时就完成累加计算。def max_sum_optimal(n): total 0 current n # 处理因子2 while current % 2 0: current // 2 total current # 处理奇数因子 d 3 while d * d current: while current % d 0: current // d total current d 2 # 处理剩余的大质数如果current1那么它一定是质数 if current 1: # 此时 current 就是最后一个质因子 total 1 # current // current 1 return total这个版本更加高效它省去了列表的构建和遍历边分解边累加空间复杂度 O(1)。注意处理最后剩余质数的那一步如果current 1说明它本身就是一个质数可能是初始的n是质数也可能是分解到最后剩下的一个质因子。按照我们的算法需要执行current // current得到 1 并累加。所以total 1。5.4 使用位运算加速在 C 等语言中可以使用位运算来判断奇偶、计算平方等但在 Python 中这些优化效果微乎其微有时甚至更慢因为 Python 的解释开销远大于这些低级操作。所以对于 Python 实现保持代码清晰易读更重要。6. 测试用例与调试心得设计全面的测试用例是验证代码正确性的重要环节。以下是一些关键的测试场景最小输入n 1。根据题目操作直到n变为 1那么对于n1无法进行任何操作总和应为 0。但题目通常保证n 1不过为了代码健壮性可以处理一下。质数n 17,n 2,n 3。结果都应该是 1。2的幂次n 8 (2^3)。过程8-4-2-1。总和 4217 8-1。n16总和84211516-1。可以总结规律对于n2^k总和为n-1。平方数n 36 (2^2 * 3^2)。分解质因数[2,2,3,3]。计算36/218(sum18), 18/29(sum27), 9/33(sum30), 3/31(sum31)。可以手动模拟验证。包含大质数的合数n 2 * 998244353假设 998244353 是质数。过程先除以最小质因子2得到 998244353总和加上这个数然后 998244353 是质数再除以它自身得到1总和加1。最终总和 998244353 1。这个用例测试了算法对大质因数的处理。连续乘积n 2*3*5*7 210。分解为[2,3,5,7]。计算210/2105(sum105), 105/335(sum140), 35/57(sum147), 7/71(sum148)。调试心得打印中间过程在开发时可以在循环中打印出当前的n、找到的质因子p、计算出的x以及当前的total这能帮你快速定位逻辑错误。例如如果你发现n没有按预期减小或者total累加不对就能马上看到是哪一步出了问题。专注于边界很多错误发生在边界情况比如n1,n2,n是质数n是完全平方数等。单独测试这些用例。对比暴力法如前所述对于小数据用暴力搜索的结果来验证优化算法的正确性是确保贪心策略正确的“金标准”。7. 总结与扩展思考经过上面的分析我们把“最大分解”这道题从问题理解、贪心证明到算法实现、细节优化、测试调试完整地过了一遍。这道题的核心价值在于它将一个看似复杂的操作序列优化问题通过深入分析转化为了一个清晰的贪心策略并进一步转化为高效的质因数分解问题。回顾一下关键点每次选择当前数的最大真因数即除以最小质因数这个贪心策略能得到全局最优解。实现上最优雅高效的方法是对n进行质因数分解然后从小到大依次除以每个质因子并将每次的商累加。扩展思考如果操作可以任意选择真因数而不是必须选最大的求所有可能操作序列中总和的最小值是多少这个问题就变成了每次选择最小的真因数1除外那显然就是每次都除以最大的质因数让数下降得最快。总和就是n / pk n/(pk*pk-1) ...其中pk是最大质因数。如果每次操作可以将n替换为它的任意一个真因数但目标不是总和最大而是要求操作次数最少或最多这就变成了图论中的路径问题。最少次数显然是每次除以最大质因数路径最短。最多次数则是每次除以最小的质因数2路径最长。蓝桥杯真题的变形蓝桥杯经常会在原题基础上稍作修改。比如可能不是求和而是求最后的n变成 1 时所有被替换掉的数的乘积。或者可能规定每次不能选择之前选过的因数。这就需要我们灵活运用从这道题中学到的分析问题的方法——寻找不变量或最优子结构。这道题虽然标为“ALGO”算法训练但其蕴含的思想对于解决更复杂的动态规划、图论问题都有启发。它告诉我们面对一个多步决策问题不要急于上马写搜索或DP先冷静分析操作的性质看看有没有贪心选择策略或者能否将问题转化为更简单的数学模型。这种“转化”的能力是算法竞赛中更高级、更重要的能力。