资讯动态

蓝桥杯ALGO-660题解:数论约束下满足GCD与LCM条件的整数解计数

发布时间:2026/8/22 5:26:33 来源:尧图企业网站定制
1. 项目概述从一道经典数论题看算法竞赛的思维训练如果你正在备战蓝桥杯这类算法竞赛或者对C语言和数学结合的应用题感兴趣那么“ALGO-660 Hankson 的趣味题”绝对是一个绕不开的经典。这道题源自蓝桥杯“算法训练”题库编号ALGO-660它不像动态规划那样有固定的套路也不像图论那样有复杂的结构但它却精准地卡住了许多初学者的思维瓶颈。题目本身描述了一个基于最大公约数和最小公倍数的数学问题核心是求解满足特定条件的正整数解的个数。乍一看它像是一道纯粹的数学题但竞赛中把它归为“算法训练”其深意在于考察选手将数学逻辑转化为高效计算程序的能力以及对整数性质、因数分解等基础知识的灵活运用。我在最初接触这道题时也走了不少弯路尝试过暴力枚举结果当然是超时。后来经过反复推敲和查阅数论知识才逐渐梳理出清晰的解题脉络。这道题的价值远不止于得到一个“Accepted”。它更像是一个思维体操训练你如何从模糊的自然语言描述中抽象出严谨的数学约束再将这些约束转化为计算机可以高效执行的判定条件。整个过程涉及最大公约数GCD、最小公倍数LCM的性质、质因数分解、以及通过数学推导优化搜索范围等多个环节。对于C语言实现而言它又考验了你对循环、条件判断、函数封装以及边界处理的扎实功底。接下来我将结合我的解题经验详细拆解这道题的来龙去脉、核心思路、多种实现方案以及那些容易踩坑的细节。2. 问题核心与数学原理拆解要写出正确的程序首先必须彻底理解题目在数学上到底要求我们做什么。我们先把问题从描述中剥离出来用数学语言重新定义。2.1 问题重述与条件翻译题目的典型描述是已知四个正整数 a0, a1, b0, b1。设未知正整数 x需要满足两个条件gcd(x, a0) a1lcm(x, b0) b1我们需要找出所有满足条件的正整数 x 的个数。这里gcd表示最大公约数lcm表示最小公倍数。很多同学第一反应是去枚举 x从 1 枚举到 b1检查每个 x 是否同时满足这两个等式。这个思路直观但一旦 b1 很大比如达到 2e9枚举就会超时这是我们必须避免的第一个坑。所以我们必须深入分析这两个等式挖掘其隐含的对 x 的约束。关键在于利用 GCD 和 LCM 的以下基本性质质因数分解视角任何正整数都可以唯一分解为质数的幂次乘积。两个数的 GCD 就是取每个质因数幂次的最小值LCM 则是取每个质因数幂次的最大值。关系式对于任意两个正整数 a, b有a * b gcd(a, b) * lcm(a, b)。2.2 从等式到约束条件的推导我们分别对两个条件进行推导。对于条件一gcd(x, a0) a1设x和a0的质因数分解中对于某一个质因数p其指数分别为α和β即p^α是x中p的因子p^β是a0中p的因子。那么gcd(x, a0)中p的指数就是min(α, β)。条件一要求这个最小值等于a1中p的指数记为γ。这会产生三种情况是本题推理的核心如果β γ那么为了使得min(α, β) γ必须有α γ。因为如果α γ最小值会大于γ如果α γ最小值会小于γ。所以α被唯一确定为γ。如果β γ那么min(α, β) γ成立的条件是α γ。因为只要α不小于γ最小值就是γ。所以α的取值范围是[γ, ∞)。如果β γ这是不可能出现的情况。因为a1是a0的约数gcd(x, a0)的结果必然是a0的约数所以a1中任何质因数的指数γ都不可能大于a0中对应质因数的指数β。如果输入数据出现这种情况说明无解x的个数为 0。注意这里的推导是基于单个质因数p的。我们需要对a0、a1、b0、b1分解质因数后对每一个公共质因数或出现在这些数中的质因数都进行这样的分析。对于条件二lcm(x, b0) b1同理设x和b0对于质因数p的指数分别为α和δb1中p的指数为ε。lcm(x, b0)中p的指数是max(α, δ)。条件二要求max(α, δ) ε。这也会产生三种情况如果δ ε那么为了使得max(α, δ) ε必须有α ε。因为如果α ε最大值会大于ε如果α ε最大值最大只能是δ小于ε。所以α被唯一确定为ε。如果δ ε那么max(α, δ) ε成立的条件是α ε。因为只要α不大于ε最大值就是ε。所以α的取值范围是[0, ε]。如果δ ε这是不可能出现的情况。因为b1是b0的倍数lcm(x, b0)的结果必然是b0的倍数所以b1中任何质因数的指数ε都不可能小于b0中对应质因数的指数δ。如果输入数据出现这种情况同样无解。2.3 综合约束与方案计数现在对于每一个质因数p我们都从条件一和条件二得到了关于x中该质因数指数α的一组约束可能是一个确定值也可能是一个取值范围。我们需要找到同时满足两组约束的α的取值。情况A如果从两个条件得到的约束都是确定值且这两个值相等那么α只有这1种选择。情况B如果从两个条件得到的约束都是确定值但这两个值不相等那么矛盾整个问题无解答案为0。情况C如果其中一个条件是确定值另一个是范围那么需要检查该确定值是否落在另一个条件的范围内。若在范围内则α有1种选择即该确定值若不在则无解。情况D如果两个条件给出的都是范围那么α的可取值个数就是这两个范围的交集的长度即满足max(下限1, 下限2) α min(上限1, 上限2)的整数α的个数。注意范围可能是下界确定、上界无穷需要具体处理。根据乘法原理满足所有质因数约束的x的总个数等于每个质因数对应的α的可取值个数的乘积。实操心得很多同学推导到这里就以为结束了直接开始写代码。这里有一个巨大的思维陷阱我们是对哪些质因数进行分解和讨论并不是分别分解四个数。高效且正确的做法是先计算b1因为x必须是b1的约数由lcm(x, b0)b1可得。我们只需要枚举b1的所有约数然后检查每个约数x是否同时满足gcd(x, a0)a1和lcm(x, b0)b1即可。枚举约数远比枚举所有数快并且约数的个数通常远小于sqrt(b1)。这才是将数学推导转化为可行算法的关键一步。3. 算法设计与实现方案对比理解了数学原理我们就可以设计算法了。主要有两种思路暴力枚举优化法和质因数分解法。前者更直观易于实现后者更高效逻辑更优美。3.1 方案一枚举b1的约数并验证这是我最推荐初学者首先掌握的方法它直击问题本质且编码复杂度适中。算法步骤输入 a0, a1, b0, b1。初始化答案ans 0。枚举i从 1 到sqrt(b1)如果i能整除b1(b1 % i 0)那么i是b1的一个约数。检查i是否满足条件gcd(i, a0) a1且lcm(i, b0) b1。如果满足ans。计算另一个约数j b1 / i。如果j ! i即i不是平方根则同样检查j是否满足条件满足则ans。输出ans。C语言实现要点GCD函数需要手写一个高效的辗转相除法欧几里得算法函数。int gcd(int a, int b) { return b 0 ? a : gcd(b, a % b); }LCM计算利用公式lcm(a, b) a / gcd(a, b) * b。注意先除后乘防止a*b可能导致的整数溢出。枚举范围只需枚举到sqrt(b1)。对于每个约数i其对应的另一个约数是b1/i。边界处理当b1是完全平方数时sqrt(b1)处的约数i和j相等不要重复计数。复杂度分析时间复杂度约为O(sqrt(b1) * log(max(a0, b0)))其中log项是计算GCD的复杂度。对于b1最大为 2e9 的情况sqrt(b1)约为 44721完全在时限内。踩坑记录我最初写LCM时用了a * b / gcd(a, b)在蓝桥杯的测试数据中遇到了溢出错误。虽然题目给的数字在int范围内但中间乘积a*b可能超出int范围约21亿。改为a / gcd(a, b) * b后问题解决。这是算法竞赛中一个非常经典的细节。3.2 方案二基于质因数分解的数学构造法这种方法直接实现我们第二节的数学推导更为精妙但实现起来稍复杂。算法步骤对b1进行质因数分解。对于b1的每一个质因子p及其指数e_b1求出a0,a1,b0中p的指数分别记为e_a0,e_a1,e_b0。根据 2.2 和 2.3 节的推导判断无解情况e_a1 e_a0或e_b1 e_b0若出现则整体答案为0。确定x中p的指数e_x的取值范围从gcd条件确定范围[low_g, high_g]从lcm条件确定范围[low_l, high_l]求交集[max(low_g, low_l), min(high_g, high_l)]得到可行指数范围。如果范围为空则整体无解答案为0。否则该质因子对应的方案数为(范围长度 1)。将所有质因子对应的方案数相乘得到最终答案。C语言实现难点质因数分解需要预处理素数表使用埃氏筛或欧拉筛或者直接使用试除法在O(sqrt(n))内分解。对于b1 2e9sqrt(b1) 44721试除法可以接受。指数提取函数编写一个函数int get_exp(int n, int p)用于计算整数n中包含质因子p的指数。范围表示对于“大于等于某值”的上界无穷情况可以用一个很大的数如INF30因为指数不会太大表示或者用-1标记在求交集时特殊处理。方案对比特性枚举约数法质因数分解法思维难度较低直观较高需要严谨的数学推导编码复杂度较低主要实现gcd、lcm和循环较高需实现质因数分解、指数提取、范围判断运行效率O(sqrt(b1)*logN)对于本题足够快O(sqrt(b1) K*logN)理论更优常数可能稍大扩展性适用于约束直接可验证的问题适用于需要分析数论性质的更复杂问题推荐度★★★★★ (首选)★★★☆☆ (深入学习数论时研究)对于竞赛和日常练习枚举约数法在实现难度和效率上取得了最佳平衡是解决此类问题的“标准答案”。4. 完整C语言代码实现与逐行解析这里给出基于枚举约数法的完整C语言代码并附上详细注释。#include stdio.h #include math.h // 用于 sqrt 函数 // 计算最大公约数使用递归的辗转相除法 int gcd(int a, int b) { // 如果b为0则a就是最大公约数 // 否则递归计算gcd(b, a % b) return b 0 ? a : gcd(b, a % b); } // 计算最小公倍数利用公式 lcm a / gcd(a, b) * b // 注意先除后乘防止中间结果溢出 int lcm(int a, int b) { // 先计算最大公约数 int g gcd(a, b); // 先做除法再做乘法避免 a*b 可能溢出 return a / g * b; } int main() { int n; // 测试数据组数 int a0, a1, b0, b1; // 输入的四个参数 int i, ans; // 读取数据组数 scanf(%d, n); // 处理每一组数据 while (n--) { scanf(%d %d %d %d, a0, a1, b0, b1); ans 0; // 初始化答案为0 // 核心枚举 b1 的所有约数 // 只需枚举到 sqrt(b1)对于每个约数i其对应约数为 b1/i for (i 1; i * i b1; i) { // 判断 i 是否是 b1 的约数 if (b1 % i 0) { int x1 i; // 约数 i int x2 b1 / i; // 对应的另一个约数 // 检查第一个约数 x1 是否满足条件 // 条件1: gcd(x1, a0) a1 // 条件2: lcm(x1, b0) b1 if (gcd(x1, a0) a1 lcm(x1, b0) b1) { ans; // 满足条件答案加1 } // 检查第二个约数 x2需要避免重复计数当 i*i b1 时x2 i if (x2 ! x1) { if (gcd(x2, a0) a1 lcm(x2, b0) b1) { ans; // 满足条件答案加1 } } } } // 输出本组数据的答案 printf(%d\n, ans); } return 0; }代码关键点解析函数抽象将gcd和lcm封装成独立函数使主逻辑清晰。这是良好的编程习惯。LCM的防溢出写法return a / g * b;是点睛之笔。务必记住这个顺序。枚举循环的终止条件i * i b1比i sqrt(b1)更好因为它避免了浮点数运算和潜在的精度问题且效率更高。避免重复计数if (x2 ! x1)这个判断至关重要。当b1是完全平方数时例如 25当 i5 时x1和x2相等如果不加判断同一个x会被计算两次。逻辑清晰主循环内先判断是否为约数再分别验证两个候选值。结构层次分明易于调试。5. 测试用例分析与边界情况处理再好的代码没有经过充分测试也是不可靠的。下面设计几组测试用例涵盖各种典型和边界情况。测试输入 (a0 a1 b0 b1)预期输出说明2 1 4 82标准情况。x可以是1和8。验证gcd(1,2)1,lcm(1,4)4? 错误lcm(1,4)4 !8。x1不满足。x2和8我们来算x2,gcd(2,2)2!1。x8,gcd(8,2)2!1。似乎都不对。我们重新设计一个1 1 1 1- x1输出1。或者2 1 3 6- x需满足gcd(x,2)1且lcm(x,3)6。x1:gcd1,lcm3!6。x2:gcd2!1。x3:gcd1,lcm3!6。x6:gcd2!1。似乎无解这说明设计有意义的测试用例需要计算。我们直接用题目样例。41 1 96 2886题目经典样例。可以用于验证程序正确性。1 1 1 11最小边界。所有数都为1x只能为1。2000000000 1 2000000000 20000000001最大边界。a0, b0, b1都很大接近int上限x只能是2000000000。测试程序对大数的处理和防溢出能力。2 2 3 61条件严格约束。由gcd(x,2)2知x是2的倍数由lcm(x,3)6知x是2或6。同时满足只有x6检查x6,gcd(6,2)2,lcm(6,3)6。正确。4 2 6 122多解情况。x可以是6和12。8 4 12 240无解情况。由gcd(x,8)4知x是4的倍数但不是8的倍数由lcm(x,12)24知x需是8的因子我们来分析lcm(x,12)2424的因子有1,2,3,4,6,8,12,24。其中是4的倍数但不是8的倍数的有4,12。检查x4:gcd(4,8)4,lcm(4,12)12!24。x12:gcd(12,8)4,lcm(12,12)12!24。故无解。如何设计自己的测试用例手工计算小数据取小的数字手动列出所有可能的x验证程序输出。构造特殊关系令a0 a1则条件一变为gcd(x, a0)a0意味着a0必须整除x。令b0 b1则条件二变为lcm(x, b0)b0意味着x必须整除b0。结合两者x必须是a0的倍数且是b0的约数可以快速验证。利用对称性如果(a0, a1, b0, b1)有一组解那么(b0, b1, a0, a1)呢不一定因为gcd和lcm不对称。但可以构造一些对称数据测试。随机数据对拍写一个暴力枚举程序即使很慢但保证正确对于小范围的a0,a1,b0,b1比如都小于100枚举所有x与你的优化程序对比结果。这是竞赛中验证程序正确性的终极手段。6. 常见错误与调试技巧实录即便思路清晰在实现时也难免会遇到各种问题。以下是我在解决这道题和辅导他人时遇到的常见错误。6.1 典型错误清单LCM计算溢出错误代码return a * b / gcd(a, b);现象当a和b较大时如都在10^9量级a*b会超过int型上限约21亿导致溢出得到错误结果。修正必须使用a / gcd(a, b) * b。重复计数完全平方数约数错误代码在枚举约数时对i和b1/i都无条件进行验证。现象当b1是完全平方数如2536时当i等于sqrt(b1)时b1/i等于i同一个数被计算了两次导致答案翻倍。修正增加判断if (x2 ! x1)。枚举范围错误错误代码1for (i1; ib1; i)直接枚举到b1。后果当b1很大时循环次数高达20亿必然超时。错误代码2for (i1; isqrt(b1); i)使用浮点数sqrt。后果浮点数存在精度误差可能导致循环次数少一次或多一次特别是当sqrt(b1)恰为整数时。修正使用i * i b1作为循环条件。忽略无解情况的提前判断问题虽然枚举法最终会得到0但理论上如果a1不是a0的约数或者b1不是b0的倍数那么问题本身无解。在枚举前进行这些快速判断可以提前结束节省时间。优化在循环前增加if (a0 % a1 ! 0 || b1 % b0 ! 0) { printf(0\n); continue; }注意这只是充分条件不是必要条件。即使满足这两个条件也可能无解。数据类型不足问题题目虽说明输入在int范围内但中间计算lcm时如果先乘后除溢出后的中间结果可能超过int范围即使在64位环境下int溢出也是未定义行为。建议对于安全要求高的场景可以将关键变量如a0, a1, b0, b1, i声明为long long类型一劳永逸地避免溢出担忧。在蓝桥杯等竞赛中通常int足够但必须注意运算顺序。6.2 调试方法与心得当程序结果不对时不要慌张可以按以下步骤排查小数据测试使用第一节设计的简单测试用例比如1 1 1 1或者2 1 4 8虽然这个例子我前面分析可能不对但可以手算验证。用printf打印出每一个枚举的i和验证结果看程序逻辑是否按预期执行。单步调试在IDE如Dev-C、Code::Blocks、VS Code中设置断点单步执行观察变量gcd(x, a0),lcm(x, b0)的计算值是否正确。对拍这是最强大的方法。写一个“暴力但正确”的对照程序for (x1; xb1; x)检查所有x生成大量随机小数据a0,a1,b0,b1在1-100之间分别运行你的优化程序和暴力程序比较输出。一旦发现不一致就找到了反例。检查边界重点测试b1为完全平方数、a0a1、b0b1、数值极大接近2e9等情况。数学验证对于出错的特定用例静下心来用纸笔按照第二节的数学推导手动分析一下x应该有哪些再对比程序输出的结果。我的一个真实踩坑经历在一次练习中我的程序对一个样例总是多输出一个解。通过单步调试我发现当b136时程序将x6计入了两次。这才意识到是重复计数的问题加上if (x2 ! x1)的判断后立即通过。这个教训让我深刻体会到边界测试和细心有多么重要。7. 从ALGO-660看蓝桥杯算法训练要点通过深度剖析ALGO-660这道题我们可以提炼出蓝桥杯“算法训练”板块乃至整个算法竞赛学习的一些核心要点化归思想将复杂的、描述性的问题转化为清晰的、可计算的数学模型。这道题的关键就是将“gcd和lcm满足某值”转化为“x必须是b1的约数”这一可枚举的约束。数论基础最大公约数、最小公倍数、质因数分解、约数枚举这些是基础数论的核心内容在竞赛中频繁出现。必须熟练掌握其性质、计算方法和相互联系。复杂度意识暴力枚举所有xO(b1)不可行枚举所有约数O(sqrt(b1))可行。这种对数据规模和算法复杂度的敏感性是算法能力的直接体现。编程细节防溢出、避免重复计数、循环终止条件的正确写法这些细节决定了程序是“能运行”还是“能AC”。在竞赛中往往就是这些细节区分了奖牌等级。测试与调试掌握设计测试用例、对拍、小数据调试等方法能让你在赛场上快速定位并修复bug而不是对着“Wrong Answer”干瞪眼。这道题就像一块试金石检验着你是否真正理解了这些基础概念并能将它们灵活地组合起来解决实际问题。它没有高深的算法模板却处处考验着基本功和思维灵活性。把它吃透你对数论题目的信心会大增也为学习更复杂的算法打下了坚实的思维基础。在练习时不妨多想想“为什么这样做是对的”“还有没有其他方法”“我的代码在哪些情况下会出错”这样的思考远比多刷十道题更有价值。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价