蓝桥杯Java省赛真题复盘:二分答案、组合计数与模拟题实战解析 第13届蓝桥杯省赛Java B组的Q5~Q7我复盘了很多遍越看越觉得这三题是整张省赛试卷里最值得细嚼的部分。Q5求阶乘考二分答案Q6小蓝与钥匙考组合计数Q7内存空间考模拟解析三个题没有一个是让你背模板就能过的但每一个都能靠扎实的基本功稳稳拿分。这篇文章我会把这3道题从题意到数学模型、从代码实现到赛场坑点完整拆一遍提供可直接对照的Java解法同时把背后可迁移的思考方式讲清楚。无论你现在正在备赛蓝桥杯还是想通过真题练一练Java算法和代码能力都可以参考这份复盘。1. 先看整体Q5~Q7在省赛里是什么定位1.1 位置、分值和难度分布蓝桥杯省赛Java B组的编程大题题号越靠前通常越简单Q5到Q7正好处在“能拿分”和“拉开差距”的过渡区。从实际体验来看这三题的分值占比不低而且呈现三种完全不同的风格Q5是一道偏数学的二分题只要想到单调性就能快速解决Q6是一道组合计数题难度主要在建模和排列组合细节Q7是一道典型的大模拟题不考高级算法但极其考验解析能力和代码耐心。三道题放在一起看其实比单个刷题更有价值。因为它们恰好覆盖了算法竞赛里三种最核心的能力数学转化能力、组合计数能力、复杂模拟能力。很多人在备赛时疯狂刷最难的动态规划和图论反而忽略了这类“中档题”。实际上省赛的分数线往往就卡在Q5~Q7这种题目上谁能写得更稳、更快谁就能在排名上往前挤一大截。1.2 三道题共同考察的东西表面上三个考点完全不相干但本质上都在考察同一件事把一个描述得很生活化的场景翻译成冷冰冰的数学模型再用代码精确表达出来。求阶乘要把“末尾有几个0”翻译成因子5的计数小蓝与钥匙要把故事背景抽离成排列组合问题内存空间要把一行行Java声明语句翻译成字节数累加。我见过不少同学刷题只看“这道题用什么算法”却忽略了“为什么能想到这个算法”。这三题最典型的特征就是算法本身并不深奥难度全在看穿题目的那一刻。你在赛场上有没有解题思路往往不是取决于背了多少模板而是取决于能不能快速识别出题目底层的数据结构和数学结构。下面我按题号逐一把这种“看穿过程”呈现给你。2. Q5 求阶乘一道靠二分答案就能拿下的数学题2.1 把“末尾0个数”翻译成数学式子Q5的大意是给定一个整数k求最小的正整数n使得n!的十进制表示末尾恰好有k个连续的数字0如果不存在这样的n就输出-1。先解决最核心的问题怎么快速求出n!末尾有多少个0。十进制下末尾的0是由因子10产生的而10可以拆成2和5两个质因子。在n!这个乘积里因子2的数量远多于因子5的数量比如从1乘到100能被2整除的数大概有一半而能被5整除的数只有五分之一。所以末尾0的个数本质上只取决于n!里面有多少个因子5公式是f(n) n/5 n/25 n/125 ...这个式子要理解透n/5统计的是1到n中5的倍数个数这些数至少贡献一个5n/25统计的是25的倍数个数它们额外再多贡献一个5以此类推。用循环来实现就非常直观static long countZero(long n) { long cnt 0; while (n 0) { n / 5; cnt n; } return cnt; }这里有一个新手容易犯的错千万别用循环从5开始累加直到n比如for (int i 5; i n; i 5)一旦n达到10的18次方量级循环直接超时。只要用除法逐次缩小n复杂度就是O(log n)级别。2.2 二分查找的正确打开方式有了f(n)的计算公式接下来要回答的问题是给你一个k怎么找到最小的n先观察f(n)的性质。n越大n!里面包含的因子5只会越多所以f(n)是关于n单调不减的。对于“单调函数求满足条件的值”这种组合第一反应就应该是二分。k的范围很大直接枚举n会超时到天际但用二分只需要大约60次迭代就能把范围缩到极限。二分找的是“第一个使f(n) k的n”。为什么不是直接找f(n) k因为f(n)并不是连续变化的它会在n跨越5的幂次倍数的瞬间突然跳跃这意味着某些k值可能根本不存在对应的n。比如f(24)4f(25)6中间直接跳过了5所以k5时无解。因此二分的思路是先找到下界n0使f(n0)刚刚大于等于k最后再验证f(n0)是否严格等于k。另一个关键点是二分的右边界。f(n)约等于n/4所以理论上答案不会超过5k直接把右边界设成k * 5 5就足够安全long left 1; long right k * 5 5; while (left right) { long mid left (right - left) / 2; if (countZero(mid) k) { right mid; } else { left mid 1; } }这里我习惯用left (right - left) / 2而不是(left right) / 2虽然Java的long加法溢出概率不高但这是一个好习惯特别在高强度竞赛环境中能避免很多奇怪问题。2.3 完整Java实现与边界处理把上面两部分合起来Q5的完整代码如下import java.util.Scanner; public class Main { static long countZero(long n) { long cnt 0; while (n 0) { n / 5; cnt n; } return cnt; } public static void main(String[] args) { Scanner sc new Scanner(System.in); long k sc.nextLong(); if (k 0) { System.out.println(1); return; } long left 1; long right k * 5 5; while (left right) { long mid left (right - left) / 2; if (countZero(mid) k) { right mid; } else { left mid 1; } } if (countZero(left) k) { System.out.println(left); } else { System.out.println(-1); } } }边界情况要注意两点。第一k0时答案是1因为1!1末尾有0个0这在部分题面里可能被忽略。第二当k特别大时k * 5 5要确保不超出long范围题目一般会把k限制在1e18以内所以不会溢出。稳妥起见也可以用倍增法动态扩大右边界不过比赛中直接用k * 5 5就够用了。2.4 从这道题带出的通用模板Q5虽然简单但它背后的“二分答案”套路可以迁移到一大类题目上。凡是题目里出现“求满足某条件的最小值/最大值”而且该条件关于答案是单调的都可以尝试二分答案。比如给定一个数组要求分成若干段问每段和最大值的最小值这就是经典的二分答案题。使用这个模板时最关键的是设计判断函数。判断函数写得好不好直接决定二分能否在时限内完成。Q5的判断函数是O(log n)的非常快但如果判断函数本身是O(n)或O(n log n)你就要估算整个二分过程能否通过时间限制。一个常用的经验值二分迭代次数通常不超过60判断函数的复杂度控制在O(n)以内数据范围在1e5级别时都没问题。3. Q6 小蓝与钥匙别被故事唬住背后是经典组合计数3.1 抽掉故事外壳核心模型是什么“小蓝与钥匙”这题在不同的回忆版本里细节略有差别但赛场上我们不需要纠结背景要做的是第一时间把故事抽成数学模型。这类题通常围绕一个东西打转有编号的钥匙和有编号的锁在某种随机对应或逐天取用的规则下统计满足某个条件的方案数或概率。这题最经典的底层模型有两个。第一个是错排模型n把钥匙对应n把锁如果随机一一匹配问恰好有k把钥匙能打开对应编号锁的方案数。第二个是“全覆盖”模型m种钥匙一天取一把取n天问每种钥匙至少被取到一次的方案数。两者都经常出现在竞赛题里解题套路也相对固定。先说错排模型。如果题目问的是“恰好k把能打开”那先选出这k把匹配正确的钥匙有C(n,k)种选法剩下的n-k把钥匙必须全部错开不能有任何一个匹配正确这就是经典的错排问题。答案就是C(n, k) * D(n - k)其中D(m)表示m个元素的错排数递推公式是D(0)1D(1)0D(m)(m-1)*(D(m-1)D(m-2))。3.2 错排公式与组合数的实现如果n的范围不大比如n小于等于30用long就能存下结果。但一旦n超过20C(n,k)乘上错排数很快就可能撑爆long稳妥做法是用BigInteger。Java里BigInteger虽然慢一些但对于这类小规模组合计数绰绰有余import java.math.BigInteger; public class KeyLock { static BigInteger C(int n, int k) { if (k 0 || k n) return BigInteger.ZERO; if (k n - k) k n - k; BigInteger res BigInteger.ONE; for (int i 1; i k; i) { res res.multiply(BigInteger.valueOf(n - k i)) .divide(BigInteger.valueOf(i)); } return res; } static BigInteger derangement(int n) { if (n 0) return BigInteger.ONE; if (n 1) return BigInteger.ZERO; BigInteger[] d new BigInteger[n 1]; d[0] BigInteger.ONE; d[1] BigInteger.ZERO; for (int i 2; i n; i) { d[i] BigInteger.valueOf(i - 1) .multiply(d[i - 1].add(d[i - 2])); } return d[n]; } public static void main(String[] args) { int n 10; // 示例 int k 2; BigInteger ans C(n, k).multiply(derangement(n - k)); System.out.println(ans); } }这里组合数用了一个小优化先判断k和n-k哪个小取小的那个做循环能减少乘法次数。这个习惯在写组合数时非常实用因为大多数时候我们只需要C(n,k)并不需要整个杨辉三角。3.3 如果是“至少全部出现一次”的容斥做法如果题目变成“取n天m种钥匙都至少出现一次”那就用到容斥原理了。直接算“全部出现”比较麻烦反过来算“至少有一种没出现”就简单了。设Ai表示第i种钥匙一次都没出现那么答案用容斥公式表达为ans Σ (-1)^i * C(m, i) * (m - i)^n 其中 i 从 0 到 m当i0时(m)^n是总方案数减去某一种没出现的方案数加回某两种同时没出现的方案数以此类推。实现同样可以交给BigIntegerstatic BigInteger allAppear(int n, int m) { BigInteger total BigInteger.ZERO; for (int i 0; i m; i) { BigInteger term C(m, i) .multiply(BigInteger.valueOf(m - i).pow(n)); if ((i 1) 0) { total total.add(term); } else { total total.subtract(term); } } return total; }这个式子就是容斥原理的经典形态很多“至少出现一次”“全部覆盖”“收集齐全”的题目最终都会落到这个公式上。如果你觉得容斥不好理解另一个替代方案是状态压缩DP用二进制位表示哪些钥匙已经出现过逐天转移。n和m不超过20时状压DP完全可行而且思路更机械、不容易出错。3.4 Java里算组合数要注意的事比赛时用Java写组合计数最大的坑就是精度和溢出。很多同学在C语言里用long long习惯了到Java里继续用long结果n取到25以上直接溢出白白丢分。我建议的原则是只要题目没有明确保证结果在int或long范围内就直接用BigInteger。虽然BigInteger需要多写几行代码但它能把你从“这个中间结果会不会爆掉”的焦虑里彻底解放出来。另外组合数的计算顺序也隐含一个数学细节每一步res * (n - k i) / i都能整除因为每一步的res其实就是C(n-ki, i)这个值一定是整数。所以乘完再除没问题不需要担心精度。4. Q7 内存空间一道专门考验耐心的模拟题4.1 题意与内存大小的计算规则Q7“内存空间”是一道字符串模拟题题目会给出一段类似Java代码的声明语句让你统计最终占用的内存大小。这类题在竞赛里不太常见但一旦出现往往就是区分度所在。内存计算规则本身不复杂常见的约定是int占4个字节long占8个字节String引用占4个字节注意实际JVM里引用大小可能随压缩指针配置变化但竞赛会按明确规则给以题面为准。数组的空间等于数组长度乘以元素类型大小。比如long[] nums new long[10]占80字节int[][] grid new int[3][4]占3乘4乘4等于48字节。有的题目会包含大括号初始化比如int[] arr {1, 2, 3}此时数组长度就是大括号里元素的个数占12字节还可能有一行声明多个变量的情况比如long a 1, b 2要统计两个变量共16字节。这些细节都是模拟题的出题点。4.2 解析思路一行一行抠还是直接上正则拿到这种题很多人的第一反应是用正则表达式提取数字。正则确实可以快速匹配new int[数字]这种模式但模拟题真正的复杂度不在于提取单个模式而在于组合情况很多一方面可能有多个变量并列另一方面数组初始化的大括号里也有逗号这会导致直接用split(,)切分时出错。我的做法是分步走。第一步去掉行内所有空格和分号让字符串变紧凑第二步根据开头判断类型是int、long还是String第三步把大括号初始化内容先保护起来比如把{1,2,3}整体替换成占位符{}然后再用逗号切分多个变量。这样int a 1, b 2能被正确切成a 1和b 2而大括号内部的逗号不会干扰。切分完之后对每一小段判断是不是数组。如果是数组提取所有方括号里的数字并相乘如果是普通变量直接累加类型大小。提取方括号数字这一步可以用正则\\[(\\d)\\]但要注意提取时别漏掉多维数组的每一维。4.3 完整Java实现下面是一个能处理多数常规声明的参考实现import java.io.*; import java.util.regex.*; public class Memory { public static void main(String[] args) throws IOException { BufferedReader br new BufferedReader(new InputStreamReader(System.in)); int n Integer.parseInt(br.readLine().trim()); long total 0; for (int i 0; i n; i) { total parse(br.readLine().trim()); } System.out.println(format(total)); } static long parse(String line) { String s line.replaceAll(\\s, ).replace(;, ); int typeSize; int typeLen; if (s.startsWith(long)) { typeSize 8; typeLen 4; } else if (s.startsWith(String)) { typeSize 4; typeLen 6; } else { typeSize 4; typeLen 3; } String rest s.substring(typeLen); String protectedStr rest.replaceAll(\\{[^}]*\\}, {}); String[] parts protectedStr.split(,); long ans 0; for (String part : parts) { if (part.contains([)) { int len 1; Matcher m Pattern.compile(\\[(\\d)\\]).matcher(part); boolean hasLen false; while (m.find()) { len * Integer.parseInt(m.group(1)); hasLen true; } if (hasLen) { ans (long) len * typeSize; } else { ans typeSize; } } else { ans typeSize; } } Matcher brace Pattern.compile(\\{([^}]*)\\}).matcher(rest); if (brace.find()) { String inner brace.group(1); int count inner.isEmpty() ? 0 : inner.split(,).length; ans (long) count * typeSize; } return ans; } static String format(long x) { StringBuilder sb new StringBuilder(); long[] units {1024L * 1024 * 1024, 1024L * 1024, 1024, 1}; String[] names {GB, MB, KB, B}; for (int i 0; i 4; i) { long cnt x / units[i]; if (cnt 0) { sb.append(cnt).append(names[i]); x % units[i]; } } return sb.toString(); } }这段代码的思路是先处理普通变量和new出来的数组再单独处理大括号初始化。实际比赛中如果遇到更复杂的语法你需要在这个基础上增加分支但整体框架不变。注意parse里对大括号和普通数组两段统计不能重复如果一行里既有数组初始化又有其他变量代码会把它们相加。这个实现没有考虑数组元素本身是String且包含逗号的情况不过竞赛数据一般不会走到这种极端。4.4 输出格式与易错点输出格式通常是按1024进制从GB到B逐级拆分比如1025字节输出1KB1B1073741824字节输出1GB。实现上从大到小除就好注意1024L * 1024要加L不然两个int相乘会先溢出再转型这是Java里特别经典的低级错误。这道题易错点总结如下同一行多个变量比如long a 1, b 2很容易漏数一个变量。二维数组要相乘有些人只取到第一个维度。大括号初始化没有new关键字容易在判断数组时被漏掉。总字节数可能超过int范围累加结果必须用long。输出时如果某项为0要省略不能输出0GB0MB...。模拟题的得分核心就是“细心”两个字。我建议在考场上一旦看到这种题先在草稿纸上把可能出现的所有声明形式列一遍再开始写解析代码。列清楚再动手比边写边想节省大量时间。5. 从Q5~Q7总结出的Java组参赛经验5.1 高频IO模板直接背下来这三道题对IO要求不高但蓝桥杯省赛有些题的数据量很大这时候再依赖Scanner就慢了。Java选手最好把下面这套IO模板背下来考场直接默写BufferedReader br new BufferedReader(new InputStreamReader(System.in)); PrintWriter out new PrintWriter(new OutputStreamWriter(System.out)); String line br.readLine(); out.println(ans); out.flush();BufferedReader按行读字符串PrintWriter负责输出可以避免Scanner在数据量超过10万时的性能瓶颈。一个隐藏的好处是按行读入对Q7这种模拟题特别友好因为你本来就要逐行处理。5.2 大数、溢出与类型选择Q5里面二分算countZeroQ6里面组合数相乘Q7里面统计总字节数三题全都有“用错类型就会翻车”的陷阱。我总结出一个经验竞赛中凡是涉及乘法累加、组合数、阶乘统计第一反应先问自己“这个结果可能超过21亿吗”别想当然觉得答案小。Java里long最大约9.2乘以10的18次方看起来很大但组合数增长是指数级的n到30左右就撑不住了。判断不好就用BigInteger或者尽量把计算顺序优化成先除后乘。在Q5这类二分题里二分边界和判断函数全部用long不要因为k看起来小就误用int。5.3 设计自测数据的三板斧比赛时提交前一定要自测我常用的方法是围绕三个方向构造数据边界数据、极大数据、格式变体数据。边界数据比如Q5的k0或k1Q7的空数组new int[0]。极大数据比如Q5取k为10的18次方Q7构造一个超过1GB的总内存用来验证输出格式。格式变体数据比如Q7包含int[][]、大括号初始化、同行多变量覆盖所有语法分支。很多选手平时刷题只跑样例样例过了就提交结果在线评测一跑就错。养成构造自测数据的习惯能帮你至少拿回10%的分。5.4 赛场时间分配建议Q5~Q7这三题我建议的总用时控制在60到70分钟以内。Q5是纯套路题10到15分钟应该拿下Q6需要一点建模推导20分钟左右Q7是模拟题代码量大留25到30分钟比较稳。如果哪一题卡了超过20分钟还没思路果断先跳过去做后面的题不要让一道题拖垮整张试卷。省赛的难度分布通常是由易到难但偶尔会出现某一题特别“拧巴”的情况。这时候大局观就很重要。Q5~Q7整体属于“投入产出比”很高的区域比最后一题钻研半天拿不到分的性价比高得多。6. 复盘与拓展练习建议6.1 从Q5延伸出去的二分类题目Q5做完之后可以找几道经典的二分答案变体练手。比如“给定n个数的数组分成m段求每段和最大值的最小值”这是二分答案里最常见的应用场景再比如“在有序数组中找到第一个大于等于target的位置”这是二分查找的原始模板。把这些题放在一起对比你会发现判断函数的设计才是二分题的精髓模板本身反而是最不重要的部分。另外一个容易考的变体是“统计n!在二进制下末尾0的个数”。思路完全一致只不过把因子5换成了因子2公式变成n/2 n/4 n/8 ...。这种变题在比赛中偶尔会出现理解了原理就能秒杀。6.2 从Q6延伸出去的组合计数题Q6做完之后建议把三类组合计数问题放在一起复习错排、容斥、卡特兰数。错排问题掌握递推公式容斥问题掌握“至少/恰好”的转化技巧卡特兰数则要理解括号匹配、出栈序列、二叉树计数等经典背景。Java的BigInteger在这些题里是救命稻草建议把组合数、错排数、容斥模板都封装成函数考场直接调用。如果对“全覆盖”模型感兴趣还可以了解一下“优惠券收集问题”它和容斥公式关系非常紧密很多概率题都能套这个模板。做这类题的关键是先把“方案数”和“概率”区分开题目问概率时最后不要忘了除以总方案数。6.3 从Q7延伸出去的模拟题写法模拟题虽然没有高深算法但最考验代码组织能力。我的建议是写模拟题前先花5分钟列一个“状态清单”把可能出现的每一种声明形式写下来然后设计一个解析函数逐一处理。Q7这种题的错误来源几乎全是漏分支而不是逻辑复杂所以分支覆盖越全得分越稳。平时练习的时候可以专门找字符串解析类的题目比如解析表达式、解析JSON片段、解析命令行参数这些都能提高对字符串的敏感度。Java里常用的字符串处理API也建议熟练掌握replaceAll、split、substring、StringBuilder、正则表达式的Matcher和Pattern。注意正则在某些复杂场景下性能一般比赛里数据量不大但日常工作中还是要谨慎。我个人的体会是Q5~Q7这三道题放在一起恰好给了备赛者一个清晰的信号蓝桥杯省赛Java B组的重点不是偏难怪算法而是你能不能把一个看似复杂的场景快速抽象成简单的数学或逻辑模型并且用代码精确无误地表达出来。如果你正在准备接下来的比赛与其刷一大堆超过省赛难度的题不如先把这类中档题练到“看到题就有思路、写完代码一次过样例”的程度。稳扎稳打省赛拿奖真的没那么玄乎。