ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

快手算法B卷核心考点解析:KMP、动态规划与贪心实战指南

快手算法B卷核心考点解析:KMP、动态规划与贪心实战指南 快手2019年春季校园招聘笔试里的“算法B试卷”在当年参加过的同学圈子里讨论度一直不低。很多人第一次看到“算法B”这个命名会有点懵以为只是难度分档实际上它指向的是一套有明确岗位倾向、题型结构和考察侧重点的算法笔试卷。最近不少准备暑期实习和秋招的同学翻出这套题来刷跑来问我当年是怎么准备的、B卷到底考哪些东西、哪些算法是必练的。我不打算去复述某一道具体原题——网上能搜到的回忆版题目本身信息也不完整而且每年的题都会换。我更想做的是把这类“算法B卷”的考察逻辑、高频算法点、一道典型题目的完整解题路径以及笔试现场最容易丢分的地方一次性讲清楚。无论你是第一次参加算法笔试还是已经刷了不少题想查漏补缺这篇内容都值得你花十分钟看完。1. 快手算法B卷的试卷构成与考察范围复盘1.1 “算法B”这个后缀透露出什么信息快手校招笔试分不同卷型“算法”两个字说明岗位方向是算法岗而后缀的“A”和“B”通常不是难度之分而是岗位细分或者投递方向不同。B卷更多偏向搜索、推荐、广告这类业务线算法岗考察的内容不会像纯研究岗那样深挖论文推导而是更看重工程实现能力和算法基本功。我自己参加过的算法笔试里B卷的明显特征是题目数量不多但每道题都有一定区分度选择题会考察概念理解和复杂度的判断编程题则考察数据结构和算法在真实场景下的应用能力。和A卷相比B卷在题目表述上会更贴近业务——比如会用一个“用户点击序列”或者“物品相似度”的包装但内核还是经典的算法题。1.2 题型结构与考点频次从过往同学反馈和各家算法笔试题的横向对比来看算法B卷的常见结构是三到四个部分题型题量考察重点耗时建议单选题10~15题数据结构、排序、复杂度、基础概念15~20分钟多选题5题左右算法边界条件、机器学习基础5~10分钟简答/推导题1~2题KMP、DP状态定义等原理推导15分钟编程题2~3题数据结构 算法综合应用60分钟以上单选题里出现频率很高的包括排序算法的稳定性、各种排序在最好最坏情况下的时间/空间复杂度、栈和队列的应用场景、二叉树遍历的性质、哈希表的冲突处理方式。这些不单是死记硬背而是考察“你在写代码时有没有真正理解这个数据结构为什么这样设计”。编程题的高频考点则集中在几个方向字符串匹配类KMP、动态规划背包、序列DP、区间DP、贪心算法、图的最短路Dijkstra、Floyd和拓扑排序、以及排序思想的应用快排的partition、归并的逆序对计数。1.3 难度梯度送分题、核心题、压轴题的分界我是按“三档难度”来拆解这类试卷的。第一档是送分题占比大约40%。这类题基本是数据结构基础操作比如实现栈、队列、链表反转、二叉树层序遍历。需要注意的是送分题不等于可以不准备——正因为它简单写错一个边界条件就完全没分反而最容易拉开差距。第二档是核心题占比约40%。这类题需要你选出正确的算法方向并且能在规定时间内写出正确代码。典型的是给一个数组求连续子数组的最大和、求两个字符串的最长公共子序列、给定多个区间合并重叠区间。这些题难在“你能不能快速判断它属于哪类算法”而不是算法本身有多难。第三档是压轴题占比约20%。这题通常是业务场景包装过的综合题可能结合两个以上的算法点。比如先贪心排序再动态规划求最优解或者用KMP解决字符串匹配后再套一个哈希表做统计。这题不指望所有人都能做出来但要尽量拿到部分分展示你的思考过程。2. 高频算法考点背后真正想考的能力拆解2.1 KMP与字符串匹配next数组不是背出来的算法B卷的选择题里经常出现这样一道对于模式串 pabacaba其 next 数组是多少。很多同学看到这种题就慌因为KMP的next数组定义在不同教材里有不同版本有的从0开始有的从-1开始一不小心就选错。我给一个所有人都能记住的推导方式。KMP核心是当匹配失败时模式串指针不是从头再来而是回退到“已经匹配部分的最长相同前后缀长度”。所以next数组的本质就是求模式串每个前缀子串的“最长相等前后缀长度”。拿 pabacaba 举例我按next[i]定义为“前i个字符组成的子串中最长相等前后缀长度”来算i1子串 a前后缀为空next[1]0i2子串 ab前缀a后缀b不相等next[2]0i3子串 aba前缀a后缀a相等长度为1前缀ab后缀ba不相等next[3]1i4子串 abac前缀a和后缀c不匹配next[4]0i5子串 abaca前缀a后缀anext[5]1i6子串 abacab前缀ab后缀ab且最长next[6]2i7子串 abacaba前缀aba后缀aba且最长next[7]3所以最终 next [0, 0, 1, 0, 1, 2, 3]。这里的关键不是背答案而是你亲手画一遍“前缀后缀比较”的过程。笔试时如果紧张可以用“错开一位比较两个指针”的朴素方法手算虽然慢一点但不会错。KMP在编程题里通常不是单独考而是作为中间步骤。比如给定一个长文本串和一个模式串统计模式串在文本串中出现的次数允许重叠。这种题暴力解法在字符串长度达到10^5级别时会超时必须用KMP的O(nm)复杂度来解决。实际上你在做题时先写一个暴力版本然后优化成KMP版本会更容易理清思路。2.2 贪心算法如何证明你的贪心是对的贪心算法是算法B卷里一个很微妙的存在。它看起来简单——每次选当前最优就行了——但笔试中真正的区分度在于你有没有办法证明你的贪心策略是对的。比如经典的发饼干问题每个孩子有一个胃口值每块饼干有一个大小值饼干不能拆分问最多能满足多少个孩子。正确的贪心策略是把孩子和饼干都排序然后用最小的饼干去满足胃口最小的孩子。这个策略可以证明是优于“用大饼干满足小胃口”的因为最“容易满足”的孩子被最小可用饼干满足不会浪费更大饼干的可能性。但有些贪心策略是看起来很对、实际是错的。比如“在数组里找两个数使它们差的绝对值最大”有人会觉得贪心就是找最大值和最小值但实际上这题只要一次遍历记录最大值最小值就可以了根本不是贪心问题。所以笔试里看到贪心标签的题先问自己三个问题这个选择是否只依赖当前状态做出选择后子问题是否独立局部最优是否真的能推出全局最优如果三个问题里有任何一个不确定那就应该考虑动态规划。比如区间调度问题你贪心按结束时间排序证明方式是“交换论证法”——假设最优解中第一个区间不是结束时间最早的那个可以把它替换成最早结束的区间而不影响后续选择从而逐步证明贪心解不低于最优解。笔试时就算不写完整证明也要在代码注释里写清“为什么这样贪心”的直觉部分给分时很有帮助。2.3 动态规划状态定义比转移方程更先想清楚算法B卷编程题里动态规划出现概率极高而且通常是核心题。很多同学的第一反应是套模板比如“背包九讲”、LIS、LCS之类但真正的考点往往不是模板本身而是你对状态的定义能不能覆盖题目所有约束。以最长连续递增子序列为例。很多同学看到“子序列”就想去套LIS的O(n^2)解法但实际上这题要求连续所以状态可以压缩成“以当前位置结尾的最长连续递增序列长度”一次遍历就完成。再比如最大子数组和Kadane算法的核心是定义dp[i]为“以nums[i]结尾的子数组的最大和”转移就是 dp[i] max(nums[i], dp[i-1] nums[i])最终答案是max(dp)。我在这里想强调一个备考思路做题时先写清楚状态定义再写转移方程最后再写代码。如果你发现自己需要三维数组才能表示状态先停下来想想是不是状态定义里包含了冗余信息。常见的优化方向包括把区间DP的起点终点两个维度压缩成“区间长度起点”两个维度把二维dp变成滚动数组降维用二分优化LIS的dp数组。笔试中还有一类DP容易让人懵带约束的背包问题。例如有n个物品每个物品有重量和价值背包容量为W但要求选取的物品数量不能超过k个。这种题如果不注意很可能用二维dp导致空间超限而正确做法是在物品维度上循环时增加一个数量维度复杂度为O(nkW)。这种“多一个约束就多一个维度”的思路值得考前自己推一遍。2.4 图算法与排序基础能力才是区分度算法岗笔试中图算法看起来不常考但一旦考就是核心区分题。最典型的是Dijkstra算法。很多人背模板但没想明白一个关键点——Dijkstra为什么不能处理负权边因为它的核心假设是“每次从堆里弹出的点当前距离已经是最终最短距离”这个假设在存在负权边时会被推翻。笔试选择题特别喜欢在这个地方设坑给你一个带负权边的图问Dijkstra跑出来的结果为什么不对。另一个高频图算法是拓扑排序。它不单考“能不能用Kahn算法实现”还常常结合业务场景比如任务依赖调度、课程安排是否可行。Kahn算法的核心是维护入度为0的节点集合每次取出一个节点加入结果序列并更新邻居节点的入度。这里有个容易错的地方如果图中的节点数量很大用数组标记入度时要小心重边题目没说“无重边”时需要在读入时做去重或累加处理。排序算法更是选择题的重灾区。归并排序的稳定性和逆序对计数、快排的partition操作和它在无序数组中找第K大元素的应用、堆排序在Top K问题里的变体这些都是B卷选择题的高频素材。我建议用一张表把常见的算法时间复杂度、稳定性、空间复杂度整理清楚算法平均时间最坏时间空间稳定性冒泡O(n^2)O(n^2)O(1)稳定快排O(n log n)O(n^2)O(log n)不稳定归并O(n log n)O(n log n)O(n)稳定堆排O(n log n)O(n log n)O(1)不稳定复习重点不要放在背诵上而是每张表都能用自己的话解释一句“为什么”。比如归并为什么需要O(n)辅助空间是因为合并两个有序数组时需要一个临时数组来存结果这是空间换时间的典型案例。3. 一道典型算法B卷真题的完整解题复盘3.1 题目描述与输入输出约定结合搜索引擎里“快速幂算法C”“KMP算法”“二分图HK算法”这些高频搜索词我选了一道足够经典、难度定位在“核心题”到“压轴题”之间的题目来做完整复盘。题目是这样设计的给定两个字符串 text 和 pattern长度分别为 n 和 m1 n, m 10^5求 pattern 在 text 中出现的所有位置的下标。允许重叠匹配输出所有起始位置按从小到大排序。如果不存在匹配输出空行。这个题目一看就是KMP的主场。但很多人会犯一个理解错误题目说“允许重叠匹配”有些人就以为要对每个可能的起始位置都做一次匹配结果写成O(n*m)的暴力解法。实际上KMP天然支持重叠匹配因为匹配成功后模式串的指针回退到 next[m] 而不是退回到0。3.2 暴力思路为什么会超时暴力解法很好写枚举 text 的每个位置作为起点逐位和 pattern 比较。但仔细算一下复杂度外层循环 n 次内层比较 m 次最坏情况下每次比较到最后一个字符才发现不匹配复杂度是O(n*m)也就是10^10次操作在笔试环境下几乎不可能通过。有些同学会提出“用Python的str.find或者Java的indexOf”但笔试系统里这种内置方法不一定被允许而且它内部的实现比如BM算法虽然平均性能不错在最坏情况下仍然可能退化。所以当题目明确考察“字符串匹配”时最优解法就是KMP。3.3 KMP完整实现与next数组推导下面给出KMP的完整Python实现不是最优的炫技写法而是最清晰、最容易笔试不写错的标准写法def build_next(pattern): m len(pattern) nxt [0] * m j 0 for i in range(1, m): while j 0 and pattern[i] ! pattern[j]: j nxt[j - 1] if pattern[i] pattern[j]: j 1 nxt[i] j return nxt def kmp_match(text, pattern): if not pattern: return [] n, m len(text), len(pattern) nxt build_next(pattern) res [] j 0 for i in range(n): while j 0 and text[i] ! pattern[j]: j nxt[j - 1] if text[i] pattern[j]: j 1 if j m: res.append(i - m 1) j nxt[j - 1] return res重点说一下匹配成功后的处理。当 j m 时说明 pattern 在 text 中完成了一次完整匹配此时把起始位置 i-m1 加入结果。然后 j 要回退到 nxt[m-1]也就是模式串最长相同前后缀的长度这样才能支持重叠匹配。举个例子帮助理解text abababapattern aba。匹配到 i4 时text[2:5]aba下一次应该从哪个位置继续找正确做法是让 j 从3回退到 nxt[2]即1因为aba的最长相同前后缀是a。这样下一次比较 text[5] 和 pattern[1]就能找到位置 i4即 text[4:7]aba这个重叠匹配。3.4 易错点与边界用例自测这道题面试中真正让人丢分的地方不在KMP本身而是几个细节。第一个易错点当 text 的长度小于 pattern 时不需要任何匹配直接返回空列表。如果 n m还去跑KMP虽然结果也是对的但浪费了时间而且在某些极端写法下可能出现数组越界。最简单就是在函数开头加一个判断if n m: return []第二个易错点next数组构建时的循环边界。很多同学用 for i in range(1, m) 导致最后一位的next没算出来或者 while 里面条件写错产生了死循环。我的建议是自测时用几个特殊pattern空串、单个字符、全部相同字符、无重复字符的串。比如 patternaaaanext应该是[0,1,2,3]patternabcdenext应该是[0,0,0,0,0]。第三个易错点是输出格式。题目要求“输出所有起始位置按从小到大排序”。KMP天然是从左到右扫描的所以顺序是对的不需要额外排序。如果不用KMP而用暴力法也要注意不要每次都从尾部往头部寻找否则顺序会反。最后把这段代码跑几个用例print(kmp_match(abababa, aba)) # [0, 2, 4] print(kmp_match(aaaa, aa)) # [0, 1, 2] print(kmp_match(hello, xyz)) # []如果这三个输出都正确代码基本就没问题了。4. 笔试现场最容易丢分的环节与应对策略4.1 输入输出与本地调试的坑算法笔试最常见的“非技术性挂科”原因是输入输出格式不对。快手这类在线笔试系统一般要求从标准输入读取数据输出到标准输出。很多同学平时练习时习惯用LeetCode的模板函数参数已经给好了到了笔试题里要自己写输入解析就手忙脚乱。我建议平时练题时至少用两种方式各写一遍一次按LeetCode方式写函数一次按ACM模式自己处理sys.stdin.read()。尤其是多组测试用例的情况输入里可能有多行每行是空格分隔的整数需要写一个统一的数据解析逻辑。比如import sys def main(): data sys.stdin.read().strip().split() if not data: return t int(data[0]) idx 1 for _ in range(t): n int(data[idx]) m int(data[idx 1]) idx 2 text data[idx] pattern data[idx 1] idx 2 res kmp_match(text, pattern) if res: print( .join(str(x) for x in res)) else: print() if __name__ __main__: main()笔试时千万注意不要在输出结果的末尾多打空格也不要用print列表的方式输出带方括号的结果否则判题系统会判格式错误。4.2 时间分配先拿必得分再啃难题我的策略是“两遍法”。第一遍把试卷从头到尾快速扫一遍花2到3分钟标记出哪些题是送分题哪些是核心题哪些是压轴题。第一遍先做送分题确保稳定拿到手第二遍做核心题每道题给自己设定一个时间上限一般是20分钟压轴题留到最后如果还剩20分钟以上再碰否则果断放弃。编程题部分我强烈建议先写一个能跑的暴力版本哪怕复杂度高。原因有两点第一笔试题很多时候是按通过用例百分比给分的暴力版本能帮你拿到部分用例的分第二先写暴力版能帮你确认自己对题意的理解是否正确避免思路跑偏还觉得自己很对。4.3 没有思路时的保底得分技巧遇到完全没思路的题不要直接空着。用文字在代码注释里写清你的思考方向比如“我打算用贪心按结束时间排序每次选最早结束的区间”能得到一些手工判分老师的印象分。有些题目支持打印调试信息你可以把中间结果打印出来帮助自己理解数据但提交前记得删除。还有一个技巧是多看几遍题目给的示例。示例通常包含了题目设计者认为最容易错的边界情况比如输入为0、输入为负数、空数组、重复元素、完全逆序。如果示例没覆盖自己在脑子里补几个极端场景能帮你提前发现代码的bug。5. 考前两周的复习路线与常见误区5.1 按优先级排序的复习清单如果距离笔试只剩两周我建议不要盲目刷题而是按“高频 中频 低频”的顺序来安排。我在实际准备中把高频数据结构和算法按优先级分了三个梯队第一梯队必会考前一天都不能丢数组、链表、栈、队列的遍历与操作二分查找排序快排、归并哈希表二叉树的前中后序与层序遍历动态规划的经典模型背包、子序列、子数组、区间。第二梯队大概率考要能默写模板KMP、Trie树、并查集、Dijkstra、拓扑排序、贪心经典题、快速幂。第三梯队有精力再准备红黑树的原理、B树、最小生成树Kruskal、线段树、最大流、字符串的Z算法、Manacher算法。这些在B卷里很少考Java实现更多是选择题里考概念时间不够就只记结论和适用场景。5.2 刷题数量 vs 刷题质量很多同学问我“两周刷300题够不够”。我的观点是高效准备不是刷题数量堆出来的关键是每道题做完后有没有做“复盘三问”——这是什么类型的问题为什么会想到这种解法这道题能不能变形考如果三个问题都能回答清楚做50道题比盲目重复做300道题效果要好得多。刷题的具体建议是每天分三个时段早上做2~3道“核心题”重点是练习快速定位算法类型下午集中整理错题把做错的题按错误原因分类——是因为边界条件、因为思路错了还是因为某个算法不熟晚上花30分钟手写一个“高频算法模板”不用电脑直接在纸上写KMP的next数组、快排的partition、Dijkstra的堆版本。5.3 机器学习算法在B卷里的出现形态既然岗位方向是算法岗试卷里也会出现少量机器学习的基础题但通常不会要求手推复杂的公式。出现的形态一般是选择题比如过拟合的解决方案有哪些、什么是交叉验证、SVM的核函数作用、决策树的信息增益和基尼系数的区别、K-Means聚类如何选取K值。这类题分值占比不高但如果基础不牢很容易在选择题里被拉开差距。我建议时间紧张的同学只看“机器学习高频面试题整理”这种知识清单不要在笔试备考阶段去啃西瓜书的推导细节。你真正需要准备的是“模型、指标、适用场景”这三层对应关系。还有一点容易被忽略有些算法热词看起来高大上比如粒子群算法、模拟退火、卡尔曼滤波、强化学习但在校招算法B卷里出现的概率很低。它们更多是研究岗或特定方向面试才会深入考察。如果备考时间有限不建议在这些方向花太多时间先把基础算法练扎实才是正路。5.4 复习时最容易踩的三个坑第一个坑是只刷不做总结。每天刷题刷到很晚但从不整理遇到类似的题还是做不出。我自己的经验是准备一个错题本记录每道题的“思维卡点”——到底是哪一步没想到。半个月后回看错题本你会很清楚地看到自己的薄弱点集中在哪几类算法上。第二个坑是忽略手写代码训练。笔试时不一定有自动补全和语法高亮有些在线系统甚至没有缩进自动调整。如果你平时全靠IDE的提示到笔试时很可能连一个简单的树的遍历都会卡壳。办法是每天用纸和笔写代码或者在任何一个普通文本编辑器里写不依赖代码补全。第三个坑是不做模拟考试。至少考前一周完整模拟一次90分钟的笔试用难度相近的题计时完成。模拟的意义不只是练题更是适应笔试节奏找到适合自己的时间分配方式。我模拟时发现自己的选择题做太慢导致编程题时间不够后来调整为“选择题限时15分钟超时先蒙一个标记起来”的策略编程题时间才够用。我最后再分享一个实际操作中的体会考前那晚一定要把KMP、快排、Dijkstra、归并排序这四个算法手写一遍再睡。它们不是最难的算法却是最容易在考场上突然脑子一片空白、写不出正确代码的算法。刷题再多不如临考前亲手过一遍模板来得踏实。
RELATED READING

延伸阅读

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