1. 赛题整体回顾题型分布与备考定位蓝桥杯JavaB组省赛的题目风格从我接触的这些年来看其实一直很稳定前几题考基本功后几题考算法模型最难的那道往往不是算法本身难而是你压根想不到该往哪个方向套。2017年第八届这套题到现在我还时不时拿出来当训练材料尤其是“包子凑数”和“k倍区间”这两道题放在很多公司的笔试里一点不过时。整套题一共10道前5道是填空题后面是编程大题。填空题对Java选手来说通常就是数学推导加一点暴力枚举很多时候根本不写代码口算或者Excel都能算出来。但这里有一个很现实的问题省赛是按奖线卡人的填空题的正确率直接决定你是省一还是省二。很多人觉得编程大题才重要其实前5道题你哪怕慢一点也要确保拿满因为大题的测试点很多你未必能全过。这套题的核心难点集中在后5道日期处理题考的是标准库的使用边界递推递归题考的是对问题结构的提取能力二分查找、动态规划、前缀和这类常见优化模型考的是你能不能把题目翻译成模型。顺着这个思路我把当年最有代表性的几道题拿出来逐题拆解包括题面大意、分析过程、解法代码、我踩过的坑以及考场上应该怎么控制时间。2. 逐题拆解题面、思路与完整代码2.1 纸牌三角形暴力枚举的经典练手题先讲一道我印象很深的填空题——纸牌三角形。题面大意A、1、2……9一共9张牌摆成一个正三角形每条边4张牌边与边共用顶点的牌。要求三条边上的数字之和相等问一共有多少种不同的摆法。旋转和镜像后相同的摆法算同一种。我第一次做这道题的时候傻乎乎地手写排列写了一整页纸还数错了。后来才反应过来这种数据量小的题目唯一靠谱的办法就是程序暴力枚举。思路拆解9个位置放9个数字全排列是9!种也就是362880种对计算机来说就是一瞬间的事。关键是怎么设计“校验一条边”的逻辑。我的做法是把三角形的9个位置标上号三条边分开看边1是位置[0,1,2,3]边2是位置[3,4,5,6]边3是位置[6,7,8,0]这样三个顶点的共用关系就体现在边首尾位置共享上。核心代码逻辑如下int[] arr {1, 2, 3, 4, 5, 6, 7, 8, 9}; // 对arr做全排列 int count 0; for (int[] p : permutations) { int sum1 p[0] p[1] p[2] p[3]; int sum2 p[3] p[4] p[5] p[6]; int sum3 p[6] p[7] p[8] p[0]; if (sum1 sum2 sum2 sum3) { count; } } // 每条边有3个方向三角形有3个旋转位置所以除以6 System.out.println(count / 6);这里最关键也最容易被忽略的一步就是最后要除以6。为什么因为同一个三角形你把纸片转三个角度每条边还能从两端各读一次一共对应6种排列结果但它们在题目里是同一种摆法。省赛考这个点就是看你会不会处理“去重”。我后来给学生讲这道题时经常说暴力枚举不是笨办法而是你用有限的大脑处理海量可能性时的最可靠手段。但暴力的前提是你要把状态空间定义清楚不然枚举半天全是重复答案。2.2 日期问题标准库用得熟不熟这一题就暴露了日期问题几乎每年蓝桥杯都会出现但每年都有一大批人因为格式、解析、闰年这些细节翻车。当年这道题说的是输入一个字符串格式可能是年/月/日、月/日/年、日/月/年三种中的任意一种都要尝试解析输出所有合法日期按从早到晚排序。这道题的核心考点有两个一个是日期合法性另一个是去重和排序。很多人第一反应是自己写判断闰年的逻辑然后手写比较日期大小。其实Java标准库里的LocalDate早就把这些都封装好了你只需要正确解析和捕捉异常。我当时用的方案是String dateStr 02/03/04; // 示例输入 ListLocalDate results new ArrayList(); String[] parts dateStr.split(/); int a Integer.parseInt(parts[0]); int b Integer.parseInt(parts[1]); int c Integer.parseInt(parts[2]); // 三种排列方式 try { results.add(LocalDate.of(2000 a, b, c)); } catch (Exception e) { /* 不合法就跳过 */ } try { results.add(LocalDate.of(2000 c, a, b)); } catch (Exception e) { } try { results.add(LocalDate.of(2000 c, b, a)); } catch (Exception e) { }注意两点一是“00”这种年份要映射到2000而不是1900二是去重因为三种排列可能解析出同一天排序前要先去重。这道题考场上最常见的错误是年、月、日哪个对应哪个搞反日期不合法不检查比如2月30日直接被LocalDate抛弃但你得捕获异常输出格式没按题目要求的“yyyy-MM-dd”来。我后来复盘的时候总结了一句能不用自己实现的就不要自己实现。日期时间这种基础能力标准库比你稳得多把精力留给真正的算法题。2.3 取数位递归终止条件一错整题白写这道题是填空题给了一段不完整的递归代码让你补全。函数的作用是返回某十进制整数从高位数的第k位数字。我当时看到这段代码的第一反应是这是考递归的终止条件和递推方向。一个典型的实现是不断把数字除以10缩小范围直到只剩一位然后回溯取值。补全的部分通常是这么一行static int f(int x, int k) { if (k 1) return x % 10; return f(x / 10, k - 1); }很多人会写成x % 10然后直接返回这样只处理了最低位。正确的理解是你要取的是从高位往低数第k位所以每次递归先把最右边一位削掉直到k变成1此时x的个位就是你要找的那一位。这里我要专门提醒一个点递归函数的返回值并不一定是要一路传回来的。如果递归过程中需要“回溯”或者“累计”要特别小心每个栈帧返回什么、外层怎么接收。这种填空题在考场上看不出来问题但如果你自己实现一遍就会发现递归边界错一位结果全错。2.4 最大公共子串动态规划入门必刷题最大公共子串这道题可以说是动态规划里最温柔的一道。题面很简单给定两个字符串求它们的最长公共子串的长度。注意是“子串”要求连续不是“子序列”。用动态规划的思路非常直白定义dp[i][j]表示字符串A的前i个字符和字符串B的前j个字符中以A[i-1]和B[j-1]结尾的最长公共子串长度。如果两个字符相等dp[i][j] dp[i-1][j-1] 1否则就是0。过程中用最大值记录下来。public static int longestCommonSubstring(String s1, String s2) { int n s1.length(), m s2.length(); int[][] dp new int[n 1][m 1]; int max 0; for (int i 1; i n; i) { for (int j 1; j m; j) { if (s1.charAt(i - 1) s2.charAt(j - 1)) { dp[i][j] dp[i - 1][j - 1] 1; max Math.max(max, dp[i][j]); } } } return max; }这里我踩过的一个坑是把数组下标和字符串下标对应错了。dp[i][j]对应的字符是s1.charAt(i-1)不是s1.charAt(i)一上来写成charAt(i)会直接数组越界或者结果偏一位。你要是觉得绕干脆让字符串前面加个占位符从下标1开始遍历这样逻辑更顺。这道题虽然简单但它的价值很大。因为蓝桥杯里很多字符串题都是在这个模型上叠条件比如要求输出子串本身、要求不区分大小写、要求用滚动数组优化空间。一次把这个弄透后面再见到就不慌了。2.5 分巧克力二分答案的标准模板分巧克力这道题题面说有N块长方形巧克力尺寸分别是Hi×Wi要切成K块一模一样大的正方形巧克力问正方形边长最大能是多少。巧克力可以切但不能拼接。这题的常规思路是二分边长。为什么能二分因为“边长x是否满足能切成至少K块”这个函数是单调的边长越小能切的块数越多。所以我们可以从1到最大值之间二分查找找出满足条件的最大的x。判定函数怎么写呢对于一块Hi×Wi的巧克力能切成(Hi / x) * (Wi / x)块边长为x的正方形。把所有的块数加起来如果大于等于K说明x可行。static boolean check(int x, int n, int k, int[][] chocos) { int cnt 0; for (int[] c : chocos) { cnt (c[0] / x) * (c[1] / x); if (cnt k) return true; } return false; } public static void main(String[] args) { // 输入省略 int l 1, r 100000; // 根据题目数据范围定 int ans 1; while (l r) { int mid (l r) / 2; if (check(mid, n, k, chocos)) { ans mid; l mid 1; } else { r mid - 1; } } System.out.println(ans); }这里要注意的是右边界不要习惯性写成100000最好根据所有巧克力的最小边来确定上限不然二分次数多几次虽然没关系但显得你不严谨。另外check函数里可以提前退出一旦块数超过K就返回true能省不少时间。这类“最大值最小化”或者“最小值最大化”的问题在省赛里几乎每年都有。判断你能不能反应过来的关键就是你有没有形成条件反射看到“最大可能值”“最少需要多少”这类描述第一反应就是二分。2.6 包子凑数数论与背包的结合题包子凑数算得上整张卷子的难点分水岭。题面是这样的有N种蒸笼每种蒸笼能装固定数量的包子比如第一种能装4个第二种能装7个。问有多少种正整数包子数量是凑不出来的。如果凑不出来的数量有无穷多个就输出INF。这里涉及一个数论常识如果所有蒸笼容量都满足某种“不互质”的条件那么凑不出来的数就是无穷多个。更严格地说如果这些容量的最大公约数不是1那么大于某个界限的数里只有gcd的倍数能被凑出来其他数全都凑不出来所以数量无限。一句话版本把所有容量的gcd求出来等于1就用动态规划算有限个凑不出的数不等于1直接输出INF。动态规划部分就是完全背包的变种开一个布尔数组dp[i]表示i个包子是否能凑出来。初始dp[0]true然后每个容量都可以无限使用for (int cap : capacities) { for (int i 1; i MAX; i) { if (i cap dp[i - cap]) { dp[i] true; } } }这里MAX要取多少是个门道。一般取一个足够大的上界比如10000。因为只要gcd为1凑不出的数量只会出现在有限的范围内超过某个值之后全都能凑出来。10000对于省赛的数据量足够了但如果你不放心可以直接开20000。这道题我第一遍做的时候犯了个低级错误没求gcd直接跑DP跑出来一个“看起来合理”的答案但完全判断不了INF的情况。后来明白数论结论是这种“无限凑不出”题目的咽喉先证明再动代码顺序不能反。2.7 k倍区间前缀和与同余思想最后重点讲一下k倍区间。题意是给一个长度为N的数组和整数K求有多少个连续子区间其元素和能被K整除。暴力枚举所有区间的话复杂度是O(n²)数据量一大直接超时。正确做法是前缀和加同余。设前缀和数组pre[i]表示前i个元素的和。区间[l, r]的和等于pre[r] - pre[l-1]。如果这个差能被K整除那说明pre[r]和pre[l-1]对K取模的余数相同。所以问题就变成了统计前缀和里有多少对相同余数。MapInteger, Long modCount new HashMap(); modCount.put(0, 1L); // 前缀和为0代表空前缀 long ans 0; long sum 0; for (int num : arr) { sum num; int mod (int) ((sum % k k) % k); // 处理负数 if (modCount.containsKey(mod)) { ans modCount.get(mod); } modCount.put(mod, modCount.getOrDefault(mod, 0L) 1); }注意最开始为什么要往map里放一个0的计数1因为前缀和本身能被K整除时对应的区间从1开始到当前位置这个区间要算进去但它没有“左端前缀”配对所以需要预先定义pre[0]0这个虚拟元素。还有一点sum % k可能是负数当sum为负数时Java的取模符号跟被除数一致用(sum % k k) % k把它转成正数再放进map里才不会漏统计。这道题在蓝桥杯里属于“听过一遍就会没听过多死磕一小时也悬”的类型。它教会我一个很重要的思维方式遇到区间和问题先想前缀和前缀和遇到整除问题先把取模后的余数拎出来看余数相同就是配对。3. 核心算法思想总结从题目反推知识点做完这套题你会发现几个高频考点像约好了一样反复出现暴力枚举、标准库处理、递归、动态规划、二分、数论、前缀和。这些不是孤立的它们背后是一套通用的解题流程。遇到一道题先看数据范围。N小于10大概率可以暴力枚举N在10^5级别那你基本可以放弃O(n²)的念头往二分、前缀和、动态规划上靠。这套题里纸牌三角形的数据量就是冲着暴力去的k倍区间和分巧克力就是冲着优化去的。你的脑子要在看到数据范围的一瞬间完成这个判断这比会写某一种算法更重要。第二个值得总结的地方是“描述方式”。很多比赛选手卡题不是因为不会算法而是题面的场景包装太花看不出它在问什么。比如“包子凑数”本质就是个完全背包问题但如果你没从“无限用、判断可达性”这两个词上认出背包模型那你就只会跟着场景走写出个四不像的搜索。我的习惯是把题面翻译成自己不熟悉的领域类比把“蒸笼装包子”换成“硬币凑金额”把“切巧克力”换成“在若干根绳子上找公共最大段长”模型的影子马上就出来了。这个转译能力只能靠刷题量积累没有捷径。第三填空题别傻写代码。前5道题有些是纯数学题比如纸牌三角形你可以写程序枚举但如果你手边正好有Excel或者Python口算推导可能更快。这里要提醒的是Java选手不要把填空当大题写填空题没有测试点答案对了就行怎么快怎么来。4. 考场实战技巧与高频易错清单这一节写几个我亲眼见过无数人踩的坑以及我自己调整过的考场策略。4.1 输入输出别侥幸蓝桥杯的输入输出用的是标准输入输出Java选手建议全部用BufferReader或者Scanner加PrintWriter。Scanner读大输入会慢到怀疑人生PrintWriter用println自动刷新也不太建议最好是攒到最后一次flush()。另外输入里可能有空行、多余空格用split(\\s)处理比split( )稳得多。数据范围没说一定是整数有的行首尾有空格直接Integer.parseInt可能抛异常。4.2 大数组和递归栈要提前评估Java的默认栈空间在蓝桥杯的评测环境里并不富裕如果你用深搜递归处理10^5级别的数据很容易栈溢出。这时优先改成显式栈或者迭代。我当年就吃过这个亏递归写得爽但是最终有几个测试点RE白白丢分。数组方面二维数组能不建就不建能用一维滚动绝不建二维。比如最大公共子串那道题其实还能用一维数组优化到O(m)空间只要注意更新方向是倒着来的。总体思路是空间复杂度不达标等于白解。4.3 易错点速查表题目类型常见错误正确做法日期解析三种顺序搞混、闰年未处理LocalDate try-catch去重排序全排列去重忘了旋转、镜像结果除以对称次数递归填充边界条件搞错一位手写小规模用例验证二分边界mid写错用left right统一模板避免死循环前缀和负数取模出错加K再取模保证非负背包数组越界循环里判断i cap标准库手写日期比较直接用Comparable接口或LocalDate4.4 时间分配策略省赛一共四个小时我的建议是前40分钟拿下所有填空题遇到卡壳超过10分钟的先跳过。编程大题从前往后做先保证每道题都能交一版至少过样例的代码再回头优化复杂的那两道。千万别在第一题就死磕到底后面的大题分值更高而且测试数据往往比第一题好得分。5. 备考复盘与日常训练建议如果把这套真题看完你会发现自己的短板在哪个方向立刻清晰。我当时复盘的时候给自己列了三件事第一把“暴力枚举”练到形成肌肉记忆。纸牌三角形这类题一看到小数据范围就要能立刻写全排列而不是犹豫要不要用数学公式。全排列写法、去重写法、状态压缩写法这些模板要在赛前背下来。第二把“区间和”题型统一归类。k倍区间、子数组最大和、二维前缀和都归到一类里反复刷直到一看到“区间连续”“总和”这些关键词前缀和的条件反射自动触发。第三刻意练习读题转译。每做一道题先用自己的话把题面说成算法模型再动手写代码。你能把“包子凑数”说成“无限物品完全背包”这道题你就已经赢了一半。这一套题放到现在依然是很好的训练材料。当年我在某东的笔试里见过k倍区间变体在另一家做直播业务的公司笔试题里见过分巧克力的换皮版本可见这些模型的生命力有多长。把2017年的这套题吃透比盲目刷一百道新题更划算。如果还有时间多想想每一题能不能改造。把切巧克力改成折绳子把最大公共子串改成删三个字符后的最大公共子串举一反三地玩题比照着答案抄一遍学习效率高得多。算法的东西说到底就是“见过类似模型 能准确套用 在边界条件下不出错”这三步都练好省赛拿奖也就是水到渠成的事。