ARTICLE DETAIL

资讯详情

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

蓝桥杯算法精解:从整除与贪心策略到“幸运店家”问题

蓝桥杯算法精解:从整除与贪心策略到“幸运店家”问题 1. 问题引入当“满减”遇上“幸运数字”最近在整理蓝桥杯的算法训练题时又遇到了ALGO-985这道“幸运的店家”。题目本身描述很简单但每次重做都能发现一些新的思考角度。它本质上是一个结合了整除、贪心策略和边界条件处理的数学问题非常考验解题者对问题本质的抽象能力和代码实现的严谨性。题目大意是作为店家你有一件商品原价N元。你的促销策略是如果顾客购买的总价恰好是3的倍数那么他就可以享受免单即只支付总价的2/3。顾客为了享受这个优惠可能会选择多买几件同样的商品使得总价是3的倍数。现在顾客想用最少的钱买到至少一件商品请问他最少需要支付多少钱举个例子如果商品单价N4元。顾客直接买一件价格4不是3的倍数无法优惠需付4元。如果买两件总价8元也不是3的倍数。如果买三件总价12元是3的倍数可以享受优惠实际支付12 * 2 / 3 8元。但8元比直接买一件的4元要贵所以顾客不会为了优惠而多买。因此最少支付金额就是4元。这个场景是不是很像电商平台常见的“满X减Y”或者“满X打Z折”只不过这里的“满额条件”非常特殊总价必须是3的倍数。顾客需要在“多花钱凑单享受折扣”和“直接原价购买”之间做出最优选择。我们今天就来彻底拆解这个问题从最朴素的暴力枚举到数学推导出最优策略最后给出无懈可击的代码实现。无论你是C选手还是C语言爱好者都能从中找到清晰的解题路径。2. 核心逻辑拆解为什么不能简单粗暴地枚举拿到这个问题很多人的第一反应是这不就是一个简单的循环枚举吗让购买数量k从1开始不断增加计算总价total k * N然后检查total % 3 0是否成立。如果成立就计算优惠后价格pay total * 2 / 3并记录最小值。直到k大到一定程度比如k3因为3是周期或者pay已经不可能比直接买一件更便宜时停止。这个思路方向是对的但直接实现会掉进几个大坑。我们先来写一个最直观的错误示范看看问题出在哪里。#include stdio.h #include limits.h int wrong_solution(long long N) { long long min_pay LLONG_MAX; // 初始化一个很大的数 for (long long k 1; k 3; k) { // 天真地认为最多买3件 long long total k * N; if (total % 3 0) { long long pay total * 2 / 3; if (pay min_pay) { min_pay pay; } } } // 如果没找到优惠方案就按原价买一件 if (min_pay LLONG_MAX) { min_pay N; } return min_pay; }这个代码至少有三处致命错误循环边界k3是武断的凭什么认为最多买3件如果N1元买一件总价1非3倍数两件总价2非3倍数三件总价3是3倍数优惠后付2元。但有没有可能买4件更便宜4件总价4非3倍数不行。5件总价5不行。6件总价6是3倍数优惠后付4元比2元贵。看起来k3确实是最优。但如果N5呢买1件付5元买2件总价10非3倍买3件总价15是3倍优惠后付10元比5元贵买4件总价20非3倍买5件总价25非3倍买6件总价30是3倍优惠后付20元更贵。似乎也没问题。但我们需要一个严谨的证明而不是猜测。实际上对于某些巨大的N可能最优的k远大于3。整数溢出问题题目中N的范围没有明确给出但在蓝桥杯系统中通常可能达到10^9甚至10^18级别取决于题目具体描述但我们必须考虑大数。k * N这个乘法非常危险。如果N很大k也很大乘积很可能超过long long通常是2^63-1约9.2e18的范围导致溢出计算结果完全错误。这是算法题中最常见的“暗坑”之一。忽略了“至少买一件”的前提我们的循环从k1开始这没问题。但关键在于即使找到了一个优惠方案我们也要和“只买一件原价”的方案进行比较。上面的代码虽然做了比较但它的搜索空间可能不完整导致找不到真正的最优解。那么正确的思路是什么我们不能无脑枚举k因为k可能非常大。必须通过数学分析缩小搜索范围或者直接找到最优解的计算公式。3. 数学推导寻找最优购买数量的规律让我们暂时忘记代码用数学思维来重新审视问题。设商品单价为N购买数量为kk 1。总消费原价T k * N优惠条件T % 3 0实际支付P T * 2 / 3 (2 * k * N) / 3顾客的目标是在所有满足T % 3 0的正整数k中找到使P最小的那个并且最终答案ans min(P, N)因为也可以选择不享受优惠直接原价买一件。由于P (2 * k * N) / 3且k * N是3的倍数所以P必然是一个整数。要使P最小就是要在满足k * N % 3 0的条件下让k尽可能小吗不一定因为P和k是成正比的P (2N/3) * k所以在满足优惠条件的前提下购买数量k越小实际支付P就越小。这个结论是直观的既然优惠是比例折扣2/3那么多买只会多花钱凑单只是为了获得折扣资格。因此问题转化为寻找最小的正整数k (k1)使得k * N是3的倍数。找到这个k_min后计算P_min (2 * k_min * N) / 3最终答案就是min(P_min, N)。现在我们集中火力解决给定N求最小的正整数k使得(k * N) % 3 0。根据模运算的性质(a * b) % m ((a % m) * (b % m)) % m。所以(k * N) % 3等价于((k % 3) * (N % 3)) % 3。N除以3的余数只有三种可能0, 1, 2。我们分情况讨论情况1N % 3 0这意味着N本身就是3的倍数。那么当k1时1 * N就是3的倍数。所以最小的k就是1。 此时P (2 * 1 * N) / 3 (2N)/3。 因为N是3的倍数设N3t则P2t。显然P2t 3t N因为t0。 所以最终答案就是(2N)/3。顾客直接买一件就能享受优惠而且比原价便宜。情况2N % 3 1设 N 3t 1。 我们需要(k * (3t1)) % 3 0即(k * 1) % 3 0因为3t项模3为0。 所以问题变成k % 3 0。 最小的正整数k满足k%30那就是k3。 此时P (2 * 3 * N) / 3 2N。 我们需要比较P2N和原价N。显然2N N。 所以在这种情况下顾客凑单买3件享受优惠后实际要付2倍的原价反而更亏了因此理性的顾客会选择直接原价购买一件支付N元。结论当N%31时答案就是N。情况3N % 3 2设 N 3t 2。 我们需要(k * (3t2)) % 3 0即(k * 2) % 3 0。 这意味着(2k) % 3 0。由于2和3互质这等价于k % 3 0。 同样最小的k是3。 此时P (2 * 3 * N) / 3 2N。 比较P2N和原价N依然是2N N。 所以凑单享受优惠同样不划算。结论当N%32时答案也是N。等等这里似乎有问题而且是一个经典的思维陷阱在情况2和情况3中我们默认了k必须是3的倍数。但是k一定要是3吗有没有更小的k重新审视条件我们需要(k * N) % 3 0。当N % 3 1时条件为(k * 1) % 3 0k % 3 0。k必须是3的倍数3确实是最小的。当N % 3 2时条件为(k * 2) % 3 0。我们解这个同余方程2k ≡ 0 (mod 3)。因为2在模3下的逆元是2因为2*24≡1 mod 3两边乘以2得k ≡ 0 (mod 3)。所以k仍然必须是3的倍数3是最小的。推导没错。那么有没有可能k1.5不行k必须是整数。有没有可能k2代入N%322 * 2 4, 4 % 3 1不满足。k2不行。所以从数学上看当N不是3的倍数时想要总价是3的倍数最少需要购买3件。但支付金额2N大于原价N所以顾客不会选择优惠方案。但是这里存在一个极其重要的边界情况我们整个推导基于一个假设顾客购买的数量k是正整数。如果顾客可以购买非整数件吗显然不行。但如果N本身很小导致2N优惠后支付虽然大于N但如果我们买更多件呢比如k6, 9...因为P 2N * (k/3)k越大P越大所以更不可能比N小。因此当N%3!0时直接原价购买就是最优解。然而我们忽略了一个至关重要的现实因素顾客必须支付整数金额。在计算P 2N * k / 3时由于k*N是3的倍数所以P一定是整数。但是当我们比较P和N时我们是在比较两个整数。有没有可能P虽然大于N但P是顾客为了获得商品必须支付的金额不对顾客还有“直接原价买一件”的选项。所以比较是有效的。真正的坑在于情况1的变形当N % 3 0时我们得出答案P 2N/3。但是如果2N/3这个值因为整数除法的问题计算不准确怎么办例如N32N/3 2正确。但如果N很大2N可能溢出吗我们在代码中必须小心。此外还有一个更隐蔽的坑当N0时怎么办虽然题目中商品价格N应该是正整数但严谨的编程习惯要求我们考虑边界。如果N0商品白送无论买多少件都是0元。但题目要求至少买一件所以支付0元。不过通常题目会保证N1。4. 算法实现与代码细节从C到C的避坑指南经过数学分析我们得到了一个极其简洁的结论若N % 3 0则答案为2 * (N / 3)。否则答案为N。注意计算2 * (N / 3)而不是(2 * N) / 3这样可以先做除法避免2*N可能导致的溢出尽管在N%30时N/3是整数但2*(N/3)仍在范围内而(2N)/3可能溢出。这是一个重要的优化。现在我们来用代码实现。这里分别给出C语言和C的版本并详细解释每一个细节。4.1 C语言实现注重可移植性与健壮性#include stdio.h int main() { long long N; // 使用 long long 类型存储防止大数溢出 scanf(%lld, N); long long ans; if (N % 3 0) { // 关键先除后乘避免中间结果溢出 ans 2 * (N / 3); } else { ans N; } printf(%lld\n, ans); return 0; }代码细节剖析数据类型选择使用long long通常是64位有符号整数来存储价格N和结果ans。这是处理蓝桥杯算法题中大数据范围的标配。即使用不到那么大的数养成这个习惯也能避免很多潜在的溢出问题。输入输出格式符对应为%lld。核心逻辑直接对应我们推导出的数学结论。分支清晰没有冗余循环。运算顺序优化ans 2 * (N / 3)是点睛之笔。如果写成ans (2 * N) / 3当N接近long long最大值的一半时2*N就会溢出导致未定义行为。而N/3的结果一定在long long范围内再乘以2安全性高得多。这是一种非常实用的防溢出技巧。边界情况这段代码对于N0也能正确处理输出0。对于N为负数呢题目通常约定价格为正整数所以可以不加处理。如果为了健壮性可以在输入后加一句if (N 0) return 0;。4.2 C实现利用语言特性与输入输出加速#include iostream using namespace std; int main() { // 关闭同步提升cin/cout速度适用于大量数据输入输出 ios::sync_with_stdio(false); cin.tie(nullptr); long long N; cin N; long long ans; if (N % 3 0) { ans 2 * (N / 3); } else { ans N; } cout ans endl; return 0; }C版本特有细节输入输出流加速ios::sync_with_stdio(false);和cin.tie(nullptr);是C竞赛编程的“起手式”。它们的作用是解绑C的cin/cout与C的stdio的同步并解除cin与cout的绑定可以大幅提升输入输出效率。在处理大量数据时效果显著。代码简洁性逻辑与C语言版本完全一致但使用了cin和cout对于熟悉C的开发者来说更自然。关于endl这里使用endl输出换行并刷新缓冲区。在输出量不大时没问题。如果是超大规模输出为了极致性能可以改用\n因为endl的刷新缓冲区操作有开销。本题单次输出影响可忽略。4.3 错误解法对比与深度测试为了加深理解我们构造几组测试数据对比正确解法和几种常见错误解法。测试输入(N)预期输出错误解法1 (循环k3)错误解法2 (直接算2N/3)错误解法3 (忽略溢出)111 (正确)0 (错误)1 (正确)322 (正确)2 (正确)2 (正确)444 (正确)2 (错误)4 (正确)100000000010000000001000000000 (正确)666666666 (错误)1000000000 (正确)333333333222222222222222222 (正确)222222222 (正确)222222222 (正确)9223372036854775803 (接近LLONG_MAX)6148914691236517202溢出/错误溢出/错误溢出/错误错误解法2分析直接算2N/3。当N4时2*4/38/32整数除法。它错误地认为买一件4元的商品可以只付2元这显然违背了优惠规则总价4不是3的倍数不能优惠。这种解法混淆了“求满足条件的最小支付”和“对任意数量都打折”的区别。错误解法3分析if(N%30) ans2*N/3; else ansN;。逻辑看似和正确解法一样但2*N/3存在溢出风险。当N非常大时如表格最后一行2*N会超出long long的表示范围发生溢出导致结果错误。而正确解法2*(N/3)则安全。关键经验在涉及大整数的乘除运算时优先进行除法运算可以有效地降低中间结果溢出的风险。这是一个非常重要的编程习惯。5. 举一反三问题变种与思维拓展“幸运的店家”这个问题虽然代码简单但其背后的数学模型和优化思想可以延伸到许多类似场景。掌握它不仅仅是解决一道题更是锻炼了一种将生活问题抽象为数学模并寻找最优解的能力。5.1 变种一折扣比例变化如果优惠规则不是“总价是3的倍数则打2/3折”而是“总价是M的倍数则打D折”D1该如何求解设折扣为D例如8折则D0.8模数为M。问题变为求最小正整数k使得(k * N) % M 0然后计算P D * k * N最终答案ans min(P, N)。解法我们需要解k * N ≡ 0 (mod M)。设g gcd(N, M)最大公约数。那么条件等价于k * (N/g) ≡ 0 (mod M/g)。由于N/g与M/g互质所以k必须是M/g的倍数。因此最小的k M / g。例如N4 M6 D0.55折。g gcd(4,6)2M/g 3。所以最小k3。总价12是6的倍数打5折后付6元。而原价是4元64所以顾客不会凑单最终付4元。这个推广公式涵盖了原题M3 D2/3 ggcd(N,3)。当N是3的倍数时g3 k1当N不是3的倍数时g1 k3。5.2 变种二多种商品与背包问题如果店里有多种不同价格的商品顾客可以任意组合购买仍然享受“总价是M的倍数则打折”的优惠求顾客买到所有商品至少一件所需的最低支付。这就从一个简单的数学问题变成了一个复杂的组合优化问题类似于背包问题。我们可以定义状态dp[i][j]表示考虑前i种商品总价模M余j时的最小花费。这是一个典型的带模数的完全背包/多重背包问题难度大大增加。这提醒我们许多复杂问题都是简单问题的叠加和组合。5.3 在竞赛中的定位与策略ALGO-985在蓝桥杯算法训练中属于比较基础的数学题和贪心思维题。它考察的重点不是复杂的算法和数据结构而是问题抽象能力能否从“凑单打折”的生活场景中提炼出“寻找最小k使k*N被3整除”的数学模型。数学推导与简化能力能否通过模运算分析将枚举k的问题转化为对N模3的分类讨论从而得到O(1)的解法。编程严谨性能否注意到整数溢出、运算顺序等细节写出健壮的代码。在竞赛中遇到此类题目正确的打开方式是先暴力枚举小数据找规律写一个简单的循环程序打印出N从1到20的结果观察规律。你很快会发现答案序列是1, 2, 3, 4, 5, 4, 7, 8, 6, 10, 11, 8, ... 当N是3的倍数时答案明显变小。大胆猜想小心验证根据规律猜想“如果N是3的倍数答案是2N/3否则是N”。数学证明尝试像我们前面那样用模运算证明猜想的正确性。考虑边界与溢出用大数值如1e18测试你的公式确保计算过程不会溢出。最终编码用最简洁、安全的代码实现。这道题的价值在于它像一把钥匙打开了“用模运算和数论简化问题”的大门。以后遇到“周期性”、“循环节”、“整除性”相关的问题你都会下意识地想到“能不能像‘幸运的店家’那样对模数进行分类讨论”
返回列表