2023年OPPO秋招算法岗笔试
每年八月底到九月初,OPPO的秋招笔试一发出来,朋友圈就有人开始刷屏问“算法岗笔试到底考什么”“准备了两周够不够”。正好我刚刷完他们的2023届秋招算法岗笔试,趁着记忆还热乎,把这套笔试的题型分布、考察思路、以及我在答题过程中的具体踩坑记录整理出来。不管你是明年准备投递,还是正在备战其他厂商的算法岗,这套笔试题的含金量都值得好好拆一拆。
这套笔试整体给我的第一感觉是:不偏不怪,但覆盖面非常广,不像某些厂动不动就上压轴难题,OPPO更多是在考察你的算法基础和工程落地意识。选择题涉及数据结构、机器学习基础、深度学习常识,编程题则偏向经典算法和中等难度偏上的思维题,整体时间比较紧张。适合准备时间较短、希望快速了解国内大厂算法岗笔试风格的同学们参考。
1. OPPO算法岗笔试到底在考什么?先吃透整体布局
1.1 笔试的题型分布与基本规则
先把最基础的规则说清楚。2023届OPPO秋招算法岗笔试在牛客网上进行,总时长大概90分钟,题型是“选择题 + 编程题”的组合。选择题部分大概有20到25道,覆盖范围从C++语法、数据结构、操作系统,到机器学习基础、深度学习网络结构、概率统计,甚至还有一两道图像处理相关的题目。编程题一般是2到3道,难度从LeetCode中等题到困难题不等。
这里有个容易忽略的点:OPPO笔试的选择题并不是单选,很多是多选题,少选得部分分,选错扣部分分,这种规则意味着你不能蒙,蒙错反而倒扣。我当时就因为在多选题上太贪,有一道题硬是选了一个不太确定的选项,结果倒扣了2分,这个教训后面细说。
整个笔试的通过逻辑不是按排名卡比例,而是看你是否达到一个基本线。往年经验来看,选择题正确率要尽量保持在75%以上,编程题至少要AC一道,才有可能进入后续的面试环节。如果编程题全部白卷,基本直接挂,哪怕选择题满分也没用。
1.2 考察重点:数据结构和算法题占了半壁江山
从考点分布来看,数据结构与算法占比大概是50%到60%,集中在排序、二分查找、贪心、动态规划、字符串匹配、二叉树遍历这几个高频考点上,和热词中频繁出现的KMP算法、排序算法、堆排序、Dijkstra等高度吻合。机器学习与深度学习的基础题占比大概20%到25%,剩下的就是C++语法、概率统计、图像处理等杂项。
再说个有意思的现象,选择题里竟然出现了“粒子群算法原理”相关的选项,还有“模拟退火算法”的适用场景判断,这说明OPPO对智能优化算法有一定关注,毕竟他们的业务涉及影像算法、通信算法等多个方向,招算法岗不仅仅是招刷题高手,也需要了解优化算法实际应用场景的候选人。
至于编程题,我拿到的是三道题,一道和字符串处理有关,一道是数组上的动态规划,还有一道是图论相关的最短路径变体。整体难度不算离谱,但时间紧的时候很容易在第二道题上卡住,这时候就考验你的取舍能力了。
1.3 90分钟时间如何分配最合理
90分钟做完20多道选择题加3道编程题,时间非常紧。我自己的分配方案是:选择题控制在35分钟内,剩下55分钟全部留给编程题。选择题不能纠结太久,一道题如果超过1分半还没头绪,先标记一下,跳过去,等编程题做完如果还有剩余时间再回头仔细想。
编程题的部分,我给自己的底线是第一道简单题必须在15分钟内AC,第二道中等题花20到25分钟,第三道难题如果做完前两道还有时间就尝试写部分分或暴力解。实际考试的时候,我第一道字符串题比较顺利,14分钟就AC了,结果第二道动态规划题卡了近30分钟,最后只过了一半的测试用例,第三道图论题只写了个基础版本的Dijkstra,过了30%的用例,整体算是中等偏上的结果。
这里有个建议:考试前一定要用牛客网的模拟环境练几次,尤其是要适应他们那个“提交后才知道过没过”的模式,和本地IDE调试完全是两种体验。我平时习惯在VSCode里跑通再贴过去,结果第一道题因为输入输出格式不对,白白浪费了6分钟,这个坑你们一定要提前规避。
2. 高频考点逐个拆解,这些题真的会反复出现
2.1 字符串算法:KMP和next数组是必须拿下的基础分
热词里提到的“在KMP算法中,对于模式串p='abacaba',其next数组”这类题目,在OPPO选择题里居然真的出现了,只是它问的是next数组某一位的值。KMP算法在前几年秋招中出现的频率很高,最近有所下降,但OPPO似乎仍然偏爱,所以你要是准备投OPPO,KMP的原理和next数组的手推过程必须熟练。
KMP的核心思想是当模式串与文本串匹配失败时,不从头开始匹配,而是利用已经匹配的前缀信息,将模式串向右滑动尽可能远的距离。next数组的求解是整个算法的灵魂,next[i]表示的是模式串前i个字符组成的子串中,最长的相同前缀后缀的长度。以“abacaba”为例,手推一下:next[1]=0,next[2]=0,next[3]=1(因为前缀a和后缀a相同),next[4]=1(abac的前后缀公共部分还是a),next[5]=2(abaca中前缀ab = 后缀ca?不对,是前缀ab和后缀ca中的a相同……这里要特别小心,很多人在这里算错),next[6]=3(abacab中前缀aba = 后缀cab?这里其实是前缀aba和后缀bac……不对,应该是前缀ab = 后缀ab,长度2……),我的建议是到考场前把几个经典模式串的next数组自己推一遍,不要只背代码。
我当时在这道选择题上没有手推完整数组,而是根据next数组的定义直接写代码手算的,浪费了不少时间。如果用“前缀后缀最长公共长度”的表格法,30秒就能出结果,这是一种非常实用的考场技巧。
2.2 排序算法:从原理到复杂度,稳定性和适用场景都是考点
排序算法几乎是国内所有算法岗笔试的必考内容,OPPO也不例外。选择题里直接出现了“冒泡排序算法C++实现”的代码填空、“堆排序算法”的时间复杂度与稳定性判断、“快速排序最坏时间复杂度”等题目。这些内容本身不难,但考察角度很多变,记忆容错率很低。
我整理了一个考点速查表,建议考前反复默写:
| 排序算法 | 平均时间复杂度 | 最坏时间复杂度 | 空间复杂度 | 稳定性 |
|---|---|---|---|---|
| 冒泡排序 | 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(1) | 不稳定 |
| 归并排序 | O(n log n) | O(n log n) | O(n) | 稳定 |
| 插入排序 | O(n^2) | O(n^2) | O(1) | 稳定 |
| 选择排序 | O(n^2) | O(n^2) | O(1) | 不稳定 |
这个表看着简单,但多选题经常考“以下哪些排序算法是稳定的”或者“以下哪些排序算法的最坏时间复杂度和平均相同”,每次都有不少人因为把堆排序和归并排序的空间复杂度记混而丢分。另外,OPPO还特别喜欢结合场景考察排序算法的选择,比如“数据量很大但数值范围有限,应该选择什么排序”,这种题答案是计数排序或基数排序,如果你只背了八大排序,很容易漏掉这两个非基于比较的排序算法。
2.3 机器学习与深度学习基础:选择题里暗藏杀机
OPPO算法岗笔试的机器学习相关题目,考查的不是深度学习框架的使用,而是基本概念和原理推导。我印象比较深的有几道题:一道是问“KNN算法的应用能力包括哪三个方面”,这类题需要你对KNN的分类、回归、异常检测能力有全面认知;另外一道是“K-L散度(KL Divergence)与ELBO的关系”,还有一道是关于“BM25算法”的适用场景。
这些题目对非科班选手来说可能有一定难度,因为BM25和ELBO属于信息检索和变分推断方向的内容,在一般的机器学习课程中不会重点讲。但如果你准备的是算法岗,这些知识都属于“可能考到”的范畴,建议重点关注信息检索中的BM25算法原理、概率图模型中的变分推断、以及传统机器学习算法的适用场景。我在备考时总结了几个高频命题方向,后面专门写一篇文章细说,这里先提一句:OPPO的算法岗既然分了影像、通信、数据等多个方向,笔试中涉及的多选题往往会有技术方向上的区分,你只需要选择自己最确定的选项,不要贪多。
2.4 手撕代码题的常见套路与解题模板
编程题部分,OPPO不会出特别偏门的题,考的主要是经典算法和数据结构的组合应用。常见的套路有:前缀和与差分、双指针、滑动窗口、动态规划的状态压缩、Dijkstra及堆优化、并查集、DFS与回溯。这些算法和热词中的“贪心算法”“Dijkstra算法”“快速幂算法”“KMP算法”等高度重合,所以备考时应当以这些经典算法为主,反复练习,直到不需要思考就能写出核心代码。
比如快速幂算法,OPPO的笔试题里虽然没直接考,但在计算组合数取模、求解幂运算相关的大数问题时会用到。它的核心思想是通过二进制分解指数,将O(n)的幂运算降到O(log n)。在C++里用递归或循环都能实现,但循环版本的常数更小,更推荐在笔试环境中使用。
再看动态规划题,它的难点不在状态转移方程,而在于如何定义状态。我在这套笔试的第二题就吃了大亏:题目是“给定一个数组,可以任意选择一个子区间乘以-1,问最终数组的最大连续子数组和”,我一开始按照常规的“最大子段和”去写,发现怎么都不对,后来才意识到需要维护两个状态,一个是不翻转的最大值,一个是翻转后的最大值。这就是典型的状态定义题,需要你在考试现场的紧张状态下快速转变思路,平时要多积累这类“状态扩展”的题目。
3. 实战环节:从读题到提交的完整思路
3.1 拿到编程题之后的三个关键动作:读题、估算复杂度、确定数据结构
每一次编程题,我的流程都是固定的:先用3分钟把题目通读两遍,第一遍看懂题意,第二遍圈出数据范围,然后立刻估算时间复杂度。比如数据范围n <= 10^5,那O(n^2)的算法肯定过不了,必须往O(n log n)或O(n)方向想;如果是n <= 20,那可以考虑状态压缩或暴搜。
数据范围是做题最关键的线索,很多人忽略了这一点,看到一个题就直接套记忆里的模板,最后不是超时就是爆内存。OPPO的编程题数据范围给得相对友好,一般不会玩“卡常数”这种恶心操作,但数据范围仍然会直接影响你的算法选择。比如第一道字符串题,n <= 10^5,意味着O(n^2)的暴力必然超时,需要使用KMP或字符串哈希;第二道动态规划题,n <= 10^5,要使用O(n)或O(n log n)的做法,我当时想到了O(n^2)的暴力DP,看到数据范围后立刻否决,卡了很久才想到状态扩展的方法;第三道图论题,n <= 10^4,m <= 10^5,Dijkstra的堆优化版本是标准解。
确定算法后,再想数据结构:需要快速查询区间极值,用线段树或ST表;需要维护连通性,用并查集;需要快速查找,用哈希表或平衡树;需要处理优先级,用堆。这一步想清楚后,代码的骨架基本就出来了,剩下的就是写代码、调细节。
3.2 用一道“最大连续子数组和变体”完整走一遍思路
我拿第二道“翻转区间求最大子段和”来做个完整复盘。题目的原始描述大致是:给定一个长度为n的整数数组a,你可以选择任意一个连续子数组,将这个子数组的所有元素乘以-1,求最终数组的最大连续子数组和。
看到“最大连续子数组和”第一反应是Kadane算法:用一个变量cur记录以当前元素结尾的最大值,一个变量ans记录全局最大值。但这道题加了一个“翻转区间”的操作,等于把一个连续区间变成负数,这时候答案的形态就复杂了:可能是原数组的最大子段和,也可能是一段未翻转的正常段 + 一段被翻转的负数段(翻转后变成正数)+ 一段正常段。
正确的状态定义是:dp[i][0]表示到第i个元素为止,当前处于“没有翻转过”的状态下的最大子数组和;dp[i][1]表示当前正在翻转子数组中的最大子数组和;dp[i][2]表示翻转已经结束,后续是正常状态的最大子数组和。状态转移看图会更清晰,但文字描述就是:
- dp[i][0] = max(a[i], dp[i-1][0] + a[i]),要么从当前元素重新开始,要么接在之前未翻转的段后面。
- dp[i][1] = max(-a[i], dp[i-1][0] - a[i], dp[i-1][1] - a[i]),表示当前元素被翻转,可以是从这个元素开始翻转,也可以接着之前的翻转段。注意这里dp[i-1][1] - a[i]表示翻转段持续。
- dp[i][2] = max(a[i], dp[i-1][1] + a[i], dp[i-1][2] + a[i]),表示翻转结束,当前元素恢复正常,可以接在翻转段之后,也可以接在之前已经恢复正常状态的段之后。
最终答案是max(dp[n-1][0], dp[n-1][1], dp[n-1][2])。这个三维状态的DP,其实理解起来并不复杂,但如果你只在考场上见过“普通版的最大子段和”,在紧张状态下很容易想不到。我建议备考时把Kadane算法的各种变体都过一遍,尤其是“允许翻转一次区间求最大子段和”“允许删除一个元素求最大子段和”这类题,它们都是热点。
3.3 在线IDE与本地环境的差异,提交前要检查的细节
OPPO笔试用的是牛客网在线IDE,它和本地VSCode/CLion的差异比你想象的大得多。第一是编译标准,默认一般是C++14或C++17,但你不要赌C++17的特性,最好只用C++11也支持的特性,比如auto、unordered_map、lambda表达式这些没问题,但结构化绑定、if constexpr这类高级特性就别用了,容易编译失败。
第二是输入输出格式,牛客网不像力扣那样把数据封装成参数,而是需要你自己写cin/cout或scanf/printf。笔试过程中最常见的错误就是多读了一行或者少读了一行,导致提交后AC率直接是0。我的习惯是:先写一个本地测试样例,跑通后提交一次,如果编译或格式错误,再检查输入输出,而不是急着改算法逻辑。
第三是内存限制,OPPO笔试的编程题内存限制一般是256MB,使用vector 存储10^5的数据完全没问题,但如果需要开大数组或二维DP,就要注意空间复杂度了。比如二维DP开dp[1000][1000]还好,如果开dp[5000][5000],每个int占4字节,那就是100MB,接近内存限制。遇到这种情况,要么用滚动数组,要么用short替代int,要么重新思考状态定义。
4. 备考阶段怎么准备最有效,别再盲目刷题了
4.1 刷题范围和优先级:基础不牢,刷一万道也没用
我见过太多同学备考秋招算法岗,一上来就是LeetCode困难题狂刷,刷了200道,结果到了笔试现场,发现最基础的二分查找都能写错。这不是个例,而是因为很多人的“刷题”只是在看题解、复制粘贴,而不是独立思考。OPPO的笔试题强调基础,所以备考时优先级应该这样排:
第一优先级:数据结构基础。数组、链表、栈、队列、哈希表、二叉树、堆、并查集、字典树,这些数据结构的原理、适用场景和代码实现都要非常熟练。尤其是二叉树的前中后序遍历(递归和迭代两种写法),以及堆的建堆、插入、删除操作,几乎是每一场笔试的“前菜”。
第二优先级:经典算法。包括排序算法(至少能手写快排、归并、堆排)、二分查找(注意边界条件和整型溢出)、贪心算法(能证明或至少能感知到是否正确)、动态规划(背包、最长递增子序列、最长公共子序列、区间DP、状态压缩DP)、图算法(DFS、BFS、拓扑排序、Dijkstra、并查集)。这些算法的代码模板要刻在脑子里,考场上是没有时间现场推导的。
第三优先级:字符串算法。KMP、字符串哈希、Trie树。虽然字符串题在国内大厂笔试中出题频率不如动态规划高,但OPPO这几年的出题风格里字符串一直有一席之地,所以不能掉以轻心。
第四优先级:数学与计算几何。快速幂、大数取模、GCD与LCM、素数筛、组合数取模。这些不是每次笔试都会考,但一旦考到就是“送命题”和“送分题”的分水岭,掌握它们能让你在笔试中多拿一两道题的分数。
4.2 复习笔记怎么做?用“一题三问”复盘法打败遗忘曲线
很多同学刷题有个坏习惯:刷完一道题,AC了,就直接过,过两天再遇到同样的题,还是不会做。针对这个问题,我推荐“一题三问”复盘法:每做完一道有价值的题,在笔记上写下三个问题的答案——这道题考察的是什么核心知识点?我最初的思路错在哪里?如果我下次遇到同类题,第一步应该做什么?
比如我在复盘“翻转区间求最大子段和”这道题时,写下的三问答案是:核心知识点是动态规划中的状态扩展;最初思路错在只考虑了一个状态,没有考虑“当前是否处于翻转区间内”这个维度;下次遇到类似“可以做一次特殊操作”的题,第一反应是给DP增加状态维度。这种复盘方式让我在一道题上的收获远超盲目刷十道题。
同时要建立一个“错题集”,把笔试或刷题中做错的题按知识点分类,定期回头重做。我第一次做KMP题目时,next数组求解写错了,错题本上记录了一次,一个月后再做类似的题,果然又快又准。学习算法最忌讳的就是“狗熊掰棒子”,刷一道忘一道,到最后发现自己还是只会做最简单的那几道题。
4.3 笔试前的最后三天:回归基础比疯狂刷难题更重要
笔试前的最后三天,我个人的建议是不要再碰新题难題了,而是把精力放在三个方面:一是把已经做过的经典题重新手写一遍,不是看题解,而是从零开始AC一遍,确保代码形成肌肉记忆;二是把所有常用算法的复杂度、稳定性和适用场景默写一遍,这是应对选择题高性价比的方式;三是把C++ STL的常用容器和算法函数过一遍,包括vector、map、set、unordered_map、priority_queue、sort、lower_bound、unique等。
用三天时间来“减负”而不是“增量”,这是我多次笔试后的血泪经验。以前我总想在最后几天多刷点难题,结果上了考场,大脑一片空白,连Kadane算法都差点写错。后来调整策略,最后三天只看笔记和错题集,反而笔试时手脚更麻利。
5. 常见问题与避坑经验,这些坑每一个都是我踩过的
5.1 选择题常见的五个失分点
失分点一:多选题贪多。前文说了OPPO多选题选错会倒扣分,所以不确定的选项一定不选。有些同学总觉得“多选几个,总能蒙对”,但规则是选错扣分的,蒙错的代价远大于少选得分,这个账一定要算清楚。
失分点二:时间复杂度记忆混淆。最典型的是堆排序和归并排序的空间复杂度,堆排序是O(1),归并排序是O(n),很多人记反了。还有快速排序的最坏时间复杂度是O(n^2),但平均是O(n log n),考试喜欢考“最坏”。建议自己画一张表,考前半小时快速过一遍。
失分点三:稳定性判断错误。常见不稳定排序是快选堆希(快速、选择、堆、希尔),稳定排序是插冒归基(插入、冒泡、归并、基数),这个口诀很管用,但做题时还是要现场验证一下,特别是选择排序为什么不稳定,有的人理解了就不会错。
失分点四:KMP的next数组计算错误。这一块尤其要注意next数组的定义,有些教材定义next[i]是“前i个字符组成的子串的最长相同前缀后缀长度”,有些定义为“最长相同前缀后缀长度减一”,不同定义下数组内容不一样,做题前先看题目给的定义,别套错模板。
失分点五:机器学习概念的细节。比如KNN是惰性学习算法,训练阶段不做任何事,所有的计算都发生在预测阶段。这个知识点在很多选择题里出现,但很多平时用sklearn的同学根本没注意过。还有KNN的三个应用能力:分类、回归、异常检测,这也常被拿来出题。如果你不熟悉这些概念,考前最好把常见机器学习算法的特点整理一遍。
5.2 编程题写不完怎么办?学会“拿部分分”才是聪明人
编程题三道,很多人到交卷时第三题一道都没写,或者第二题只过了一半。这时候最忌讳的是死磕一道题,导致后面的题目连暴力的分都没拿到。我的策略是:先写所有题目的暴力解,拿保底分,再逐步优化。具体来说,先花5分钟写出每道题的暴力解法,不管时间复杂度多高,只要能过样例就行,然后把暴力解提交上去,一般能过20%到30%的测试用例,最后再回头优化关键题目。
这样做的逻辑很简单:在90分钟的笔试里,满分是奢望,拿到尽可能多的题目才是最现实的目标。暴力解虽然不优雅,但能给你带来保底分,而保底分往往就是决定你是否能进入面试的关键。如果你在第二道题上花了40分钟写了个只过了一半用例的优化解,然后第三道题完全空白,你的总分很可能不如“第二题暴力拿30% + 第三题暴力拿30%”的人,这就是典型的投入产出比失衡。
5.3 笔试之后如何高效复盘,把面试主动权抓回手里
笔试结束不等于万事大吉。很多人在笔试后完全不复盘,结果到了面试环节被问到笔试题思路时一脸懵,直接暴露了笔试“水分”。我的建议是:无论笔试成绩如何,都要在结束后48小时内把三道编程题重新做一遍,用最优解AC,并整理出完整的解题思路。笔试中那些没做出来的题,更是面试时的高频问题,面试官可能会直接问“你为什么当时没想出来”“现在会做了吗?讲讲思路”。
复盘时特别注意记录当时的思维卡点,比如我这次笔试的第二题,卡点是没有想到用三个状态表示翻转状态。面试前把这个卡点重新梳理一遍,如果面试官问到,我就能完整地说出“我当时的思路是普通Kadane,但后来意识到需要状态扩展”,这比支支吾吾说“我回去后看了题解”要专业得多。
同时,笔试结束后还可以去牛客网讨论区看看其他同学对这套题的看法。有时候你会发现某道题其实还有更巧妙的解法,或者某道选择题的争议很大——如果这道题恰好你选了另一种答案,可以记下来,在面试时主动提出来“这道题我认为有两种理解方式”,展示自己的思辨能力,这在面试中是很加分的。
5.4 一个值得借鉴的备赛时间线参考
分享一下我当时从零开始准备到笔试的具体时间线,给大家一个参考。提前8周开始,前两周打基础,主要任务是过完所有基础数据结构和经典算法,这个过程不用刷太多题,重点是理解和手写代码;中间三周集中刷题,每天保证3到5道高质量题目,刷题范围以LeetCode的Hot 100和剑指Offer为主,同时整理错题集和“一题三问”笔记;最后三周进入冲刺阶段,每天上午按照笔试时间做一套模拟题(牛客网的历年真题优先),下午复盘错题和笔记,晚上手写代码模板。
这个时间线的好处在于:不同阶段的目标非常明确,不会出现“刷了一段时间后不知道干什么”的迷茫。如果你只剩两周,就把基础阶段压缩到5天,刷题阶段压缩到9天,冲刺阶段不变。如果你只剩一周,那就只看笔记、错题集和经典题模板,不要再刷新题了,把已有的东西巩固好,作用远大于去碰一堆做不出来的难题。
最后说一个我个人的体会:OPPO算法岗笔试并不靠炫技,它更像一次全面体检,考察你是否具备一个算法工程师应有的基本功。真正能拉开差距的,往往不是会不会做压轴难题,而是在基础题上的稳定输出和时间分配。平时多花点时间把KMP、堆排序这些“基本功”练到肌肉记忆,比考前临时抱佛脚刷难题有用得多。希望这份还原和分析能帮到准备秋招的你,也祝大家在笔试场上都能稳住心态,把该拿的分都拿到。