ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

2022小米秋招算法笔试解析:动态规划、KMP与快速幂实战

2022小米秋招算法笔试解析:动态规划、KMP与快速幂实战 秋招季一到算法笔试就成了很多技术求职者的第一道坎。我这边收到过不少准备软件开发岗位的同学发来的求助问得最多的就是“2022年小米秋招笔试-算法-卷1”这类卷子到底难不难、考点是什么、刷题方向该怎么定。说实话这种算法卷并不追求天马行空的偏题怪题反而特别看重基础算法的熟练度、边界处理能力和把思路快速转成代码的稳定性。这篇内容我按照自己复盘秋招笔试的习惯从卷子整体结构、高频考点、一道典型题从读题到AC的完整过程再到实战中的坑和处理经验一次性讲透。1. 2022小米秋招算法卷1到底在考什么1.1 题型结构与考察范围我复盘过多家互联网公司秋招的第一轮算法笔试小米这套算法卷1并不是个例它基本沿用了“概念选择 经典算法变形 在线编程题”的组合方式。有些同学以为会出大量机器学习、深度学习相关的算法题实际上不是这样。研发岗笔试对模型算法的要求很低偶尔会在选择题里以场景形式出现例如问某类数据分布适合用什么聚类方式或提到神经网络里的常见优化手段但真正决定能不能进面试的还是数据结构与算法的基础编程能力。题目覆盖范围有一定规律。数组、链表、栈与队列、二叉树、图论基础、字符串匹配、动态规划、贪心、二分查找和排序算法基本都会涉及。编程题通常不只一题难度呈阶梯式上升。第一题往往让你热身比如简单的哈希统计或者模拟题第二题开始上强度常见的是需要状态定义的动态规划或带一定剪枝的暴力搜索最后一题大概率是综合题可能会把排序、前缀和、双指针、单调队列揉在一起不只是考单点知识更考代码组织能力。1.2 难度梯度和筛人逻辑整个卷子的难度梯度很有讲究它不是用来难倒所有人的而是在做分层筛选。前面的选择或填空考的是概念是否清楚。比如给一组数据让你判断最优排序算法或者问某个算法时间复杂度的上限和下限这种题没有套路的余地会就是会不会只能靠排除法。中间的编程题考的是能否在限定时间内把经典算法写对。这里的“写对”不只是能跑出样例还包括对空数组、单元素、大量重复元素等边缘样例的防御。最后的综合题才是真正拉开区分度的地方。笔试不追求满分但一定要保证基础题不失误。从多年观察来看常见的淘汰情况有两类一类是基本功不过关比如排序写错边界导致死循环动态规划状态转移写漏一个条件另一类是时间分配失败在中间难度的题目上耗时过长导致最后一题即使有思路也没有时间敲完。所以看这份卷子不要只盯着某一道难题要把整场笔试当成一次限时信息处理目标是让会做的题全部稳定拿分。2. 高频算法考点拆解从原理到代码2.1 动态规划与贪心状态设计是得分关键动态规划几乎是所有算法笔试的必考项小米这类大厂笔试也不例外。常见考法不是让你默写斐波那契或背包模板而是用一个看起来像模拟题的场景让你自己抽象出状态。比如“打家劫舍”类型的题会以一排房屋抢或不抢来包装实际上就是经典的线性DP。还有“最长上升子序列”会变成“最多能选择多少个任务而不冲突”。这种包装后的题最考验的是你能不能把业务描述翻译成状态方程。状态设计有一个很实用的思路先看题目要求的结果类型是最大值、最小值、方案数还是一个布尔值可不可行。通常状态数组的维度就来自决策过程中的限制条件。比如常见的二维DP一般是因为有两个东西在变化比如前i个物品放进容量为j的背包。如果只有一个维度在变化比如前i个字符的某一结果就优先考虑一维DP能不能做。写完状态后一定要先写递推关系再确定初始化最后才写循环。很多人习惯直接上手写循环结果状态转移公式都还没列清楚很容易在索引和边界上报错。贪心算法在笔试中同样是高频考点。贪心的难点不是写代码而是证明局部最优能推出全局最优。真实笔试里很少要求你严格证明但你要能从题目特征推测适不适合贪心。最简单的判断方式是每一步都只做当前收益最大的选择而且选完之后不会影响后面的收益计算那么就可以先拿贪心试。比如会议室安排、区间调度、跳跃游戏这类题基本都能贪心解。如果你发现当前选择会影响后续选择大概率不是贪心题要转回动态规划。2.2 字符串匹配KMP的next数组到底怎么算字符串相关的题目中KMP算法是笔试选择题和编程题都很常见的考点。很多同学背过KMP的模板但一换形式就不会用尤其是next数组的不同定义经常让人一头雾水。我看到热搜词里有一个很经典的例子模式串p abacaba要求计算它的next数组。这里先把约定说清楚next[i]如果定义为“模式串前i个字符中最长相等前缀后缀的长度”那计算逻辑是这样的子串长度1对应字符a没有相等的前后缀next[0] 0子串ab前缀a和后缀b不相等next[1] 0子串aba前缀a等于后缀anext[2] 1子串abac前缀和后缀没有公共部分next[3] 0子串abaca同样是只有单个a能匹配next[4] 1子串abacab最长的相等前后缀是abnext[5] 2子串abacaba最长的相等前后缀是abanext[6] 3。所以这个例子对应的next数组就是[0, 0, 1, 0, 1, 2, 3]。如果你看到的模板是next[0] -1或者在模式串前面加了一个哨兵字符那整个数组会有一次整体偏移这并不代表两种写法里有一个是错的而是不同竞赛或教材对“前缀函数”的定义做了调整。笔试里遇到这类题先看题目对next[i]定义的文字描述再套对应的计算方式千万不要自己惯性默写。真正熟练掌握KMP不只是会算数组还要理解它在失配时如何利用这个数组回退。next[i]保存的值表示如果当前字符失配模式串指针应该跳到哪个位置这样就可以避免文本串指针回溯把匹配复杂度稳定在O(nm)。在笔试编程题里KMP的裸题反而少一些更多是问“一个字符串里是否有重复子串”“求一个字符串的最短循环节”这类题在了解next数组之后会变得很简单。2.3 排序、二分与快速幂性价比最高的基础模块排序算法是笔试的常客但重点不在于让你手写快排的每一行而是考察复杂度分析和稳定性判断。比如给定一个几乎有序的大数组插入排序会比快速排序更快如果要求稳定排序且空间不能额外占用归并排序可能是更好的选择。关于堆排序热点词里也经常有人问它的核心是建堆和下沉调整时间复杂度稳定在O(nlogn)但常数较大笔试里通常用于解决“Top K”问题或者用堆来维护数据流的中位数。二分算法值得单独拉出来说。笔试里的二分题很多并不是“在一个有序数组里找一个数”而是“在一个满足单调性的范围内找边界”。这种题目统称二分答案先把问题转化成判定函数比如给定一个最大载重判断能不能在某种限制下完成运输然后二分载重值。二分代码看起来短却特别容易出错尤其是开闭区间和退出循环的条件。我个人的习惯是使用左闭右开写法循环条件while(left right)每次取mid left (right - left) / 2来防止整数溢出写完后一定用数组长度为1和长度为2的例子手动走一遍。快速幂也是笔试里经常出现的优化技巧尤其是在需要计算大指数取模的场景。暴力循环乘n次的时间复杂度是O(n)当n达到10^9以上时显然不能接受。核心思想是把指数拆成二进制比如计算a^1010的二进制是1010也就是a^10 a^8 * a^2每次对指数做位运算同时底数不断自乘复杂度降为O(logn)。这类代码网上模板很多但真正理解二进制拆分后遇到矩阵快速幂或者斐波那契数列优化也能顺手推出来属于学一次能用很久的算法。3. 完整还原一道算法题从读题、推导到AC3.1 一道典型的快速幂场景题为了让大家更直观地理解笔试编程题的完整链路我拿一个非常常见的题目来做演示给定三个不超过10^18的整数a、n、mod求a^n对mod取模的结果。这里的陷阱就在于a和n很大如果按照常规思路用一个循环做连乘时间复杂度是O(n)一旦n达到10^18程序会直接超时。所以正确做法就是用到上一节说的快速幂算法。读题阶段要注意两件事。第一输入有可能是多组数据所以不能只处理一次就输出第二n很大必须用64位整数接收如果使用int很容易在读取时就发生溢出。笔试平台的编译器有时候不会对这类溢出做任何提示但结果会错得莫名其妙。我在实际操作中一直建议凡是看到超过10^9的数据范围长期变量类型统一用long long。最直接的朴素版本是写成下面这样def pow_mod(a, n, mod): result 1 for _ in range(n): result (result * a) % mod return result % mod这个版本在n很小时没问题但显然不是这道题想要的答案。面试官或在线评测系统看的是你是否能根据数据范围选择合适算法。进阶版本就是把指数转成二进制快速跳着计算。3.2 快速幂代码与每一步推导标准写法是def fast_pow(a, n, mod): result 1 % mod base a % mod while n 0: if n 1: result (result * base) % mod base (base * base) % mod n 1 return result这个代码的核心逻辑我用一个具体例子解释。假设要求3^1010的二进制是1010。最开始result 1base 3。循环中先看n的最低位第1位是0所以不累积结果但base要平方变成9n右移一位变成5二进制101。现在最低位是1result 9base 9^2 81n右移变成2二进制10。此时最低位是0不累积但base 81^2 6561n右移变成1。最后一次n最低位是1result 9 * 6561 59049再对mod取模得到答案。整个过程中指数只按位数做了log10次操作速度提升明显。代码里面有几个细节值得注意。第一行对result取mod是为了应对a0或mod1这种边界避免初始值已经大于等于mod。还有base每次都要先取一次模是因为如果不做这一步base在平方过程中会膨胀得非常快很快就超过long long的可表示范围。这一点在C里尤其重要Java里虽然long也有上限同样不能忽略。3.3 这里容易踩的两个隐藏坑第一个坑是取模对象的顺序。很多初学者习惯在最后才取一次模比如result * base之后再统一求余这样做在数据较小时没错但base和result相乘的中间结果可能已经溢出最终得到的就是错误答案。正确做法是每一次乘完立刻取模保证中间过程的数值始终小于mod。第二个坑是mod为1的情况。如果mod等于1任何数对1取模结果都是0但有些代码会在初始化result 1时错误地认为result等于1最后输出1。虽然看起来是极端情况但笔试的测试点往往会塞入这类反直觉边界数据。所以在讲复杂度的时候我经常提醒同学不要只对着样例调代码样例只是给你解释题意真正的敌人往往藏在数据范围的边界上。这道题如果完整写完一个函数加上主函数里的输入处理三十行以内就能搞定。能顺利拿下这道题一是因为平时专门练过快速幂模板二是读题时意识到了数据范围决定算法三是边界数据上有意做了防御。算法笔试的分数就是靠这样的细节一点点堆出来的。4. 笔试实战中的常见报错与排查经验4.1 TLE和MLE先看数量级再调代码在线笔试最常见的一个结果是“时间超限”。看到TLE的时候很多人第一反应是优化常数比如把cin换掉、把递归改成循环。但实际上TLE的根本原因百分之八十是算法复杂度不合格。如果你用的是O(n^2)的算法而题目数据范围n10^5那无论怎么调输入输出基本都无法通过。正确做法是先把数据范围拉到最大估算自己代码的时间复杂度再判断是不是需要换算法。我在实际刷题和笔试复盘时会先在纸上写一行简易估算1秒大概能执行10^8次简单运算如果有两重循环嵌套每层10^5那一共10^10次肯定会超时。这时候不是去卡常而是想到用哈希表、前缀和、双指针、单调栈或排序来减少一层循环。同理内存超限MLE也先看空间复杂度一个int数组开到10^7就是40MB左右如果系统给的内存限制是64MB再加上其他变量和递归栈就已经非常危险。不要盲目把数组开到上限要根据题目范围精确计算所需容量。4.2 边界条件和输入输出的隐性失分很多人代码逻辑没问题样例输出也对但提交就是WA多半是边界条件没处理干净。最典型的几个空数组、数组长度为1、目标值不存在、数据中有负数、元素重复、结果超过int范围等。比如在二分查找中如果查找值小于数组最小值或大于最大值很多写法会在循环结束时返回一个错误索引在字符串处理中如果输入字符串包含空格使用普通输入可能只读取到第一个空格前的部分。还有一个容易忽略的点是在线评测平台的输入格式。有些题目会先给一个测试用例总数T然后循环读取T组数据有些则是连续读取直到文件末尾也就是常见的while(cin n)或sys.stdin.read()。如果格式判断错误很容易出现“只处理了第一组数据”的失误。我的习惯是拿到题目后先扫一眼输入描述里有没有“多组测试数据”这几个字再决定用什么方式读取不要想当然。4.3 一个可以直接抄的现场检查清单根据我踩过的坑和带过的人反馈我整理了一个笔试检查清单每次提交前快速过一遍能有效减少无谓的失分。检查项具体内容操作建议数据范围是否用long long / int64读取变量类型和题目范围匹配边界样例空数组、单元素、重复值手动构造三个最小输入跑一遍取模位置乘法后是否立即取模看中间值有没有可能溢出输出格式换行、空格、小数位数对照题目输出描述逐字检查多组输入是否读取全部测试用例确认循环读取到文件末尾内存使用数组开多大有没有浪费用范围计算后再决定数组容量复杂度最坏情况是否在1秒内估算最内层循环执行次数函数返回值提前return时结果是否正确检查所有分支都有返回值这个清单看着简单但每个词条背后都是真实的失分教训。比如取模位置那一项快速幂题目里如果不小心在最后统一取模数据一大马上就溢出这类错误不会在样例里暴露只会在隐藏测试点里阵亡所以不能靠侥幸。5. 怎么针对算法卷备战才算真正有效5.1 刷题优先级先把高频分类吃透再碰冷门准备算法笔试千万不要一上来就抱着几百道题硬刷。先按类别刷优先级大概是数组和哈希、双指针、二分、栈与队列、链表、二叉树、动态规划、贪心、图论基础、字符串。这十个类别覆盖了绝大多数笔试题目。尤其是动态规划和贪心别急着追求难题先把背包、最长子序列、区间调度、股票买卖这几个经典模型吃透笔试时看到类似包装就能快速映射到对应模型。我在给同学做复习规划的时候经常把刷题比作搭乐高。基础数据结构是基础积木块双指针、前缀和、哈希是拼接工具动态规划则是一套固定的搭建思路。当你看到一个题目时第一步不是想“这个题我有没有刷过”而是想“这个题涉及哪些积木块能用什么工具拼接”。冷门算法比如后缀数组、最大流、计算几何笔试出现概率低但准备时间够的话至少了解概念和适用场景选择题里遇到不慌就行了。5.2 现场时间分配和答题顺序笔试现场的时间分配直接决定最终成绩。我的建议是拿到卷子后先花两分钟浏览全部题目把每道题按难度分个级。一般情况下选择题控制在十分钟左右不能在一道概念题上反复犹豫。编程题从最简单的开始写先确保有一道题是完整AC的。只要有一道题AC心里就有底了剩下的时间再去啃难题。如果某道编程题看了五到十分钟还没有思路先跳过去做后面的题最后再回来。人的大脑在有时间压力下死磕一道题只会越来越乱而换一道题反而能让潜意识帮你沉淀思路。还有一个小技巧代码写完但样例不过时不要反复盯着代码看试着在草稿纸上手动模拟一遍变量流程。很多逻辑错误不是靠眼睛看出来的而是靠手算走查出来的。5.3 笔试完不要急着对答案很多同学交卷后第一件事就是满世界找答案对分数这对后续面试没有任何好处反而容易影响心态。我更建议的做法是趁记忆还热乎把自己没写出来的题重新复盘写一版完整能跑的代码存进自己的题解库。那些你见过但做不出来的题才会真正进入长期记忆。笔试从来不是终点面试手撕代码的题目难度有时比笔试还高把笔试里暴露出的薄弱点补上价值远大于对答案时的短暂安心。我从自己的备战过程里感受到算法能力提升最明显的阶段不是疯狂刷陌生题的时候而是老老实实把高频题分类吃透、把每个模板的边界条件和复杂度都搞清楚的时候。2022小米秋招算法卷1这类题目说到底考的是基本功和稳定性这两点没有捷径只能靠平时一遍遍练习和复盘。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进