B站2019秋招笔试编程题解析:字符串与动态规划实战指南

📅 发布时间:2026/8/31 7:27:33
B站2019秋招笔试编程题解析:字符串与动态规划实战指南
1. 2019秋招笔试全景题型分布与考察逻辑先交代一下背景。2019年那会儿B站的秋招笔试还远没有现在这么卷但已经能看出这家公司的出题口味和一般互联网大厂不太一样。我当时完整刷过那一批题目也帮学弟学妹做过复盘一个很直观的感受是B站笔试不考偏题怪题几乎全部落在“基础算法 数据结构 简单思维题”这个范围内但它的基础题并不是白给很多题表面一看就会真正动手写才发现边界条件非常容易漏。先说笔试的整体结构。B站2019秋招技术岗笔试一般分成两部分第一部分是选择题涵盖计算机网络、操作系统、数据库、C/Java基础等常规八股第二部分就是编程题通常是3到4道难度从简单到中等递进。从当时牛客网上的投票和讨论帖来看编程题的通过率并不高很多同学挂在第二题和第三题上原因并不是题目本身有多难而是对输入输出格式、题目隐含条件、以及时间复杂度的把控不到位。我梳理了一下那批题目的类型分布大概是这样的题型出现频率典型考察点字符串处理高翻转、压缩、括号匹配、子串统计动态规划高一维DP、二维DP、背包类变体贪心与排序中高区间调度、任务安排、排序策略数学规律中找规律、位运算、幂运算模拟题中低按题意一步步模拟考察编码细节这里有个很有意思的现象B站笔试很少出现纯粹的“模板题”比如裸的LIS、裸的最短路径。它更习惯把算法包装在一个具体的业务场景里比如“视频播放器缓冲区的调度”“弹幕字符串的过滤规则”“评论区的敏感词替换”等等。这其实是B站出题的一个显著特征也是这个题集和其他公司题集最大的区别。所以如果你现在准备B站的技术岗笔试我给你的第一个建议是不要把大量精力放在背模板上而是要训练自己“把场景翻译成算法模型”的能力。这道题到底在考什么比这道题怎么解更重要。另外还有一个细节值得注意那年的笔试环境支持的语言比较广C、Java、Python都可以用但判题机的输入输出是标准的牛客风格也就是多组测试样例、每组用逗号或空格分隔、需要在循环里读取输入。很多人平时在LeetCode上刷惯了函数签名直接给好一到牛客这种“自己写完整程序”的笔试环境就懵了这是另一个隐形的失分点。后面我会专门讲这个。2. 字符串处理题笔试里最“友好”也最阴险的高频题型字符串题几乎成了2019秋招各厂笔试的标配B站也不例外。它的特点非常鲜明入口浅、上手快但边界条件极其琐碎稍不留神就漏掉一两个特殊输入导致整道题AC不了。我选了三个当时比较有代表性的题目展开讲。2.1 字符串循环右移的三种写法与边界陷阱这是一个非常经典的题目给定一个字符串和一个非负整数K将字符串循环右移K位。比如abcdefg右移2位得到fgabcde。很多人第一反应是“把末尾的K个字符剪切到开头”但真动手时问题就来了——K可能比字符串长度还大。比如字符串长度是7K是10你如果直接切末尾10个字符那直接越界崩溃就算你做了取模还要考虑“K等于0”和“字符串为空”这两个特殊输入。我当时写这道题采用的是“三步反转法”代码很短但每一步的索引必须算清楚。以abcdefg右移2位为例先反转整个字符串得到gfedcba再反转前2个字符得到fg最后反转剩余部分得到abcde拼起来正好是fgabcde。这个方案的时间复杂度是O(n)空间复杂度是O(1)不需要额外数组。实际做题时有一个非常容易踩的坑输入格式。牛客网的判题系统里字符串和K可能是用空格分隔在同一行也可能分两行输入更坑的是有的样例里K用负数表示“左移”你需要提前读清楚题目要求。我建议不管题目怎么说统一写成循环读取输入的模板避免遗漏样例。import sys def reverse(s, left, right): # 左闭右闭区间反转 while left right: s[left], s[right] s[right], s[left] left 1 right - 1 for line in sys.stdin: line line.strip() if not line: continue parts line.split() if len(parts) 2: continue s list(parts[0]) k int(parts[1]) n len(s) if n 0: print() continue k k % n # 先整体反转再局部反转 reverse(s, 0, n - 1) reverse(s, 0, k - 1) reverse(s, k, n - 1) print(.join(s))这段代码我建议大家按“零长度字符串”“K为0”“K大于n”三个用例分别测一遍只要这三个用例都能通过这道题基本就稳了。2.2 括号匹配的进阶最长有效括号长度普通括号匹配是栈的入门题B站2019秋招没有止步于此它考了一个进阶版本给定一个只包含(和)的字符串找出其中最长的有效括号子串长度。比如(()的最长有效子串长度是2)()())的最大长度是4。这道题如果只用栈写法上有一点讲究。大多数人会想到用栈存字符但那只能判断“整个字符串是否匹配”无法求出“最长连续匹配子串”。正确的做法是栈内存下标而不是字符。栈底始终保持一个“最后一个未匹配的右括号的下标”作为计算长度的基准。我贴一下我当时提交的版本容易理解也容易记def longest_valid_parentheses(s): stack [-1] max_len 0 for i, ch in enumerate(s): if ch (: stack.append(i) else: stack.pop() if not stack: stack.append(i) else: max_len max(max_len, i - stack[-1]) return max_len核心逻辑只有一句话遇到右括号就出栈出栈后如果栈空说明这个右括号无法匹配把它本身入栈作为新的基准如果栈不空那么当前下标减去栈顶下标就是一段有效括号子串的长度。笔试时我见过不少同学在这道题上TLE原因是用了replace((), )循环删除这种写法最坏情况时间复杂度是O(n²)字符串一长就超时。笔试环境的时间和空间限制通常比LeetCode更紧所以一定要用O(n)的解法。2.3 从B站弹幕场景出发的字符串压缩变体还有一道题我印象很深它模拟的是弹幕文本的压缩规则给定一个字符串将连续出现的相同字符压缩成“字符出现次数”的形式比如aaabbc压缩成a3b2c1但如果压缩后的长度不小于原串长度则保留原串。这道题的陷阱在于“如果压缩后更长就保留原串”这个条件。很多人按部就班压缩完就输出了完全忘了比较长度导致一道简单题直接0分。还有一层坑是题目里的“压缩后长度”包含数字占位如果某个字符出现了超过9次需要用两位数表示这个细节很多考生都没注意到。我的建议是字符串类题目一定先把特殊条件圈出来然后用“原串为空”“单字符重复”“字符连续出现超过9次”这三个用例自测一遍。这种题不考智商考的就是细心程度而笔试考场上最缺的就是细心。3. 动态规划把“状态定义”想清楚你已经赢了一半动态规划在B站2019秋招编程题里的出镜率非常高但它的难度曲线比LeetCode上的同类题目要柔和一些基本不出现“插头DP”“树形DP”这种竞赛级别的难点更多集中在最基础的线性DP和区间DP上。3.1 一维DP的经典模型跳石板问题有一道题让我记忆很深它用的是“跳石板”这个场景小B从编号为N的石板出发每次可以跳的步数是当前编号的一个约数且不能等于当前编号本身目标是跳到编号为M的石板问最少需要跳几次。这道题本质上是“最短路径”或“BFS/DP”模型但很多人的第一反应是递归暴力搜索导致超时。正确做法是从N到M正向递推对于每个位置i枚举i的所有约数d然后更新dp[i d] min(dp[i d], dp[i] 1)。这里有两个关键细节。第一约数不包括1吗题目说的是“一个约数且不能等于当前编号本身”1是合法约数也就是说可以从i跳到i1这一点特别容易漏。第二需要跳到的位置可能远超M因为一次跳跃可能越过M然后再回头仔细读题会发现题目隐含“只能向右跳”所以一旦位置超过M就不再考虑直接剪枝。我当时写的版本是这样的def jump_stone(n, m): INF float(inf) dp [INF] * (m 1) dp[n] 0 for i in range(n, m 1): if dp[i] INF: continue # 求出i的所有约数除数 for d in range(2, int(i ** 0.5) 1): if i % d 0: if i d m: dp[i d] min(dp[i d], dp[i] 1) other i // d if other ! d and i other m: dp[i other] min(dp[i other], dp[i] 1) return dp[m] if dp[m] ! INF else -1注意我在枚举约数时从2开始因为从i跳到i1虽然合法但它属于基础情况你可以选择在循环外单独处理或者干脆把1也加进去。笔试时这个细节并不会影响最终求得最小值因为跳1步本质上只是增加了搜索范围不会让答案变小但如果M比较大而N比较小不处理跳1步会导致大量无效状态。我在代码里从2开始枚举实际上是为了减少不必要的更新同时需要把i1单列出来处理否则可能出现“本该跳1步到达的路径没被更新”的问题。建议读者在动手前先在草稿纸上推演几个小例子比如N4,M24看看最少跳几次再对照代码结果这样印象更深。3.2 二维DP的区间模型合并回文子串还有一道题是典型的二维DP它要求判断一个字符串能否通过“插入最少字符”变成回文串。换句话说给定一个字符串s最少插入多少个字符能让它变成回文串。这道题的标准解法是求原串和逆序串的最长公共子序列LCS答案就是n - LCS长度。但如果是笔试时第一次见到这个转换并不好想。还有一种更直观的区间DP做法dp[i][j]表示子串s[i..j]变成回文串所需的最少插入字符数转移方程是如果s[i] s[j]那么dp[i][j] dp[i1][j-1]否则dp[i][j] min(dp[i1][j], dp[i][j-1]) 1这个方程的直觉是左右两端字符相等那就不用动它们只需处理中间部分不相等就选择在左端补一个和右端相同的字符或者在右端补一个和左端相同的字符取代价更小的方案。区间DP的遍历顺序要特别注意不能简单地按i从0到n、j从0到n遍历因为dp[i][j]依赖dp[i1][j]也就是说i要从大到小遍历j要从小到大遍历。这个遍历顺序错误是初学者最常见的错误笔试时一旦状态更新顺序错了结果会完全不对而且很难排查。我当时提交了一个记忆化搜索版本避免遍历顺序的坑def min_insertions(s): n len(s) memo {} def dfs(i, j): if i j: return 0 if (i, j) in memo: return memo[(i, j)] if s[i] s[j]: res dfs(i 1, j - 1) else: res min(dfs(i 1, j), dfs(i, j - 1)) 1 memo[(i, j)] res return res return dfs(0, n - 1)记忆化搜索的好处是免去了思考遍历顺序的负担坏处是递归深度可能比较大笔试环境如果限制递归深度长字符串会爆栈。所以稳妥起见还是建议掌握迭代写法并且把遍历顺序当成一个固定的套路来记外层循环枚举“区间长度”内层循环枚举“区间起点”。这个套路适用90%的区间DP题。3.3 动态规划题笔试中的通用检查清单结合那年的题目我总结了一套动态规划题的笔试检查流程每次做题都按这个来能大幅减少漏判状态定义是否覆盖了题目所有维度的信息如果题目有两个变量比如“前i个数字中选j个”那么状态大概率是二维的。初始状态是否合理dp[0]、dp[i][i]这类边界值一定要手工验证很多错解就是初始状态设错。转移方程是否有遗漏比如“每个元素最多选一次”和“可以重复选”对应的转移完全不同。最终答案从哪里取并不是所有题目都输出dp[n]有的要输出max(dp[0..n])有的要输出dp[m][n]看清楚再下手。空间能否优化如果只需要上一行的状态滚动数组能把O(n²)降到O(n)这在内存限制比较紧的笔试里非常关键。我见过太多同学一上来就写dp [[0] * n for _ in range(n)]写到一半发现状态定义错了整段代码作废重来。在纸上先把状态和转移方程写清楚再动键盘是在给后面的自己省时间。4. 贪心与排序看似朴素实则全是细节贪心算法在2019秋招笔试中出现频率不低但它的特点是解法往往只有几行证明却要花一堆时间。很多人做贪心题靠“感觉”感觉对了AC感觉错了WA复盘时根本不知道自己错在哪。我来拆两个当年出现的典型场景。4.1 区间调度问题的排序策略按左边界还是右边界题目大致是这样给定若干个区间每个区间表示一个任务的开始时间和结束时间同一时间只能做一个任务问最多能完成多少个任务。这是经典的“区间调度问题”标准解法是按结束时间从小到大排序然后贪心地选“最早结束的且与已选区间不重叠”的区间。为什么按结束时间排序而不是按开始时间因为一个任务越早结束它给后面留下的时间越多局部最优能推出全局最优。但笔试时有一种变体很坑题目要求你输出“最少需要多少个教室/多少个资源才能容纳所有任务”这就变成了“最多同时重叠的区间个数”问题解法是完全不同的。前者是“选最多的互不重叠区间”后者是“找最大重叠次数”两者都叫区间调度但一个是贪心一个是扫描线。我见过大量考生在这道题上栽跟头就是因为把这两种变体搞混了。看清题目问的是“最多完成几个”还是“需要几个资源”是你拿到题后第一件要做的事。如果按结束时间排序核心代码大概是这样的intervals.sort(keylambda x: x[1]) count 0 last_end -float(inf) for start, end in intervals: if start last_end: count 1 last_end end注意边界条件start last_end表示前后两个区间可以背靠背衔接如果题目要求“结束时间和开始时间不能相同”就改成。4.2 带权重的贪心如何验证“看起来对”的贪心策略有一道题涉及“带权任务调度”每个任务有一个截止时间di和一个收益wi每个任务耗时1个单位问如何安排任务顺序使得总收益最大。常规做法是按收益从大到小排序然后用并查集或一个布尔数组来维护“每个时间片是否被占用”尽量把收益高的任务安排在截止时间之前。这个解法本质上是贪心但它的正确性并不像区间调度那么直观笔试时你很难在短时间内严格证明。我的建议是先用小规模数据暴力枚举验证你的贪心策略如果贪心结果和暴力结果在大样本下都一致再放心提交。笔试环境里可以多写一个暴力函数作为对拍器生成随机小数据两边结果对比。这个操作看起来多花了几分钟但能让你避开大量“玄学WA”。再补充一个细节这类题通常需要对时间从后往前安排任务也就是“尽量占用截止时间越晚的时间片”这样可以给截止早的任务留出更多空间。从后往前填时间片是这个贪心策略的关键很多人写成从前往后填结果收益损失很严重。4.3 排序题里容易被忽略的“稳定性”要求B站2019秋招有一道题要求对多个结构体按某个字段排序如果字段相同则按输入顺序排列。很多考生直接用Python的sorted()但它默认是稳定排序这个没问题可如果用了C的sort()它是不稳定排序就需要在比较函数里加入下标作为第二关键字。还有一个更隐蔽的坑题目要求排序后输出“原始下标”也就是说你不能只对值排序还要保留每个元素在原数组中的位置。这种题考察的是“排序 下标映射”如果你只对值排序后面想找原始下标就只能用index()方法在数组里逐个找时间复杂度瞬间变成O(n²)碰到大样例必TLE。我建议遇到这种题一开始就把元素封装成(value, index)的形式排序后再拆包。多写一个元组能帮你省掉后面一整串麻烦。5. 手写代码的应试细节从编译环境到边界条件很多人刷了很多题但一到笔试环境就发挥失常。我这里把当年踩过的坑和后来总结出的经验集中说一遍这些都是“不刷题根本发现不了”的东西。5.1 牛客/赛码笔试环境的隐藏坑B站那几年用的笔试系统主要是牛客网偶尔用赛码网。这两个平台都有一个共同点需要你自己处理输入输出而且main函数拿到的输入可能是多组测试数据。常见的输入坑包括第一行是“测试组数T”后面每一行是一组测试数据你需要先读T再循环T次。有的题不告诉你有几组数据你需要用while True不断读读到EOF为止。数组的输入可能带方括号和逗号也可能用空格分隔你要么用split()切分要么用正则提取数字不要想当然。输出格式多了空格或者少了一个换行会被判格式错误这种错误有时候比WA更冤。我的习惯是准备一个统一的“读入模板”放在编辑器里不管什么题先搭好骨架import sys def solve(): data sys.stdin.read().strip().split() # 解析逻辑写在这里 pass if __name__ __main__: solve()这样至少不会在输入解析上浪费大量时间。如果你用的是C建议读题后用cin 配合while(cin n)一样能处理未知组数的情况。5.2 边界条件自查清单每一道编程题在提交前都应该过一遍下面的自查清单空输入字符串为空、数组长度为0你的代码会崩溃吗单元素输入只有一个字符/一个数字逻辑还成立吗极值输入数字是INT_MAX或INT_MIN有没有溢出风险Python不用考虑但C/Java必须考虑。重复输入所有元素都相同排序/去重/计数逻辑还对吗不合法输入题目规定了输入范围但你的代码会不会因为用户输入非法值而抛出异常我统计过自己在笔试中的WA原因至少有40%出在边界条件上而不是核心算法上。也就是说如果你能严格走一遍自查清单正确率至少能提升四成。5.3 时间复杂度提前判断能不能过笔试判题机一般给的时间限制是1到2秒。Python本身跑得慢如果你写的是O(n²)的算法n超过10^4就可能超时。快速估算方式是这样的n 10^3O(n²)没问题n 10^5需要O(n log n)或更优n 10^7必须是O(n)或近似O(n)n 10^8基本只能靠O(log n)或O(1)拿到题目先看一眼数据范围再决定算法这是最基本的应试素养。很多人不是因为不会做而超时而是因为“觉得暴力能过”就懒得优化结果被大样例打脸。5.4 多留一个“暴力对拍”的保险前面提到过对拍器我再细说一次操作方式。笔试时如果没有额外时间限制你可以写一个非常暴力的解法专门用来验证贪心/DP解法的正确性。比如暴力枚举所有排列或者暴力DFS搜索所有状态数据规模小的时候暴力解和你的优化解应该输出一致。这一技巧在LeetCode上用处不大但在牛客笔试里非常有价值因为判题系统不会告诉你具体哪个样例错了你只能自己猜。对拍能帮你在提交前几秒内发现低级错误避免白白丢分。6. 备考复盘从真题反推复习重点最后我想聊一聊从这批2019秋招题集里我们能反推出哪些对现在仍有价值的复习方向。6.1 刷题优先级建议如果你现在时间有限比如只有两周就要笔试我建议按照下面的优先级来安排字符串操作与处理翻转、截取、替换、括号匹配、子串统计这些题代码量不大但边界极多性价比最高。一维动态规划爬楼梯、跳石板、打家劫舍这类线性DP状态定义清晰容易举一反三。排序 贪心至少要把“区间调度”及其变体吃透这一块思路一旦通了很多题都能套。数据结构模板栈、队列、哈希表、堆的基本操作要能默写尤其是手写堆排序和手写单调栈。图论基础如果时间充裕复习一下BFS/DFS和最短路径B站偶尔会出一两道中等难度的图论题。优先级最低的是竞赛级高级数据结构比如线段树、平衡树、后缀数组这些在B站2019秋招里几乎没有出现在后续年份也不常见不建议投入过多时间。6.2 我自己的复盘心得我复盘那批题目有一个很强烈的感受B站笔试的筛选目标不是“找出算法竞赛选手”而是“找出代码功底扎实、思维缜密、能把业务场景抽象成算法模型的候选人”。所以它的题总是带一点业务色彩比如弹幕、播放器、评论区、推荐流本质上是在模拟你入职后要做的事情。那我个人的建议就是刷题时不要只看AC率每做一道题多问自己一句“如果这个字符串换成用户昵称这个数组换成视频观看时长这道题在真实业务里对应什么场景”这种思维方式在笔试后的面试环节也非常有用因为面试官会追问“你还有没有优化空间”“如果数据分布变化了你的解法还成立吗”这些问题本质上考察的就是抽象能力和边界意识。从备考策略上讲我觉得最有效的提升方式是“限时训练 错题复盘”。每次给自己分配90分钟严格按照笔试时间做一整套题做完后无论分数多少逐题检查是“思路错了”还是“边界漏了”还是“输入输出格式不对”分类记录下来。坚持做五套以上你的实战稳定性会明显好于那些只刷题不模拟的人。还有一个小技巧提交之前养成把代码里的调试输出全部删掉的习惯。我自己就栽过好几次写代码时用print输出中间变量结果提交前忘删白白拿了几个“格式错误”。这个低级错误只要养成本能反应就能完全避免。那次的题集放在现在看难度不算高但它的出题思路和考察维度对准备任何一家互联网公司的秋招笔试都有参考价值。做一遍真题比刷十道同类题库里的题更能让你摸清楚笔试的真实脉搏。