去年我带几个学弟准备蓝桥杯Java组省赛,发现一个特别有意思的现象:不少人一上来就啃动态规划、图论、数论这些“硬骨头”,结果到了考场上,连一道纯暴力枚举的填空题都因为边界条件写错丢了分。省赛获奖的差距,往往不是谁懂得多,而是谁基础扎实、谁能在有限时间内把该拿的分稳稳拿到。这也是我把“基础算法”放在整个蓝桥杯学习路线第二章的原因——它是一切的底盘。
这一章我打算围绕蓝桥杯Java组里最高频、最基础的核心算法展开,包括排序、枚举与模拟、递归与DFS、二分答案、贪心和简单动态规划。我会结合真题场景、常见误区、可复制的代码模板来写,尽量让你看完就能直接上题库里练。
如果你正处于“Java语法已经看完、但刷题不知从何下手”的阶段,那这篇文章就是为你准备的。
1. 先想清楚:蓝桥杯Java组的“基础算法”到底考什么
1.1 省赛题目的难度分布规律
蓝桥杯省赛的题型结构这几年相对稳定:填空和编程大题结合,前几题基本是送分题,中段题目考基本功,最后一两题才是真正拉开差距的压轴题。以Java组为例,省一分数线通常在50到70分之间浮动,这意味着你不需要解出最后的难题,只要把前面的题目稳定拿下,就已经超过大半选手了。
很多人一开始就把精力押在难题上,恰恰是最不划算的。Java组的考试时间和C/C++组一致,四小时,十道题左右,时间看着充裕,但如果你对基础算法不熟,光是调试一个二分边界就可能耗掉一个多小时。我在带训练的时候一直强调一句话:省赛比的是“不丢分”的能力,而不是“超神”的能力。
这里有一个历年真题的明显规律:填空第二三题、编程第一二题,大概率落在枚举、日期处理、模拟、简单排序这些范畴内。也就是说,基础算法直接覆盖了省赛30%以上分数的考查区间。
1.2 基础算法在竞赛里的真实定位
很多初学者把“基础算法”理解为一堆孤立的代码模板,比如背个冒泡排序、背个递归求阶乘,就觉得自己会了。实际上竞赛里的基础算法更像“组合拳”:排序会配合二分查找使用,二分会配合贪心判定使用,DFS会配合状态标记和回溯使用,枚举则几乎无处不在地作为题解的基础版本出现。
举一个蓝桥杯真题的典型例子——某年省赛一道关于“搬货箱”的题,最直接的解法是全排列枚举所有顺序,然后检查是否满足约束。这题数据范围很小,枚举全排列完全能过,根本不用想贪心或DP。但如果你连递归生成全排列都写不利索,这个分就丢了。这就是基础算法的真实定位:在正确的时间用正确的基础工具,快速、准确地拿分。
1.3 一个反直觉的事实:高级算法题解的核心也还是基础算法
我在整理近几年省赛题解时发现一个有趣的现象:很多标答写着“动态规划”,但你细看状态转移方程,本质上就是对枚举过程的优化;写着“深度优先搜索”,其实就是递归的思想加上一个visited数组;写着“二分答案”,那就是在一个有序的“解空间”里做枚举筛选。
所以这一章并不只是给新手准备的,对那些已经开始刷难题、但经常卡壳的人,回头补一遍基础算法,往往能让题解的思路“豁然开朗”。这也是为什么每年学完Java语法的人那么多,真正能稳定拿省一的却那么少——底盘不稳,再好的招式也使不出来。
2. 排序算法:蓝桥杯里最寻常也最见功底的考点
2.1 冒泡排序:理解原理很重要,但别拿它硬刚大数据
冒泡排序是入门第一课,它的原理很简单:每一轮从头到尾相邻比较,把大的元素一路“冒”到末尾。代码不难写,但我想说的是:在蓝桥杯赛场上,冒泡排序几乎只用来处理极小规模数据或填空压轴题,指望拿它过10^5级别的数据,基本属于自杀。
看看典型的冒泡实现:
public static void bubbleSort(int[] arr) { int n = arr.length; for (int i = 0; i < n - 1; i++) { boolean swapped = false; for (int j = 0; j < n - 1 - i; j++) { if (arr[j] > arr[j + 1]) { int tmp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = tmp; swapped = true; } } if (!swapped) break; // 优化:若本轮无交换则提前结束 } }注意那个swapped标志位,这是我在实际调试中经常提醒学弟们加上的优化。不加它,当数组本身有序时你仍然要跑满n-1轮;加了它,最好情况能降到O(n)。虽然量级不变,但在填空代码分析题里,判断“最坏情况比较了几次”“是否稳定”这类问题,这个细节就是分数差距。
2.2 Arrays.sort的本质:Java组选手最该掌握的排序姿势
蓝桥杯Java组和C/C++组最大的不同在于:Java提供了现成的排序API,不需要像C++选手那样手写一堆sort。Arrays.sort()在竞赛里是绝对的主力。
但这里有个经典大坑:Arrays.sort()对基本类型的数组用的是快速排序(Dual-Pivot Quicksort),对对象数组用的是归并排序(TimSort)。这意味着如果你需要对一个Integer[]数组排序,稳定性是没问题的;但如果用int[],排序是不稳定的。虽然蓝桥杯多数题目不要求你说明稳定性,但当你自定义对象排序、并把排序结果用于后续依赖相对顺序的算法时(比如求逆序对数量),这个差异就直接影响答案正确性。
我在搜“java排序”相关热词时,发现很多人问sort函数用法java,这里统一列一下常见场景:
| 需求 | 写法 | 备注 |
|---|---|---|
| int数组升序 | Arrays.sort(arr) | 默认升序 |
| int数组指定范围 | Arrays.sort(arr, from, to) | 左闭右开,to不包含 |
| Integer数组降序 | Arrays.sort(arr, Collections.reverseOrder()) | 注意不能用于int[] |
| List升序 | Collections.sort(list) | 也可以直接用list.sort(null) |
| List指定规则 | list.sort((a, b) -> a - b) | Lambda表达式 |
还有一个小技巧:int数组想降序,可以先升序再倒序,或者用Java 8之后的Arrays.stream(arr).boxed().sorted(Comparator.reverseOrder())转成流操作。但流操作拆箱装箱有开销,数据量大的时候不如自己写个反向循环。
2.3 手写快排和归并,什么时候才真有必要
有同学问:既然Arrays.sort()这么好用,蓝桥杯为什么还要学手写快排和归并?
我的回答是:平时比赛能用API绝不自找麻烦,但有两种情况你必须会手写。第一,Arrays.sort()在极端数据下可能退化为O(n^2)(虽然Java的快排做了随机化处理,概率极低,但不能完全不防),当你明确知道数据里可能存在大量重复元素时,手写三路快排反而更稳。第二,归并排序的“副产品”——求逆序对数量——是省赛里反复出现的一个考点,让我认真看一下这个题目。
求逆序对的经典代码框架:
static long mergeSort(int[] arr, int l, int r) { if (l >= r) return 0; int mid = l + (r - l) / 2; long cnt = mergeSort(arr, l, mid) + mergeSort(arr, mid + 1, r); int[] tmp = new int[r - l + 1]; int i = l, j = mid + 1, k = 0; while (i <= mid && j <= r) { if (arr[i] <= arr[j]) { tmp[k++] = arr[i++]; } else { cnt += mid - i + 1; // 左半部分剩余元素全部构成逆序 tmp[k++] = arr[j++]; } } while (i <= mid) tmp[k++] = arr[i++]; while (j <= r) tmp[k++] = arr[j++]; System.arraycopy(tmp, 0, arr, l, tmp.length); return cnt; }核心理解点:当arr[i] > arr[j]时,从i到mid的所有元素都和arr[j]构成逆序对,因此一次性加上mid - i + 1。这个一次性统计就是归并排序能高效求逆序对的思路。很多学弟第一次写这个题,卡在“忘了加剩余元素”,结果答案偏小,这就是归并排序框架本身不够熟练的表现。
2.4 Comparable和Comparator:自定义排序的细节课
蓝桥杯经常出现“按照某种规则排序”的题,比如学生的成绩单、物品的性价比。这时候就需要自定义排序规则。在Java里,推荐实现Comparable或在调用时传入Comparator,两者各有适用场景。
class Student implements Comparable<Student> { int score; String name; public int compareTo(Student o) { // 按分数降序,分数相同按姓名升序 if (this.score != o.score) return o.score - this.score; return this.name.compareTo(o.name); } } // 使用:Arrays.sort(students)一个我在实际刷题中踩过的坑:Comparator的比较逻辑必须满足传递性,否则Java在排序时会抛出IllegalArgumentException: Comparison method violates its general contract。常见触发场景是使用return a - b表示升序时,如果数值差超过int范围(比如两个大数相减溢出),就会违反约定。稳妥写法永远是Integer.compare(a, b),或者直接a > b ? 1 : (a < b ? -1 : 0)。
3. 枚举与模拟:先用暴力拿到稳定分数,再谈优化
3.1 “暴力不丢人”——枚举思想的竞赛地位
先亮一个观点:蓝桥杯很多题目,你哪怕一点算法都不会,只要会枚举,就能拿不少分。枚举(暴力)是竞赛里“保底策略”的核心工具。
有一年省赛填空题要求“找出1到2020中有多少个数的某个性质”,很多人的第一反应是套公式,但那道题只要用计数循环枚举每个数就行了,甚至不需要任何数学推导。这类题每年都有,比的就是你愿不愿意老老实实写循环。
枚举的核心思路只有一条:在数据范围内穷举所有可能的状态,逐一验证是否满足条件。说白了就是双层循环、三层循环、或者DFS全排列。很多基础算法其实都是对“盲目枚举”的优化——排序让二分查找成为可能,优化很少见的分母。
3.2 经典真题:日期处理与数字枚举
日期处理是蓝桥杯的高频基础题型。比如“给定两个日期,求相隔天数”“判断某年某月某日是星期几”,解法通常依赖一个isLeapYear()函数和月份天数数组。
static boolean isLeap(int year) { return year % 400 == 0 || (year % 4 == 0 && year % 100 != 0); } static int[] days = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; // 判断第year年month月的天数 static int daysOfMonth(int year, int month) { if (month == 2 && isLeap(year)) return 29; return days[month]; }这个代码看着基础,但每年都有大批人在闰年判断上栽跟头。还记得“世纪闰年”吗?能被400整除的才是闰年,1900年不是闰年,2000年是闰年。如果你用year % 4 == 0来判断,遇到年份带“00”就会出错。这类细节就是基础算法的“陷阱题”考察点。
3.3 用位运算枚举子集:1<<n 的边界细节
有些题需要枚举一个集合的所有子集,比如选择题里“有几块积木可选,有多少种选法”。最常用的技巧是二进制枚举。
int n = 5; for (int mask = 0; mask < (1 << n); mask++) { for (int i = 0; i < n; i++) { if ((mask & (1 << i)) != 0) { // 选中第i个元素 } } }这里有个大坑:1 << n当n为31时是合法的,但当n为32时,结果变成负数(因为int只有32位,1左移32位回到1,然后又循环),这时候你会陷入死循环或漏掉状态。稳妥做法是1L << n配合long类型,或者直接限制n不超过20。省赛里出现n=20以上的子集枚举概率很低,但如果遇到可以适当考虑状态压缩DP的另一条路线。
我在给学弟讲这块时经常说:位运算枚举看起来高级,但本质上是最朴素的枚举——只是用二进制帮你在0和1之间做选择。先把这种“朴素”吃透,再学压缩技巧就不难了。
3.4 模拟题的经验:方向数组、边界条件、时间格式
模拟这类题特别吃“细心”二字,它没有算法难度,但绝对有代码实现难度。考场上一道模拟题调不出来,往往不是因为你不会,而是因为边界条件忘记了。
方向数组是模拟题里最常遇到的问题:
static int[] dx = {-1, 1, 0, 0}; // 上下左右 static int[] dy = {0, 0, -1, 1};每次移动新坐标时,都得判断是否越界:
int nx = x + dx[dir]; int ny = y + dy[dir]; if (nx < 0 || nx >= n || ny < 0 || ny >= m) continue; // 越界则跳过这个判断我自己写错过无数次:数组下标是0到n-1,但坐标习惯从1开始,一不留神就>= n写成了> n。我平时总结的经验是:写模拟题时,先把问题里的坐标范围明确标注在草稿纸上,再动笔写代码。每写一个数组访问,先问自己“这里会越界吗”。
时间格式也是模拟题的重灾区:秒变时分秒、分钟进位、小时进位、日期进位。有个小技巧是把所有单位统一到秒或分钟,运算完再拆分格式化,能避免大量进位错误。
4. 递归与DFS:把问题分解成更小的自己
4.1 递归三要素:终止条件、递归关系、返回值
递归是很多Java初学者的“心理阴影”,但其实只要掌握三个要素,大部分递归题都能拆解清楚:
- 终止条件:递归到什么时候不再调用自己,直接返回结果。
- 递归关系:把当前问题拆成一个更小规模的同类问题。
- 返回值:当前这一层要把什么结果返回给上一层。
以经典的斐波那契数列为例,终止条件是n == 0返回0、n == 1返回1;递归关系是f(n) = f(n-1) + f(n-2);返回值是第n项的数列值。就这么简单。
但直接递归有个致命问题——重复计算。画个展开图就能发现,计算f(6)时f(2)被算了无数次。所以竞赛里写递归,一定要想到记忆化搜索:
static long[] memo; static long fib(int n) { if (n <= 1) return n; if (memo[n] != 0) return memo[n]; // 已经算过就直接返回 return memo[n] = fib(n - 1) + fib(n - 2); }这里把“递归”瞬间升级成了“动态规划”的雏形。很多DP入门题,本质上就是“递归 + 备忘录”。理解了这层关系,后面学动态规划会顺畅很多。
4.2 典型递归案例:全排列、组合、爬楼梯
蓝桥杯填空题经常考“有多少种排列方式/走法”。比如经典爬楼梯问题:一次可以上1级或2级台阶,n级台阶有多少种走法?这其实就是斐波那契的变体,f(n) = f(n-1) + f(n-2),初始值f(0)=1, f(1)=1。
全排列则是递归有趣很多:
static void permute(int[] nums, int idx) { if (idx == nums.length) { // 得到一个排列,处理结果 return; } for (int i = idx; i < nums.length; i++) { swap(nums, i, idx); permute(nums, idx + 1); swap(nums, i, idx); // 回溯,恢复现场 } }注意那个回溯过程——swap回去。忘记回溯是DFS初学者最常见的错误之一。你交换了两两个位置,如果不交换回去,下一次循环看到的就是错乱的数组,生成的排列会有大量重复。
4.3 DFS的通用模板:visited数组、回溯和剪枝
蓝桥杯有大量迷宫搜索题,核心就是深度优先搜索。我总结了DFS的通用模板,大家可以直接套用:
static int n, m; static char[][] grid; static boolean[][] visited; static int[] dx = {-1, 1, 0, 0}; static int[] dy = {0, 0, -1, 1}; static void dfs(int x, int y) { if (越界|| visited[x][y] || 不是合法通行点) return; visited[x][y] = true; // 处理当前点 for (int dir = 0; dir < 4; dir++) { dfs(x + dx[dir], y + dy[dir]); } // 如果只是连通性检查,到这里就够了,不需要撤销visited }有一个关键点要说明:什么时候需要回溯(撤销visited)?什么时候不需要?
- 让你判断“能否到达终点”,只要是连通性检查,就不需要撤销,因为到过的地方不需要再去。
- 让你统计“所有从起点到终点的路径条数”,就必须撤销,因为不同路径可以经过不同点,同一个点在一条路径中只能走一次,但在另一条路径中需要被重新访问。
这俩场景我见过无数人混淆,所以单独拎出来强调。
4.4 蓝桥杯里的DFS风格:迷宫与八皇后
迷宫的DFS上面已经覆盖了,我再补一个重要的经典题:N皇后(八皇后)。这个题很多学校教材都讲,蓝桥杯偶尔会变体出现,但核心是用一维数组记录每一行皇后放的列号,然后用递归逐行尝试。
static int[] cols = new int[10]; static int count = 0; static void queen(int row, int n) { if (row == n) { count++; return; } for (int col = 0; col < n; col++) { boolean ok = true; for (int pre = 0; pre < row; pre++) { if (cols[pre] == col || Math.abs(col - cols[pre]) == Math.abs(row - pre)) { ok = false; break; } } if (ok) { cols[row] = col; queen(row + 1, n); } } }这里的判冲突条件是:同列冲突cols[pre] == col,对角线冲突Math.abs(col - cols[pre]) == Math.abs(row - pre)。每次只需要和之前的行比较,不需要二维数组,这是N皇后最经典的写法。我建议大家把这个模板背到形成肌肉记忆,省赛如果出了基本是送分题。
递归和DFS这块,我个人的体会是:别怕“慢”,先保证逻辑对,再考虑剪枝优化。很多时候DFS跑不完不是因为它不行,而是因为你在第一次实现时就把剪枝条件写错了,导致它在一个死胡同里打转。
5. 二分答案:把“求最值”变成“判断可行”
5.1 什么时候应该想到二分
二分查找大家都会,但要意识到“二分答案”这个更高级的用法:
- 题目标志:“求最大值最小(小化最大)”“最少需要多少”“最长/最短路径长度”
- 特征是答案在一个连续区间内,并且可以写出
check(mid)这个判定函数
一句话总结:如果你能回答“mid这个值是否可行”,就能用二分答案找到最优值。这是蓝桥杯基础算法章节里性价比最高的技巧,因为它能在你不会推导最优化公式的情况下,硬生生把你的暴力解法升级成O(nlogn)。
5.2 整数二分的边界问题:为什么你的代码会死循环
这是所有二分相关文章里的重灾区。整数二分最令人头痛的坑就是mid的取法:
// 第一种:找左边界(在满足条件的一侧) int l = 0, r = n - 1; while (l < r) { int mid = (l + r) >> 1; // 下取整 if (check(mid)) r = mid; else l = mid + 1; } // 终止时 l == r,即答案 // 第二种:找右边界(不满足条件的一侧) int l = 0, r = n - 1; while (l < r) { int mid = (l + r + 1) >> 1; // 上取整 if (check(mid)) l = mid; else r = mid - 1; } // 终止时 l == r,即答案注意上面两种写法中加粗的区别:一个用的是(l+r)>>1,另一个用的是(l+r+1)>>1。如果你在第二种场景下用了下取整,当 l = 2, r = 3 时,mid = 2,check(2) 为 true,l 保持 2,r 保持 3,循环就死锁了。这是初学者最容易踩的坑。
我在刷题时习惯用一套固定的方式来避免这个坑:先想清楚“可行域”在哪一侧,再决定mid是否要加1。如果不确定,直接手写几个例子跑一遍边界值,比死记结论靠谱。
5.3 浮点二分的精度控制
浮点二分相对简单,但同样有细节。我常用的模板:
double l = 0, r = 1e9; while (r - l > 1e-7) { double mid = (l + r) / 2; if (check(mid)) r = mid; else l = mid; } // 输出 l 或 r,误差在1e-7内浮点二分的终止条件不是l < r,而是判断区间长度是否足够小。一般题目会要求“结果保留小数点后几位”,你只要让精度比题目要求高两个数量级就行。注意浮点运算有误差,所以check函数内部如果涉及除法、开方,尽量用double运算,避免精度损失。
5.4 二分答案的经典例题模式:跳石头、切木块、分蛋糕
这里我拿几个典型模型来说明:
- 跳石头(最大值最小/最小值最大):在一条线上移除若干石头,要求每跳的最小距离尽量大。直接枚举距离并检查能否用不超过k次移除满足所有间距不小于mid。这个模型的check函数是“统计需要移除的石头数是否不超过上限”。
- 切木块:把若干长木棍切成k段相同长度的小段,求最大允许切出的长度。check函数是“按mid长度能切出多少段”,统计段数是否不少于k。
- 分蛋糕:把一块蛋糕分成若干块,要求每一块的最小面积/重量,求能分给人数最多的方案,check函数是“按mid大小能分几块”。
这些题本质都是“给定一个mid,从头到尾扫一遍统计”,只有二分骨架是共同的。把二分模板练熟后,每道题的差别只在check函数里。
我教学生时反复强调:二分这东西,模板定了就不要随便改。每次根据题目调整check逻辑就好,边界写法保持一致,能省大量调试时间。
6. 贪心与简单动态规划:省赛的起跑线就在这
6.1 贪心的思考模式:局部最优能否推出全局最优
贪心算法看起来简单,但很多人都被它坑过——因为“局部最优推出全局最优”这个条件通常不成立,你需要先证明或至少有直觉把握,才能用贪心。
省赛最常考的贪心模型是区间调度类:有若干活动,每个活动有开始和结束时间,问最多能参加几个不重叠的活动。标准解法是:按结束时间升序排序,依次选择最早结束且与已选活动不冲突的活动。
这个问题的贪心证明是经典中的经典:最早结束的活动一定在某个最优解中。因为如果最优解的第一个活动不是最早结束的那个,用最早结束的替换它,不会让整体冲突更严重,答案不会变差。这种“替换论证”是贪心题的灵魂。
6.2 经典贪心场景:排队打水、背包变种、区间选点
我再列几个蓝桥杯容易考到的贪心模型:
- 排队打水:多人排队接水,每人有接水时间,求最优排队顺序使总等待时间最短。答案是按时间从短到长排,这个模型背后是“短作业优先”思想,也可以用交换论证证明。
- 部分背包问题:物品可以拆分,每单位重量价值不同,问装最大价值——按单位价值从高到低装就行,这是最简单的贪心。
- 区间选点:每个区间至少需要放一个点,求最少放几个点覆盖所有区间。按区间右端点排序,然后贪心地放在当前区间右端点,能尽量多地覆盖后续区间。
这些题的共同点是都会有一个“排序 + 扫描”的套路。贪心难在识别模型,一旦识别出来,代码通常很短。所以在比赛里我建议对贪心题保持高度敏感,如果数据范围大到枚举吃不下,优先想想贪心是否成立。
6.3 动态规划入门:从斐波那契到一维状态转移方程
动态规划是基础算法章节里上限最高的部分,也是很多人的难点。但蓝桥杯省赛考到的DP大多是很基础的线性DP,核心是状态定义 + 转移方程 + 初始化。
拿最长上升子序列(LIS)举例:
// dp[i] 表示以第i个数字结尾的最长上升子序列长度 int[] dp = new int[n]; Arrays.fill(dp, 1); int ans = 1; for (int i = 1; i < n; i++) { for (int j = 0; j < i; j++) { if (arr[j] < arr[i]) { dp[i] = Math.max(dp[i], dp[j] + 1); } } ans = Math.max(ans, dp[i]); }我讲这块时经常把动态规划和“填表格”类比:你不需要关心整张表格怎么填,只需要知道当前格子由哪些之前格子决定。这个“依赖关系”就是状态转移方程。学DP最有效的路径是先学会“打表找递推关系”,靠肉眼观察几组数据,再抽象成方程。
6.4 常见的DP细节:初始化、循环顺序、滚动数组
DP写起来一波三折,但容易出问题的往往是细节:
- 初始化值:比如LIS中每个位置至少要包含自己,所以
dp[i]初始为1。 - 循环顺序:背包问题里,0-1背包要从后往前遍历容量(保证每个物品只选一次),完全背包则从前往后遍历。这个顺序问题每年都有人栽。
- 滚动数组优化:当转移方程只依赖前一行或前两行时,可以用一维数组滚动更新,把空间从O(n^2)降到O(n)。省赛压轴题里偶尔用到。
我可以给个0-1背包的核心循环对比:
// 二维版:dp[i][j] 表示前i个物品、容量j下的最大价值 for (int i = 1; i <= n; i++) { for (int j = 0; j <= W; j++) { dp[i][j] = dp[i - 1][j]; // 不选第i个 if (j >= w[i]) { dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - w[i]] + v[i]); // 选第i个 } } } // 滚动数组版:一维 for (int i = 1; i <= n; i++) { for (int j = W; j >= w[i]; j--) { // 注意从后往前 dp[j] = Math.max(dp[j], dp[j - w[i]] + v[i]); } }为什么从后往前是关键,你必须自己想明白。如果从前到后,dp[j - w[i]]可能是这一轮已经更新过的值,相当于第i个物品被选了多次,那就从0-1背包变成了完全背包。省赛里很多同学代码看着能跑,结果答案偏大,就是这个原因。
贪心和DP这部分,我给所有准备蓝桥杯的人的建议是:先从贪心入手,因为贪心题代码短、识别难度低,能快速建立信心。DP则从斐波那契、爬楼梯、LIS这类最简单的模型开始,一题一题积累,等到能熟练写出背包模板,省赛的DP基础分也就拿稳了。
7. 整套基础算法的训练路线与复盘方法
7.1 按难度分层,建立自己的“基础算法题库”
基础算法说来说去就这几大类,但要真正内化成赛场上的本能反应,必须靠刷题来巩固。我建议按下面的顺序推进:
| 阶段 | 题目范围 | 目标 |
|---|---|---|
| 入门期 | 蓝桥杯省赛前5题、力扣简单题 | 熟悉枚举、模拟、排序、二分模板 |
| 成长期 | 蓝桥杯省赛中段题、力扣中档题 | 掌握DFS、贪心、线性DP |
| 冲刺期 | 蓝桥杯历年省赛第6-10题、经典真题 | 综合运用,练比赛节奏和查错能力 |
力扣(LeetCode)有个“必刷基础算法题”题库,蓝桥杯官网也提供历年真题,这两块结合着刷效率最高。每天保持2-3道题的节奏,持续一个月,基础算法层面基本就够打省赛了。
7.2 刷题的正确姿势:先想思路,再对答案,最后复写
很多人的刷题方式就是“看题-想不出来-看题解-哦原来如此-下一题”,这样刷一个月也等于白刷。我更推荐三步法:
- 独立思考10分钟:先自己在纸上写伪代码、暴力思路也行,不追求最优,只追求“我能想到一个解法”。
- 对答案看差异:看官方题解或优质题解,重点看“它为什么想到用这个算法”,而不是“它代码怎么写的”。
- 不看答案复写一遍:第二天在没有参考的情况下重新实现这道题,确保真的理解了。
这个流程能把“我懂了”变成“我会了”。我带学弟练习时发现,凡是坚持复写的,省赛成绩普遍比那些只看不写的同学高一个档次。
7.3 比赛前的查漏补缺:模板要不要背、API记不住怎么办
临近比赛前一周,我建议做两件事:第一,把本章提到的基础模板(排序、二分、DFS、背包、LIS)自己默写一遍,确保不依赖IDE自动补全也能写出无语法错误的代码。第二,把Java常用API过一遍,重点包括StringBuilder、Arrays、Collections、HashMap、HashSet、PriorityQueue这些竞赛高频工具,它们的常用方法必须闭眼能写。
有学弟问:模板要不要背?我的答案是:理解的前提上背,不理解硬背是灾难。你至少要能在纸上画出递归过程、画出二分边界,才能保证考场上模板变形时你还会改。
7.4 一个赛季下来我最深刻的体会
陪一届又一届学生准备蓝桥杯,我最大的感受是:这个比赛真的没有“运气分”,每分都是平时练出来的。基础算法学得扎实的人,考场上遇到任何题都有保底解法;基础不牢的人,就算听说过十几个高级算法名词,真到写代码时候依然无从下手。
我自己每次准备一个赛季前,都会重新把基础算法的代码过一遍——不是为了学到新东西,而是为了把那些“肌肉记忆”重新唤醒。拿到一道题,先判断是不是枚举/排序/二分能解决的,这种条件反射比任何奇技淫巧都管用。希望这一章的内容,也能帮你把底层能力打扎实。省赛的每一分,都会在你花掉的每一个基础算法练习的夜晚里慢慢浮现。