ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

拼多多2018校招笔试题复盘:大整数乘法、贪心、模拟与字典序全解析

拼多多2018校招笔试题复盘:大整数乘法、贪心、模拟与字典序全解析 拼多多的校招笔试题一直以来在互联网圈子里都有点“传说”色彩。2018年内推那批题难度和区分度都做得相当好既不像普通校招卷那样随便刷刷LeetCode热题就能过也不像竞赛题那样脱离实际。它考的是很本质的算法功底、代码实现能力和边界条件的敏感度。这套题我反复刷过几遍每次都有新收获。这篇文章就结合我自己刷题时的踩坑经验做一次完整的复盘和拆解。这套题适合谁看如果你是正在备战大厂校招的研发岗同学建议逐题精做因为你很大概率在笔试中遇到同级别的题如果是工作两三年的开发想系统回顾一下算法基本功这五道题也是很好的自测标尺。1. 整体解题思路与这套题的底层逻辑1.1 拼多多笔试的出题风格先说一个很核心的感受拼多多的笔试题不绕弯子就是直白地考你的基础功底。不像一些公司喜欢出场景题、智力题、甚至阅读理解题拼多多的题风是相当“硬核”的——大整数乘法、组合数学、贪心模拟、树形结构。题目描述都不长但每一题的考察点都踩在算法和数据结构的关键位置上。从2018年内推这套题来看出题人的逻辑非常清晰基础的数据结构要熟练尤其是模拟题要写得又快又准基础的算法思维要扎实不要一上来就想复杂的优化有时候最简单的思路反而是最优解边界条件处理能力是关键在笔试环境里没有调试器一旦边界出错就是挂。1.2 五道题的类型拆解与能力覆盖把五道题放在一起看其实就是一个完整的算法能力测试矩阵题目考察方向难度评级核心数据结构/算法大整数乘法高精度运算中低数组模拟、进位处理数三角形组合数学中GCD、去重逻辑最大乘积贪心/扫描中极值维护、负负得正小熊吃糖模拟题中高多维排序、贪心选择选靓号状态枚举高枚举、成本计算、字典序调整前两题是基础中的基础属于保障分中间两题开始拉差距需要你真正理解贪心和模拟的精髓最后一题是压轴题它考验的是你在复杂度压力下能否找到正确的枚举状态并处理字符串的字典序问题。1.3 为什么这套题值得反复刷我的观点是刷算法题刷的不是题是解题的思维惯性。这套题里几乎没有偏题怪题每一道都是在训练你面对一个“看着不难但写起来容易出错”的任务时如何理清思路、一次通过。这种能力在真实的开发工作里非常重要——你写的每一段业务逻辑本质上都是一个需要处理各种边界的“模拟题”。2. 高精度不犯错的底层技巧大整数乘法详解2.1 题目描述与本质分析这道题的原题是这样的给定两个非常大的正整数它们的长度可能达到几千位甚至更多要求输出它们的乘积。很多同学第一次看到这道题心里想的是“用Python不就直接乘吗”或者“用Java的BigInteger啊”。这些思路没有错但注意笔试出这道题的目的恰恰是考察你在语言没有大整数支持时的处理能力。在C里没有内置的大整数类型在Java里虽然有BigInteger但如果你直接用某种程度上等于放弃了这道题考察的核心——高精度运算本身。在实际工作中高精度计算也经常出现在金融系统、加密算法、科学计算等场景里。2.2 竖式模拟法的完整推导高精度乘法的核心思想就是把我们在小学数学里学的竖式乘法用数组和循环来实现。假设第一个数的每一位是a[0]、a[1]、a[2]...从低位到高位存储第二个数同理。那么结果数组的第ij位需要累加a[i] * b[j]的结果。这是关键推导def multiply(num1, num2): # 处理特殊情况 if num1 0 or num2 0: return 0 m, n len(num1), len(num2) # 两个数相乘结果的位数不会超过mn位 result [0] * (m n) # 从低位到高位逐位相乘 for i in range(m - 1, -1, -1): for j in range(n - 1, -1, -1): mul (ord(num1[i]) - ord(0)) * (ord(num2[j]) - ord(0)) # 关键累加到对应位置 # ij1对应当前位的“个位”位置 p1, p2 i j, i j 1 total mul result[p2] result[p2] total % 10 result[p1] total // 10 # 去掉前导零 res .join(map(str, result)) return res.lstrip(0)2.3 为什么结果数组长度是mn这里有个数学小结论一个m位数和一个n位数相乘结果的位数要么是mn-1要么是mn。举例来说99两位数乘以99两位数等于9801四位数是mn而10两位数乘以10两位数等于100三位数是mn-1。所以开mn长度的数组一定够用最后去掉前导零就行。我当年写这道题时犯过的一个低级错误是直接在result[p2]位置存放乘积结果而不是累加。这样第一轮循环没问题第二轮的时候就覆盖掉了之前的结果。正确做法是先把mul和result[p2]当前的临时值相加再拆分个位和进位。2.4 这道题的三个易错点字符转数字时一定要减去字符0而不是直接拿字符的ASCII码去算。ASCII码的48到57对应数字0到9直接取值会得到完全错误的结果。处理乘数为0的情况。很多实现里如果不特判0最后的结果会是一串0而不是一个0。虽然lstrip(0)能处理前导零但如果所有位都是0它会把整个字符串变成空串。这就是我前面代码里先判断乘数是否为0的原因。存储顺序的选择。我习惯从低位到高位存储这样进位的时候是往高一位进位符合自然思维。但如果你从高位到低位存储进位方向就反了会在取模和整除那里折腾很久。选一种你习惯的、不会搞混的顺序一以贯之。注意高精度加法、乘法、减法在思路上一脉相承都是“每一位单独运算然后处理进位/借位”。把大整数乘法的模板背熟遇到大数相关题目就能快速套用。3. 最大乘积与去重组合的思路对比3.1 最大乘积的贪心思维这道题的描述是给定一个整数数组长度至少为3从中选出三个数使得它们的乘积最大输出这个最大乘积。注意数组里的整数可能是负数也可能是0。很多第一次看到这道题的同学第一反应是“排序然后取最后三个数相乘”。这个思路在全是正数的情况下没问题但一旦出现负数和0就完全失效了。举个例子数组是[-100, -98, 1, 2, 3]排序后最后三个数是1、2、3乘积是6。但正确答案应该是(-100)乘以(-98)乘以3等于29400因为两个负数相乘得到很大的正数。所以要分情况讨论最大乘积只有两种可能最大的三个正数相乘或者都是负数时最大的三个数相乘比如-1、-2、-3的乘积是-6比-1、-2、-100的乘积-200要大最小的两个数和最大的那个数相乘这个结论背后是一个很简单的数学事实乘积要最大要么全取正数里最大的要么取两个绝对值最大的负数来“变正”。写代码时一种做法是排序后比较nums[n-1] * nums[n-2] * nums[n-3]和nums[0] * nums[1] * nums[n-1]取较大值。def maximum_product(nums): nums.sort() n len(nums) return max(nums[0] * nums[1] * nums[-1], nums[-1] * nums[-2] * nums[-3])就这么几行代码但背后是清晰的贪心分类讨论。我自己的体会是排序法简单直接时间复杂度是O(nlogn)在笔试完全够用。如果你追求极致性能可以用线性扫描维护三个最大值和两个最小值时间复杂度降到O(n)但代码量和出错概率都会增加。3.2 数三角形的去重与共线检测数三角形这道题描述是这样的给定平面上若干个点问能组成多少个不同的三角形。这道题第一眼看去很容易想到暴力枚举任取三个点判断是否共线不共线就是三角形。但这里有两个坑计算量。如果点是100个C(100,3)是161700种组合这个数量级暴力枚举是能过的。但如果点是1000个呢C(1000,3)约等于1.6亿暴力枚举就非常吃力了。共线判断的精度问题。如果用斜率判断共线在浮点数计算时可能会遇到精度问题。三个点共线的本质是向量叉积为零。也就是说点A、B、C共线等价于(B.x - A.x)乘以(C.y - A.y)减去(B.y - A.y)乘以(C.x - A.x)等于0。用整数运算就能完全避开浮点误差。def count_triangles(points): n len(points) count 0 for i in range(n): for j in range(i 1, n): for k in range(j 1, n): x1, y1 points[i] x2, y2 points[j] x3, y3 points[k] # 向量叉积为零则共线 cross (x2 - x1) * (y3 - y1) - (y2 - y1) * (x3 - x1) if cross ! 0: count 1 return count3.3 大数量级时的优化GCD去重如果点的数量达到几千甚至上万三重循环就废了。这时候需要换一个思路枚举每个点作为三角形的一个顶点然后计算它到其他所有点的向量按方向去重。这个思路的数学基础是从点A出发如果有m个点与A的连线方向相同那么从这m个点中任选两个与A组成的三角形是不成立的共线所以以A为顶点的不共线三点组合总数是C(n-1, 2)减去所有共线方向上的C(m, 2)。方向去重时需要把向量(x, y)化简为最简分数形式这里就用到了最大公约数GCD。比如(2, 4)和(1, 2)化简后都是(1, 2)代表同一个方向。关键点在于向量要统一正负号标准比如规定x为负时整体取反x为0时规定y为正。否则(1, -2)和(-1, 2)会被当作两个方向但实际上它们在同一条直线上。这个题的核心就是去重和共线检测看起来是几何题考的是组合数学和数论。想要精刷校招题库的朋友这道题值得多花点时间。4. 小熊吃糖的模拟题优化4.1 初看简单的表象题目描述大致是有若干只小熊它们的战斗力不同饥饿值也不同。现在有若干颗糖每颗糖有对应的饱腹度。每只小熊按照战斗力从高到低的顺序依次选择糖果——选择目前剩余的、能让它吃饱且不超过它饥饿值的最大一颗糖。如果吃完一颗糖后还是饿可以继续选下一颗直到吃饱或糖果被选完。最后输出每只小熊剩余饥饿值。这题第一眼看过去不就是模拟嘛排序就行了。但实际上手后会发现情况比想象中复杂因为每只小熊可以吃多颗糖。4.2 双排序的解题框架我的做法分三步走第一步把小熊按战斗力降序排列同时记录它们原来的索引因为输出时要按输入顺序输出。第二步把所有糖的饱腹度排序。第三步按战斗力顺序遍历小熊对于每只小熊从大到小遍历剩余的糖只要这颗糖的饱腹度小于等于它的剩余饥饿值就吃掉它同时减少饥饿值。def bears_and_candies(bears, candies): # bears: [战斗力, 饥饿值] # candies: [饱腹度] n len(bears) # 记录原始索引 bears [(bears[i][0], bears[i][1], i) for i in range(n)] # 按战斗力降序排列 bears.sort(keylambda x: x[0], reverseTrue) candies.sort(reverseTrue) result [0] * n used [False] * len(candies) for power, hunger, idx in bears: remaining hunger for i in range(len(candies)): if used[i]: continue if candies[i] remaining: remaining - candies[i] used[i] True # 零饥饿提前退出 if remaining 0: break result[idx] remaining return result4.3 复杂度分析与优化方向最坏情况下每只小熊都要遍历所有糖果复杂度是O(n*m)n是小熊数量m是糖果数量。如果n和m都达到10^4量级这个复杂度在笔试中可能会卡时间。怎么优化关键是可以利用糖果的排序特性。糖果已经按降序排列了对于一只小熊找到第一颗不超过当前饥饿值的糖可以用二分查找快速定位。如果一棵糖被吃掉了可以用并查集并查集跳过已经用过的糖果位置让查找效率接近线性。这里的贪心思路是“每次选能吃的最大糖”这保证了最优解。为什么因为对于一只固定顺序的小熊来说它消耗的饥饿值越大后续剩余饥饿越低。同样的饥饿值下先吃大糖不会损害后续选择因为在它选糖期间没有其他熊能插队。4.4 模拟题在笔试中的通用套路小熊吃糖这道题是我认为整套试卷里最“职场化”的一道题。 它的核心是在多种排序规则的场景下找到一种不会冲突的贪心策略。我做模拟题的经验是不要急着写代码先纸上推演一遍完整流程。把“输入排序”、“处理逻辑”、“输出还原”三段式结构理清楚再动手编码。这样写出来的代码基本不会出现“明明逻辑对但输出顺序错”的尴尬局面。5. 选靓号的复杂枚举与字典序编程5.1 题目描述与问题转化选靓号这道题是整套试卷的压轴题。题目大意是给定一个手机号一串数字你可以把其中任意一个数字改成任意其他数字每次修改的代价是原始数字与目标数字差的绝对值。现在最多允许修改k次要求修改后得到的号码中至少存在一个数字出现次数最多不是指定数字是至少有一位数字出现了最大次数且需要让这个号码的字典序最小。说实话我第一次看这道题的时候人是懵的。因为变量太多了改哪个位置、改成什么数字、修改次数怎么分配、字典序怎么保证最小。解题的关键是不要试图同时处理所有数字而是固定一个目标数字然后让最大化它的出现次数。5.2 核心枚举固定目标数字枚举目标数字d从0到9把原始号码的每一位数字都看作一个“候选”计算它变成d的代价。然后选择k次修改中最小的k个代价对应的位置把这些位置改成d。这个思路看起来简单但有一个极其关键的点这里有一个很反直觉的“越界修改”的问题。举例来说把8改成0代价是8这在“把0变成8”的目标里是合理的。但如果你不限制修改次数把8改成0后再把0改成8就浪费了一次修改。所以要明确每个位置最多只能被修改一次。在计算代价时只需要计算|原数字 - d|修改一次到位不要出现二次修改。5.3 处理字典序最小优先级策略固定目标数字d之后字典序最小怎么解决这个问题的难度甚至比“怎么选修改位置”更大。核心结论对于每位数字x如果它要变成d当d大于x时修改后数字变大这种情况要尽可能把修改机会放在高位当d小于x时修改后数字变小这种情况要尽可能把修改机会放在低位。为什么因为字典序是从高位向低位比较的。高位数字变小会直接让整个号码的字典序变小高位数字变大会让字典序变大。所以如果d 原数字也就是要变小尽量在低位修改这样高位保持原样字典序更小如果d 原数字也就是要变大尽量在高位修改这样高位变大后整个号码虽然变大了但如果必须变大就让它变在更低位这个优先级关系可以用一个简单的排序规则实现按代价升序排列代价相同的优先修改更靠后的位置对于d 原数字的情况。但这里有一个特例如果d 原数字反而要优先修改更靠前的位置。所以排序规则实际上是两段的。这个细节是选靓号这道题里最容易出错的地方。我当年第一次写的时候只考虑了代价排序结果样例过了但提交后大面积报错。调试了很久才发现是字典序的优先级问题。5.4 完整实现参考def beautiful_phone_number(num, k): n len(num) # 记录每个数字出现的次数 best -1 best_digit -1 for d in range(10): target str(d) # 计算每个位置变成目标数字的代价 changes [] for i, ch in enumerate(num): cost abs(int(ch) - d) changes.append((cost, i)) # 按代价排序代价相同按位置排序 # 这里的排序规则需要根据d和原数字的相对大小来定 # 简化处理先按代价排序相同代价按位置降序优先改低位 changes.sort(keylambda x: (x[0], -x[1])) cnt 0 total_cost 0 tmp list(num) for cost, pos in changes: if cnt k: break if cost 0: continue total_cost cost tmp[pos] target cnt 1 # 统计tmp中target出现的次数 freq tmp.count(target) if freq best: best freq best_digit d best_arr tmp # 这个实现做了一点简化完整版需要根据字典序精细调整排序方向 return .join(best_arr)注意这个实现是简化的逻辑完整版还需要处理代价为0的情况、修改次数不足k但已经最大化的边界情况等。这里展示的是核心枚举框架。5.5 字典序相关的通用技巧选靓号的字典序处理包含了一个很重要的通用技巧枚举优先级控制。拿到任何一道“在多种方案里选取字典序最优”的题目可以先固定方案的关键参数然后在调整过程中用一个明确的优先级来指导每一步。优先级通常来自“字典序的定义”——从高位到低位逐个比较。6. 笔试实战时间分配与自测清单6.1 考场上的时间分配建议以2018拼多多内推笔试为例通常2到3小时做3到5道题。我的建议是第一题大整数乘法属于送分题15分钟内必须写完并且确保对第二题最大乘积排序法5分钟内搞定第三题数三角形暴力法先保底如果时间充裕再优化第四题小熊吃糖30分钟到40分钟这是决定你能不能通过的分水岭第五题选靓号最后做如果前面还有bug要修果断放弃这题的优化整体原则先易后难先拿到保底分再挑战高分题。6.2 提交前的自查清单我在刷这套题时总结了一个笔试自测清单每题提交前逐条对照全0输入测了吗空数组/空字符串呢边界值测了吗比如最大值、最小值、负数、0。输出格式对吗题目要求每行一个结果还是结果用空格分隔变量类型对吗大数溢出怎么办如果有排序索引对应关系对了吗时间复杂度和空间复杂度在题目限制下能过吗特别是第一题和第五题边界情况是重灾区。写完代码后用几个极端用例在脑子里跑一遍能抓住大量隐藏bug。6.3 笔试环境与IDE的磨合还有一个很多人忽略的细节笔试平台用的是牛客网这类在线评测系统它的输入输出格式和本地环境不太一样。有的平台要求使用标准输入输出有的平台会自动读文件。如果你平时习惯本地IDE调试考前一定要先在模拟环境里做几道题熟悉打印调试print调试的习惯因为你不能像在IDE里一样设置断点和单步执行。另外代码的输入解析也是一大坑。笔试的输入通常是多行文本需要自己按行读取和拆分。C的cin和getline混用会出问题Python的input()和sys.stdin.readline()行为也不一样。我见过太多人因为输入解析卡住最后白白浪费时间的例子。7. 从校招笔试到工程能力这套题的长期价值7.1 算法题之外的东西刷完这套题除了算法本身还有一些更重要的收获。第一题大整数乘法锻炼的是“用基础数据结构实现看似简单但实际有陷阱的功能”的能力。在工作中处理订单号拼接、金额计算、优惠券叠加时你经常会遇到类似“直接相加会溢出需要自己维护精度”的问题。第三题数三角形本质上是在训练“把几何问题、组合问题转化成纯整数计算”的思维。这种能力在图形学、游戏开发、甚至CAD软件的应用中都有用武之地。7.2 边界条件的工程意义很多人觉得笔试考边界条件是“应试技巧”但我不这么认为。真实的生产环境里你写的代码要面对各种用户输入的边界情况。一个会传空字符串参数的调用方一个可能超出整型范围的金额数值一个因为时区导致的时间边界问题。每一个都是线上事故的隐患。拼多多这套题里反复出现的“负数、0、重复元素、前导0”其实就是编程基本功里最核心的几个边界类型。能把这几类边界问题处理得干净利落的人写工程代码时也会有更强的容错意识。7.3 后续可以继续扩展的方向如果你刷完这套题还有余力我建议按这几个方向继续深入高精度运算的进一步扩展大数除法、大数取模、大数的进制转换贪心算法的进阶区间调度、哈夫曼编码、活动选择问题模拟题与状态机很多业务逻辑本质上是一个状态机面试时能画出状态转移图会让人眼前一亮字典序问题的变体字典序的第K个排列、下一个排列、字符串字典序比较的变种这些内容在LeetCode和牛客网上都有大量对应习题难度覆盖从入门到竞赛级可以根据自己的水平逐步提高。7.4 最后再分享一点刷这套题时我发现一个很有意思的现象大多数人卡住的地方不在算法本身而在“理解题意”和“处理边界”。比如选靓号明明枚举目标数字的思路几十行就能写完但很多人在“字典序最小”这个要求上绕了很久。我的经验是面对每一道编程题先用通俗语言把题目复述一遍把输入输出样例手动推演一遍再动手写代码。这个习惯养成后不仅笔试正确率大幅提升连日常开发里接到需求时的理解速度都会快不少。拼多多这套2018校招内推题放在今天依然是很好的训练材料。如果你能把这几道题吃透达到看到题目就能快速梳理出“用什么算法、需要维护什么状态、有哪些边界要处理”的水平那你在技术面试中的算法环节基本上就有了扎实的底子。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表