1. 从2017年百度的这套题说起
每年三四月份,各大厂的春招笔试就集中爆发,对计算机相关专业的同学来说,笔试成绩直接决定了你能否拿到面试门票。百度2017春招这套编程题在当年算是比较典型的代表,难度适中、区分度好,覆盖了排序、贪心、动态规划、枚举、字符串处理这些笔试最高频的考点。即便放到今天回头看,这套题依然很有刷的价值——考点不过时,题型风格也一直延续到现在。
这套题一共6道,做起来差不多两个小时,题量不算大,但每道题都有值得琢磨的细节。我自己带过不少同学刷题,2017年这套题几乎人手一份,因为它特别适合用来检验基础:贪心什么时候该排序、动态规划怎么定义状态、枚举怎么控制边界,每个点都戳在面试笔试最容易出问题的地方。
这篇文章会把6道题逐一拆开,每道题都会讲清楚三个层次:题目在考什么、最优解怎么想到的、代码怎么写不会踩坑。还会额外补充一些我在真实笔试中的经验——这些才是平时刷题平台上看不到的。
适合这样几类人阅读:正在备战春招秋招的应届生、想系统巩固算法基础的在职开发者、以及需要一套高质量练习题用来带新人的团队leader。无论你现在处于哪个阶段,这套题都能帮你找出自己知识体系里的薄弱环节。
2. 整体风格与考点分布
2.1 2017年百度笔试的出题风格
先说说这套题的整体风格。6道题里,模拟和思维题占了大头,真正的难题后面也就一两道。百度跟其他大厂不太一样的地方在于,它特别爱出“代码不复杂,但思路要绕一下”的题——看着像能暴力解,但一算复杂度就发现必须优化,这种风格在这套题里体现得淋漓尽致。
比如第2题“度度熊回家”,题目给你一个整数序列表示每次移动的距离和方向,问删掉其中一个数后从起点到终点走过的总路程最少是多少。这题第一反应可能是枚举删哪个数,然后模拟整个移动过程,复杂度O(n²),当时数据量小能过,但百度的评测机向来卡得不留情面,动态规划才是正解。
这套题同时还考察了一个很重要的能力:读题。好几道题的表述都有一定迷惑性,比如“不等式数列”那题,题目给了一个看起来很复杂的递推关系,实际上一层循环就能解。笔试跟平时刷题最大的区别就在这里——没人给你点明考点,你需要自己识别出这题在考什么、用哪套模板。
2.2 六道题的考点分布与难度评估
| 题目 | 核心考点 | 难度 | 推荐做题时间 |
|---|---|---|---|
| 买帽子 | 排序 / 去重 / 思维 | ★☆☆☆☆ | 10分钟 |
| 度度熊回家 | 枚举 / 模拟 / 动态规划 | ★★☆☆☆ | 15分钟 |
| 寻找三角形 | 枚举 / 几何公式 / 浮点数处理 | ★★★☆☆ | 20分钟 |
| 有趣的排序 | 排序 / 最长连续序列 / 思维 | ★★★☆☆ | 20分钟 |
| 不等式数列 | 动态规划 / 组合数学 | ★★★★☆ | 25分钟 |
| 进制转换 | 进制运算 / 位操作 / 大数处理 | ★★★☆☆ | 20分钟 |
从表格里能看出来,这套题没有纯粹的模板题,每道题都需要你在基础算法的基础上做一点变通。这也是我特别推荐这套题的原因:如果你能在两个小时内独立做出5道以上,你的基础应对绝大多数一二线厂的笔试题没什么问题了。
难度控制上,百度当年的策略是“第一题送分,最后一题压轴”,中间几道题拉开差距。也就是说,你至少要快速拿下第1题建立心态,才能有充足的时间啃后面的硬骨头。我见过太多同学在前两题上磨蹭太久,导致后面会做的题都没时间写。
2.3 这套题对当前备考的参考价值
可能有人会想,2017年的题,现在都这么多年了,还有参考价值吗?我的答案是:有,而且很大。笔试考点不像技术栈那样迭代那么快,排序、贪心、DP、枚举这些基础题型,到什么时候都是笔试主力。你去看最近两年各大厂的笔试真题,很多题的核心思路还能追溯到2017年这套题上。
另外一个重要原因是,这套题的数据范围和限制条件设置得非常典型,足够让你练习“根据数据范围猜测算法类型”这项关键能力。数据范围是10³还是10⁵,直接决定了你需要用O(n²)还是O(n log n)的算法,这是实打实的笔试实战技巧,平时刷题不刻意训练的话很容易忽视。
3. 六道真题逐一拆解
3.1 买帽子——最短的题往往暗藏陷阱
题目原意是:度度熊想去商场买一顶帽子,商场里有N顶帽子,有些帽子的价格可能相同。度度熊想买一顶价格第三便宜的帽子,问第三便宜的价格是多少,不存在则输出-1。
这道题看起来简单到不能再简单了,但它的陷阱恰恰藏在“有些帽子的价格可能相同”这句话里。如果直接排序后取第三个元素,遇到重复价格就会出错。
n = int(input()) prices = list(map(int, input().split())) # 方法一:排序后去重 prices.sort() unique_prices = [] for p in prices: if not unique_prices or unique_prices[-1] != p: unique_prices.append(p) if len(unique_prices) < 3: print(-1) else: print(unique_prices[2])这个方法的时间复杂度是O(n log n),主要开销在排序上。还有一种写法是用集合去重,然后转成列表排序,代码更简洁,但本质思路一样。
为什么这道题值得单独拿出来说?因为在真实笔试中,这种“送分题”恰恰是翻车重灾区。我见过不少同学,思路完全正确,但没注意去重,或者没处理“不存在”的情况,白白丢分。笔试的判题是数据驱动的,边界情况一个不处理好就可能只过部分测试用例。
我的建议是,做这类简单题也要走完整流程:先把样例跑通、再想边界情况(空数据、全部重复、只有两个不同价格等)、最后提交。简单题拼的不是智商,是细心程度。这道题你省下的时间,就是后面难题的思考时间。
3.2 度度熊回家——枚举与动态规划的路线之争
这道题的原题表述是:一个数轴上共有N个点,第一个点的坐标是度度熊现在位置,第N-1个点是度度熊家的位置。现在度度熊需要依次从第1个点走到第N个点,但是他可以选择跳过其中某一个点,问跳过哪个点可以使得度度熊走路的总距离最短,输出最短距离。
首先明确一点:题目说的是“依次从第1个点走到第N个点”,也就是说正常路径是从点1走到点2、点2走到点3……一直走到点N,总距离是相邻点距离的累加。允许跳过其中某一个点,问最短总距离。
解法一:暴力枚举
最直观的思路就是枚举删除哪个点。删除第i个点后,原来从i-1到i的路径和从i到i+1的路径被替换成从i-1直接到i+1的路径,所以总距离的变化量是:
|pos[i+1] - pos[i-1]| - (|pos[i] - pos[i-1]| + |pos[i+1] - pos[i]|)对每个i计算总距离,取最小值即可。复杂度O(n)。如果你用更朴素的“删除后重新模拟走路”的方法,每次删点后重新累加距离,那就是O(n²),在n比较大的时候有超时风险。我当时在做这道题的时候,第一次就是写的O(n²)版本,虽然当时能过,但这个习惯不好——在笔试里,一个能优化的地方都要优化,因为评测机不会每次都那么宽容。
#include <bits/stdc++.h> using namespace std; int main() { int n; cin >> n; vector<int> pos(n); for (int i = 0; i < n; i++) cin >> pos[i]; int total = 0; for (int i = 1; i < n; i++) { total += abs(pos[i] - pos[i-1]); } int ans = total; for (int i = 1; i < n - 1; i++) { int cur = total - abs(pos[i] - pos[i-1]) - abs(pos[i+1] - pos[i]) + abs(pos[i+1] - pos[i-1]); ans = min(ans, cur); } cout << ans << endl; return 0; }解法二:动态规划
这道题如果拓展一下,问“允许跳过K个点,求最短距离”,那枚举就失效了,必须要用DP。定义dp[i][j]为走到第i个点、已经跳过了j个点时走过的最短距离,状态转移为:
dp[i][j] = min(dp[i-1][j] + abs(pos[i] - pos[i-1]), dp[i-2][j-1] + abs(pos[i] - pos[i-2]))前者表示从i-1正常走到i,后者表示从i-2跳过i-1走到i。这道题只要求跳过1个点,所以用枚举就够了。但如果你在笔试里看到这个题目,可以多想一步:出题人会不会把题目改个条件让你用更高级的算法?这种提前思考的习惯,能帮你在真正的难题面前快速找到方向。
这道题我踩过的坑:忘了考虑点可以重合的情况。题目并没有说坐标互不相同,如果存在重合的点,跳过某个点前后距离可能不变。虽然不影响最终答案的正确性(取最小),但会让我在心里嘀咕是不是算错了。做笔试的时候,心态稳定很重要。
3.3 寻找三角形——浮点数精度与几何公式
第三题是这样的:二维平面内有N个点,每个点有一个颜色(红色/绿色/蓝色),颜色用字符'R'、'G'、'B'表示。现在要从中选出3个点组成一个三角形,要求这3个点的颜色两两不同或者完全相同,求满足条件的三角形最大面积,输出面积值(保留5位小数)。如果没有满足条件的三角形,输出0。
这道题有两个关键点:一是枚举所有三点组合(N通常不大,N≤50就能过O(N³)),二是面积计算用海伦公式还是向量叉积。
为什么力推向量叉积而不是海伦公式?
海伦公式需要先算三条边的长度,然后用sqrt(p * (p - a) * (p - b) * (p - c))计算面积。公式本身是对的,但存在两个隐患:第一,浮点运算次数多,误差累积更明显;第二,如果三点接近共线但并不是完全共线,海伦公式里的p - a、p - b、p - c可能因为浮点误差变成极小的负数,开根号直接报错。我在实际运行中真的遇到过这种情况,调试了很久才发现是浮点精度的问题。
向量叉积的方法则优雅得多。对于三个点(x1, y1)、(x2, y2)、(x3, y3),面积等于:
area = 0.5 * abs((x2 - x1) * (y3 - y1) - (x3 - x1) * (y2 - y1))这个公式的几何意义是:以第一个点为基准,向量AB和向量AC构成的平行四边形的面积的一半。计算过程中只有乘法和减法,浮点误差小得多,而且三点共线时叉积恰好等于0,天然能处理共线情况。
def area(p1, p2, p3): x1, y1 = p1[0], p1[1] x2, y2 = p2[0], p2[1] x3, y3 = p3[0], p3[1] return 0.5 * abs((x2 - x1) * (y3 - y1) - (x3 - x1) * (y2 - y1)) n = int(input()) points = [] colors = [] for _ in range(n): parts = input().split() colors.append(parts[0]) points.append((int(parts[1]), int(parts[2]))) max_area = 0.0 for i in range(n): for j in range(i + 1, n): for k in range(j + 1, n): if colors[i] == colors[j] == colors[k] or (colors[i] != colors[j] and colors[i] != colors[k] and colors[j] != colors[k]): max_area = max(max_area, area(points[i], points[j], points[k])) print(f"{max_area:.5f}")常见问题一:三点共线能不能算三角形?
题目说选出3个点组成三角形,从几何定义出发,共线三点不能构成三角形。用叉积算出来的面积为0,不会影响最大值。
常见问题二:如何判断“颜色两两不同”?
三个字符两两不同其实就是三者的集合大小为3,写成c1 != c2 and c2 != c3 and c1 != c3最直观。注意千万不要写成c1 != c2 != c3,这种链式比较在Python里虽然语法合法,但语义是(c1 != c2) and (c2 != c3),漏掉了c1 != c3的判断,极容易出bug。
浮点输出注意事项:题目要求保留5位小数,Python里直接用f"{max_area:.5f}",C++里用printf("%.5f\n", max_area)。如果答案是整数(比如面积为25),输出也会自动补成25.00000,这点不用担心,格式化函数会处理。
3.4 有趣的排序——思维题的精髓在“反向思考”
这道题很有百度的风格,题目是:度度熊有一个长度为N的数组,他想将该数组从小到大排序,但是度度熊只会以下操作:任取数组中的一个数,然后将它放置在数组的最后一个位置。问最少进行多少次操作能将数组变为有序。
这题看起来像排序题,实际上是一道思维题。直接模拟每次取出一个数放到末尾,复杂度太高且难以确定最优策略。正确的打开方式是反向思考:找出数组中最长的连续上升子序列的长度,答案就是n减去这个长度。
为什么?
我们换个角度想:既然我们只能把元素移到末尾,那么保持不动的元素一定是在最终排序完成后相对位置不变的那些。在一个已经有序的数组中,保持相对顺序不变的元素,在原数组中一定构成一个连续上升子序列(这里的连续指的是在原数组中顺序递增,但值不必相邻)。我们想让移动次数最少,就要让保持不动的元素尽可能多。而这些不动的元素在原数组中的顺序,必须和它们排序之后的顺序一致。换句话说,我们需要在原数组中找到一个最长的子序列,它本身已经是递增的,而且这些元素在排序后的数组中的相对位置和原数组中的相对位置完全一致。
这里有一个重要的细节:这个“最长递增子序列”必须是连续取值的。什么意思呢?比如原数组是[3, 1, 2],最长的连续上升子序列是[1, 2],长度为2,答案是1。把3移到末尾就得到了[1, 2, 3]。但如果我们找的是最长递增子序列(LIS),[3]或[1, 2]都是,但[1, 3]在原数组中的顺序是1在3后面,不行。所以这道题要找的,实际上是在原数组中位置和值都递增的最长子序列,且这个子序列中的数排序后也紧挨着——这等价于“数值上连续递增的最长子序列”。
举个例子来说清楚:
原数组:[2, 1, 4, 3, 5] 排序后:[1, 2, 3, 4, 5]在原数组中,[1, 3, 5]是递增的,但它们排序后不相邻(1和3之间隔着2),所以不能保持不动。真正能保持不动的,是排序后在目标数组中也相邻的那一串数。在[2, 1, 4, 3, 5]中,[1, 3, 5]不满足,[2, 4]也不满足(排序后2和4中间隔着3),但是[1, 3]在原数组中位置依次递增、数值也递增且相邻?也不对,排序后1和3中间隔着2。
所以正确做法是:排序后,原数组中的每个元素都有一个“排序后的位置”。我们要找的是那些在原数组中位置递增、且排序后位置也递增且连续的一段。这个问题的标准解法是把原数组排序,记录每个值排序后应该在的位置,然后遍历原数组,找出在原数组顺序中“排序位置连续递增”的最长段。
更直观的做法是:因为是1到N的排列(或者可以去重后的数组),我们可以用一个map记录每个数在原数组中的下标,然后从1开始逐个往后找,看map[i+1]是否大于map[i]。如果大于,说明这两个数在原数组中的相对顺序是1在2前面,排序后也能保持这个顺序,可以都不动。如果小于,说明i+1在i前面,必须动其中一个。这样遍历一遍就能找到最长连续递增子序列的长度。
#include <bits/stdc++.h> using namespace std; int main() { int n; cin >> n; vector<int> a(n); unordered_map<int, int> pos; for (int i = 0; i < n; i++) { cin >> a[i]; pos[a[i]] = i; } int max_len = 1, cur_len = 1; for (int i = 1; i <= n - 1; i++) { if (pos[i + 1] > pos[i]) { cur_len++; max_len = max(max_len, cur_len); } else { cur_len = 1; } } cout << n - max_len << endl; return 0; }为什么是n - max_len?因为找到能保持不动的连续上升段之后,剩下的所有数都至少需要移动一次(把它们依次放到末尾,最终就能排成有序)。这里的最少操作次数恰好等于需要移动的元素个数。你可能想问:会不会存在一种更优的方案,移动操作能“顺带”把多个元素送到正确位置?不会,因为每次只能把一个数放到末尾,而且放到末尾后它可能又被其他操作覆盖,必须单独处理。放在末尾的元素会一个接一个排好,但不能减少需要移动的数的总数。
这道题给我最大的启发是:面对“操作次数最少”的问题,不要只想着模拟操作过程,先想一想哪些元素可以不动。一旦确定了不动的元素,答案往往就呼之欲出了。这种反向思考的能力,是编程思维中特别重要的一环。
3.5 不等式数列——动态规划的经典状态设计
这道题相对偏难一些。题目意思是:度度熊最近对全排列特别感兴趣,对于1到n的一个排列,度度熊发现可以在中间根据相邻两个数的大小关系插入适当的大于号或小于号(例如一个排列1, 3, 2,因为1<3,3>2,所以可以插入为 1<3>2),使得不等式成立。现在度度熊想知道,对于1到n的排列,有多少个排列使得这些不等式中有k个小于号,答案对2017取模。
这道题的关键在于状态的定义:用dp[i][j]表示由1到i这i个数组成的排列中,恰有j个小于号的排列个数。
核心转移思路:当我们从1到i-1的排列扩展到1到i的排列时,把数字i插入到已有的排列中。数字i是当前最大的数,因此当它插入到某个位置时,会产生一些有趣的变化:如果它插入到排列的最开头,那么它和原来的第一个数之间会产生一个大于号(因为i最大),所以小于号的数量不变。如果它插入到最末尾,那么它和原来的最后一个数之间会产生一个小于号,小于号数量加1。如果它插入到排列中间的两个数字之间,会有两种可能:原来那个位置如果是一个小于号,插入i后会变成大于号(因为i最大),小于号数量减1;原来那个位置如果是一个大于号,插入i后还是会变成大于号,小于号数量不变。但要注意,插入i还会在它左右两侧各产生一个新的关系(左侧与左邻居、右侧与右邻居),这个过程需要仔细推导。
更简单直观的推导方式是:**把数字i插入到一个由1到i-1组成的排列中,一共有i个空位可以插。**设原排列中有j个小于号,那么有(j+1)个位置插入i后,小于号数量不变(最开头的位置,以及每个小于号的位置插入后代替原来的小于号),有(i-j-1)个位置插入i后,小于号数量加1(最末尾的位置,以及每个大于号的位置插入后代替原来的大于号)。这个推导过程是经典的“插空法”,也是这类DP的核心套路。
状态转移方程就出来了:
dp[i][j] = dp[i-1][j] * (j + 1) + dp[i-1][j-1] * (i - j)解释一下:dp[i-1][j]表示在i-1个数中已经有j个小于号,这时有(j+1)个位置可以插入i且不增加小于号数量;dp[i-1][j-1]表示在i-1个数中只有j-1个小于号,这时有(i - j)个位置可以插入i并恰好增加一个小于号(因为原来有(i-2)-(j-1)个大于号,加上末尾的位置,一共是i-j个位置)。
边界条件:dp[1][0] = 1,因为只有一个数时没有小于号。所有dp[i][j]在j<0或j>i-1时都是0。
#include <bits/stdc++.h> using namespace std; const int MOD = 2017; int dp[1005][1005]; int main() { int n, k; cin >> n >> k; dp[1][0] = 1; for (int i = 2; i <= n; i++) { for (int j = 0; j < i; j++) { dp[i][j] = (dp[i-1][j] * (j + 1)) % MOD; if (j > 0) { dp[i][j] = (dp[i][j] + dp[i-1][j-1] * (i - j)) % MOD; } } } cout << dp[n][k] << endl; return 0; }我在这里踩过的坑:忘记对结果取模。题目明确说了答案对2017取模,但我第一次写的时候只在最后输出时取模,中间过程没有取模,导致大数溢出。实际笔试中,所有中间运算都要取模,尤其是涉及乘法的地方。另外要注意,dp数组的维度要开到n+1,j的循环范围是0到i-1,不能超过i-1,否则会访问到未定义的状态。
这道题是整套真题中最有“含金量”的一道,它考察的是对动态规划状态转移的深入理解。很多同学背模板会做“背包”、“LIS”,但一旦换一个场景就不知道怎么定义状态了。插空法是一种非常通用的DP状态设计思路,值得多找几道类似的题练手(比如“排列的逆序对数量”)。
3.6 进制转换——细节决定成败的经典题
最后一题是一个数字进制转换的问题:给定一个十进制数M,以及需要转换的进制数N(2≤N≤16),将十进制数M转换成N进制数。如果M是负数,负号要保留。当N大于10时,应该使用大写字母A-F来表示10-15。
听起来非常简单,就是一个“除N取余法”,但它在真实笔试中通过率却不高。原因有两个:一是很多人没处理负数的情况,二是很多人没处理M等于0的情况,还有一个容易踩坑的点是转换结果要逆序输出——你算出来的第一个余数其实是转换结果的最低位。
#include <bits/stdc++.h> using namespace std; int main() { int m, n; cin >> m >> n; if (m == 0) { cout << 0 << endl; return 0; } bool negative = false; if (m < 0) { negative = true; m = -m; } string chars = "0123456789ABCDEF"; string ans; while (m > 0) { ans += chars[m % n]; m /= n; } if (negative) ans += '-'; reverse(ans.begin(), ans.end()); cout << ans << endl; return 0; }为什么处理0的情况如此重要?很多程序在m=0的时候直接进入while循环,循环一次都不执行,最终输出的ans是空字符串,判题直接判错。这种边界情况在笔试里特别常见,出题人就是故意留这种坑来拉开区分度。
还有一个细节:C++里对负数取余,结果的正负号跟随被除数,比如-7 % 2等于-1而不是1。所以一定要先把负数转成正数再处理,最后再把负号加回去。这是很多初学者容易忽视的点。
这道题看上去太简单了,甚至算不上“编程题”,但它恰恰是笔试中“基石型”题目——用来检验你的基本功扎不扎实。很多时候,大厂笔试题的压轴难题都是从这些基础题演变过来的:改条件、加限制、结合其他数据结构。能把这些基础题做到满分解,笔试就已经成功了一大半。
4. 实战经验:笔试过程中的策略与技巧
4.1 时间分配:45-60-15法则
百度的这套题,我建议的时间分配策略是“45-60-15”。前45分钟集中做前3道相对简单的题(买帽子、度度熊回家、寻找三角形),每道题控制在15分钟以内;中间60分钟攻坚后两道核心题(有趣的排序、不等式数列);最后15分钟用来做进制转换题和全面检查。
这套节奏的依据是:简单题先做能快速建立信心,同时保证基础分到手。有趣的排序和不]等式数列虽然难度高,但只要思路对了,代码量其实很少——它们真正花时间的是思考,而不是写码。进制转换放到最后是因为它实现简单但边界条件多,在心态紧张的时候最容易犯低级错误,留到最后专门处理反而能提高准确率。
实战中我的经验是:如果一道题想了10分钟还完全没有头绪,先标记好跳过,继续做后面的题。笔试时间宝贵,不要在一道题上死磕。等你把会做的题都做完了,再回头来冷静思考跳过的题目,往往会有新的思路。
4.2 输入输出处理:笔试翻车的第一大原因
很多人刷题的时候习惯用牛客网、LeetCode这种平台,函数接口已经帮你处理好了输入输出。但百度的笔试用的是自己的OJ系统,要求你写出完整的程序——包括读入、处理和输出。每年都有大量同学在这个环节失分,特别可惜。
我见过的最惨痛的教训:题目的输入是“第一行一个整数N,第二行N个整数”,但有人写的读入代码是读一行之后用空格分割。如果测试数据里有多余的换行或者末尾有多余的空格,就会读入错误。虽然题目数据通常不会那么刁钻,但养成逐行读取、按需处理的习惯总是不错的:
读取完所有输入后,检查一下有没有漏读 输入数据量不确定时,用 while(cin >> x) 循环读取 输出结果时注意换行符——很多OJ要求每行输出末尾带换行4.3 模运算与极端数据:笔试中隐藏的分数杀手
回看这套题,不等式数列要求对2017取模,进制转换要求处理负数,买帽子要求处理重复元素,这些其实都是在考察你对“边界情况”的敏感度。从我的经验来看,最终笔试成绩拉开差距的往往就是这些边界情况——思路正确但边界没处理好的代码,跟思路正确且边界完善的代码,在分数上可能是天壤之别。
有几个固定的检查清单,每次提交前都要过一遍:
- 数据范围:N取最大值时,int够不够?要不要用long long?
- 特殊输入:0、负数、空数组、全相同元素、最大/最小值
- 输出格式:小数位数、换行、保留负号
- 数组越界:循环边界是
< n还是<= n,n-1和n+1会不会越界
4.4 从这套题里能带走的东西
这套题的考点其实有很强的代表性,我梳理了一张“考点能力映射表”,方便你对照查漏补缺:
| 题目 | 底层能力 | 刷题建议 |
|---|---|---|
| 买帽子 | 边界意识、去重思维 | 把简单题做对,是为了给难题留时间 |
| 度度熊回家 | 枚举优化、暴力转高效 | 能用公式推导的变化量就不要真的去模拟 |
| 寻找三角形 | 浮点数精度、公式选型 | 遇到几何题优先用叉积 |
| 有趣的排序 | 反向思维、从“不动”入手 | 遇到最少操作,先想哪些可以不动 |
| 不等式数列 | DP状态设计、插空法 | 排列计数类DP是常考内容 |
| 进制转换 | 边界处理、基础功 | 把0、负数、进制转换每个细节都处理好 |
如果你做完这套真题,发现自己有五道以上能独立做出来,说明你的基础功底已经很扎实了。如果只能做出来两三道,也不必气馁——笔试本来就是查漏补缺的过程,把不会的题弄懂并总结成自己的解题模板,就是最大的收获。
5. 高频踩坑与提升建议
5.1 这套题里最容易踩的坑
整理一下我在批改别人代码和自己在实际做题中遇到的典型问题:
坑点一:简单题用了复杂写法反而容易出错
买帽子那题,有人非要用堆、用平衡树来维护前三小的不同值,逻辑绕了好几层,最后反而在边界条件上翻车。实际上一个排序加去重就能解决。在笔试场景下,可读性和正确性优先于炫技——只要复杂度达标,最朴素的写法永远是最不容易出错的。
坑点二:浮点数比较踩坑
寻找三角形这题,有人判断三点是否共线时,直接用面积是否等于0来判断。但因为浮点数误差,计算结果可能是0.0000000001而不是0。更稳妥的做法是设定一个极小值epsilon,比如1e-9,面积小于epsilon才认为共线。不过这道题因为取的是最大面积,所以即使共线导致个别面积算成极小值,也不会影响最大值,但养成用epsilon的习惯总没错。
坑点三:状态初始化遗漏
不等式数列的DP,初始化只设了dp[1][0] = 1。如果你把dp[i][0]的递推也写出来会发现,它等于dp[i-1][0] * 1,一直等于1——这是对的,因为1到i的排列中,完全升序的情况只有一种,小于号数量为0。所有的DP题,第一步先把边界条件和初始化状态想清楚,否则后面全乱套。
5.2 刷这套题的正确姿势
如果你准备用这套题来模拟笔试,我建议按照“全真模拟”的节奏来:设置一个倒计时,两个小时内不中断地完成全部6道题,最好在一个安静的环境里,模拟真实笔试的紧张感。做完之后,无论成绩如何,拿出一整天时间来复盘——不是看看答案就完事,而是把每一道题从头到尾重新推导一遍,总结出“我为什么没想到这个解法”“这个解法为什么是对的”。
针对薄弱环节再做专项训练:如果不等式数列没做出来,就去刷一刷排列计数类的DP;如果有趣的排序没思路,就多练习需要反向思维的题目。这套题只是一个起点,用它找出自己的问题,然后对症下药,才是它的最大价值。
5.3 笔试前一周的备战清单
根据我多年的经验,笔试前一周最有效的冲刺方式是:
- 前三天:每天做一套真题(不限公司),只看思路不看答案,模拟真实笔试
- 第四天:重点复习DP和贪心的常见模型(背包、LIS、LCS、区间DP、状态机DP)
- 第五天:复习字符串处理、进制转换、大数问题这些“小知识点”
- 最后两天:调整作息,不再做新题,只看错题和笔记,保持手感和心态
这里特别想强调的一点是:笔试前的晚上一定要早睡。我知道很多同学喜欢在考前突击到凌晨,但笔试考的更多是思维敏捷度和稳定性,休息不足的大脑很难发挥出真实水平。这套题里有不少需要细心处理的边界条件,精神不好就特别容易漏。
5.4 这套题对“面经”的启示
这套真题做下来,你能明显感觉到百度的工程师文化——题目不追求偏难怪,但特别看重思维的灵活性和基础功的扎实程度。这也和百度的面试风格一脉相承:面试官喜欢在你做完题目之后追问“为什么这么做”“还有没有更好的方案”,其实就是看你是否真正理解了思路,而不是背下了代码。
所以备考的时候,不要只刷题不思考。每一道题做完之后,多问自己几个“为什么”:为什么这个状态转移是对的?有没有其他解法?两个解法的复杂度差在哪里?这样做一轮下来,你的收获会远远超过单纯刷题三倍的量。
6. 写在最后
回头看百度2017春招这套题,它就像一套精心设计的能力检验题:从简单的排序去重到需要反复推敲的DP状态设计,6道题难度梯度合理,每道题都对基本功提出了明确要求。我见过不少基础不错的同学在这套题上栽跟头,也见过基础中等的同学利用它高效复盘,最终在正式笔试中超常发挥。差别不在于天赋,而在于是否认真对待每一道题背后的思维训练。
如果你正在准备大厂笔试,不妨在刷了大量LeetCode之后,试着用这套真题做一次“摸底考试”。把时间卡到两个小时,独立完成所有题目,然后逐题复盘,找出自己的薄弱点,再针对性地强化训练。这个过程比盲目刷新题有意义得多。
我个人在这套题上最大的收获,并不是学会了某一道题的解法,而是体会到了一种备考节奏:先刷真题摸底,再针对弱点补强,最后带着清晰的认知上考场。这套方法帮助我带过的很多同学拿到了心仪的大厂Offer。希望这篇拆解对你同样有帮助,也希望你今年春招顺利上岸。最后分享一个小技巧:每次笔试前,把你这套题中做错的题的代码重新手写一遍,不要看任何参考资料——手写代码这件事,能帮你发现很多你以为会了但其实不会的细节。