☰
拼多多2020校招笔试编程题解析:从业务场景到算法模型
2026/10/19 21:46:24 网站建设 项目流程

拼多多2020校招笔试的编程题,在当年那批大厂笔试里算是一股清流。没有特别偏门的算法,也不怎么考复杂的数据结构,但每道题都带着浓厚的业务气息,要么是订单流转,要么是果园种植,要么是营销玩法。正因如此,很多人被题目绕进去,以为要设计一个完整系统,结果发现其实就是一道模拟或者动态规划。我带过的学生里,有不少人参加过那场笔试,事后复盘时观点非常一致:基础算法吃透,再培养一点“把业务场景翻译成算法模型”的能力,拿高分的概率非常大。

这篇内容围绕拼多多2020校招部分编程题展开,重点拆解题型结构、解题思路和代码实现。我会把当时流传度比较高的几道题目重新整理,按照从读题到AC的完整过程讲一遍,同时把容易踩的坑标记出来。无论你是正在备战校招的应届生,还是想查漏补缺的工作党,只要目标是大厂笔试,这些内容都能直接用上。

1. 拼多多2020校招笔试:整体考察什么

1.1 题型分布与时间安排

拼多多2020校招的在线笔试,一般安排在90分钟左右,编程大题数量在4到5道之间。我接触到的考生反馈里,最常见的是4道编程题,没有选择题和填空题,全部在OJ环境里完成。题目难度采用梯度设计:第一题通常是最基础的模拟或哈希表应用,送分题;第二题开始上强度,可能会用到贪心或者动态规划;第三题、第四题则偏向业务场景包装下的中等算法题,偶尔会有一道偏数学期望的题目。

时间分配上,前两题建议控制在20分钟以内,后两题每道留出25到30分钟。因为每道题都需要读题、思考、编码、调试,尤其是第三道以后,题目往往又长又绕,读完没理清数据范围就动手写,很容易浪费大量时间。我见过不少同学在第二题卡了40分钟,最后第四题没时间写,这是最可惜的。

1.2 难度梯队与典型考点

从考点分布看,拼多多2020校招笔试几乎没有涉及线段树、后缀数组这类冷门数据结构,重点集中在几类高频基础算法:

  • 哈希表与模拟:用于处理订单、库存、信息匹配类问题。
  • 排序与贪心:用于区间选择、任务调度类问题。
  • 动态规划:背包问题、字符串编辑距离、打家劫舍类问题。
  • 数学期望与概率:掷骰子、抽卡类问题。
  • 滑动窗口和双指针:数组子区间类问题。

这类“业务场景 + 基础算法”的组合,是拼多多笔试的最大特点。相比某些大厂上来就是硬核红黑树、单调栈,拼多多更看重你能不能把一个具体的营销活动抽象成已知的算法模型。换句话说,题目本身不难,难的是你能不能识别出来它到底在考什么。

1.3 业务场景题的通用解法框架

面对拼多多这类业务包装型编程题,我总结了一个非常实用的三步框架,后面几道真题都会用到。

第一步,抽离“实体”。把题目里提到的用户、订单、商品、库存、坑位、骰子这些名词,变成代码里的变量和数据结构。订单就是dict,库存就是另一个dict,坑位就是数组,骰子面数就是整数n。

第二步,抽取“操作”。把业务动作转化成算法动作。检查订单能不能满足,变成查哈希表;相邻不能种树,变成动态规划状态约束;编辑字符串,变成二维DP转移。

第三步,确定“约束”。看数据范围决定复杂度。n是1e5还是1e3,决定能不能用O(n^2)的暴力,还是必须优化到O(n log n)。

这三个步骤走完,一道看起来花里胡哨的题目就变成了一行核心算法关键词。剩下的事,就是把这个关键词对应的模板代码写出来。

2. 高频真题拆解:解题思路是这样建立的

2.1 订单匹配:用哈希表把库存变成查询字典

先看第一道典型题,我叫它“订单匹配”。题目大意如下:

仓库里有多件商品,每件商品有一个商品id和库存数量。现在收到n个订单,每个订单包含若干种商品以及对应数量。如果一个订单里所有商品的需求量都不超过当前库存,这个订单就可以被满足。求最终能满足的订单数量。

约束方面,商品id是一个整数,订单总数和库存商品种类数都可能达到1e5级别,每个订单包含的商品种类总和也可能很大。

这道题就是典型的哈希表模拟。先读入所有库存,以商品id为key,库存数量为value,存进一个字典。然后遍历每个订单,对订单里的每种商品检查库存字典里有没有足够的数量。只要有一个商品不够,就判定这个订单无法满足,直接break。

这里有个小坑:同一件商品在库存输入里可能出现多行,每次出现都是增加库存,而不是覆盖。所以存字典时要写成stock[pid] = stock.get(pid, 0) + cnt,不要直接赋值。订单内部也可能有重复商品id,题目如果没说就不必合并,但为了稳妥,也可以用order.get(pid, 0) + cnt合并,不过不合并也不影响正确性,因为后续会逐项判断。

核心代码用Python写是这样的:

import sys def main(): data = sys.stdin.buffer.read().split() if not data: return it = iter(data) n = int(next(it)) orders = [] for _ in range(n): t = int(next(it)) order = {} for _ in range(t): pid = int(next(it)) cnt = int(next(it)) order[pid] = cnt orders.append(order) k = int(next(it)) stock = {} for _ in range(k): pid = int(next(it)) cnt = int(next(it)) stock[pid] = stock.get(pid, 0) + cnt ans = 0 for order in orders: ok = True for pid, cnt in order.items(): if stock.get(pid, 0) < cnt: ok = False break if ok: ans += 1 print(ans) if __name__ == "__main__": main()

简单解释一下为什么用sys.stdin.buffer.read().split()而不是input().split()。在线笔试环境里,输入数据可能非常大,几千行是常事。input()每次读一行会有额外开销,在Python里很容易导致超时。一次性把所有数据读进来再切分,速度会快一个量级。这个习惯建议从刷题第一天就养成。

2.2 果园种树:相邻限制下的动态规划

第二题是拼多多典型的业务包装题,代码很短,但思路需要转个弯。题目是这样:

一排有n个坑位,每个坑位种上树之后能带来一个价值a_i,但因为根系会互相影响,不能同时在相邻的两个坑位种树。问在满足限制的前提下,能得到的最大总价值是多少。

看到“不能相邻选两个”这个条件,就应该立刻联想到打家劫舍问题。动态规划的状态定义很简单:

  • dp[i]表示考虑前i个坑位时能获得的最大价值。
  • 对于第i个坑位,有两种选择:不种,那么结果就是dp[i-1];如果种,那么第i-1个坑位不能种,所以结果是dp[i-2] + a[i]。
  • 最终取这两种情况的最大值。

初始状态要处理好。只有一个坑位时,最大价值就是a[0];没有坑位时价值为0。我一般用两个滚动变量代替整个dp数组,省空间。

def max_value(a): n = len(a) if n == 0: return 0 if n == 1: return a[0] prev2 = 0 # dp[i-2] prev1 = a[0] # dp[i-1] for i in range(1, n): cur = max(prev1, prev2 + a[i]) prev2, prev1 = prev1, cur return prev1

这道题写起来不出十行,但有几个细节值得注意。题目中的价值可能达到1e9,所以结果要用64位整数保存,Python的int没有溢出问题,但如果你用C++就必须用long long。另外数据范围如果n达到1e5,空间复杂度O(n)也完全可以接受,但滚动变量更优雅,面试官看到也会觉得你基本功扎实。

这道题还有一个变体是环形的,也就是首尾坑位相邻,只能用一次。处理方法是分两次DP:一次不考虑第一个坑位,一次不考虑最后一个坑位,取最大值。拼多多2020年有没有出变体我不确定,但把这种变形思路记下来,遇到类似题目时就不慌了。

2.3 编辑距离:二维DP的经典模板

字符串编辑距离属于笔试常青树,拼多多那批题里也出现过。题目要求给定两个字符串s和t,允许你进行三种操作:插入一个字符、删除一个字符、替换一个字符。问把s变成t最少需要多少次操作。

这道题的状态转移是很多人的第一个二维DP模板。定义dp[i][j]表示s的前i个字符变成t的前j个字符需要的最少操作次数。

初始化很直接:dp[i][0] = i表示把s的前i个字符全部删除;dp[0][j] = j表示在空串里插入j个字符。

转移的时候,如果s[i-1] == t[j-1],那么不需要额外操作,dp[i][j] = dp[i-1][j-1]。如果不相等,可以从三种操作里挑一个最小的:

  • 删除s的当前字符:dp[i-1][j] + 1
  • 在s里插入一个字符:dp[i][j-1] + 1
  • 替换s的当前字符:dp[i-1][j-1] + 1

完整实现:

def min_distance(s, t): m, n = len(s), len(t) dp = [[0] * (n + 1) for _ in range(m + 1)] for i in range(m + 1): dp[i][0] = i for j in range(n + 1): dp[0][j] = j for i in range(1, m + 1): for j in range(1, n + 1): if s[i - 1] == t[j - 1]: dp[i][j] = dp[i - 1][j - 1] else: dp[i][j] = min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]) + 1 return dp[m][n]

如果字符串长度在1000以内,二维数组完全没问题。但如果是5000以上,二维数组可能占用25MB,有点紧张,可以用滚动数组优化成两行。不过优化时要注意dp[i][j-1]的值需要从当前行左边拿到,不能覆盖太早,处理起来比打家劫舍要小心一点。我第一次写滚动数组时就把左边的值覆盖了,结果算出一堆错误。正确写法是用一个prev保存左上角的值。

2.4 掷骰子:最优停止的期望DP

再来看一道偏数学期望的题。题目简化版是这样的:

有一个n面的骰子,每面点数分别为1到n。现在你最多可以掷k次。每次掷完骰子,你可以选择“收手”,拿走当前这个点数作为最终得分;也可以选择“继续”,但会消耗一次机会。问在最优策略下,你能得到的期望分数是多少。

这道题的状态比较有意思。设dp[i]表示当前还能掷i次时,你“准备掷骰子”的期望收益。那么当你掷出一个点数x之后,你有两个选择:拿x走人,或者继续掷,此时还能掷i-1次,期望收益是dp[i-1]。所以你一定会选择max(x, dp[i-1])。

因此转移方程是:

dp[i] = (1/n) * sum(max(x, dp[i-1]) for x in range(1, n+1))

初始dp[0] = 0,也就是没有机会时收益为0。这个初始值非常关键,它保证了只有一次机会时,期望就是所有点数的平均值(n+1)/2。

直接按照这个方程写,时间复杂度是O(nk)。k如果很大,比如1e5,n又很大,就会超时。不过我们可以做一个简单优化:当x <= dp时,max(x, dp)恒等于dp;当x > dp时,结果就是x。而x是连续的1到n,所以可以用整数边界快速求和。

def dice_expectation(n, k): dp = 0.0 for _ in range(k): t = int(dp) # 点数小于等于dp的个数 if t > n: t = n sum_part1 = t * dp if t < n: sum_part2 = (t + 1 + n) * (n - t) // 2 else: sum_part2 = 0 dp = (sum_part1 + sum_part2) / n return dp

比如输入n=6, k=2,算出来是4.25。这个数字可以自己手算验证:先算dp[1]=3.5,再算dp[2] = (3.5×3 + 4+5+6)/6 = 4.25。如果题目要求保留小数,直接print格式化即可。

这道题考察的点很综合:能把连续决策问题抽象出状态,能写出递推,还能在小数据范围内推算出样例。相比前面几道,它更考验“建模”能力。拼多多这类公司尤其喜欢在笔试里加一道这种题,用来筛掉只会背模板、不会分析问题的人。

3. 从暴力到最优:代码实现的完整演进

3.1 数据范围决定算法:先看约束再动手

很多同学看到题目就直接开始写暴力,写完才发现复杂度不对,又推倒重来。这个习惯在笔试里非常致命。我建议拿到题目后,第一件事不是写代码,而是找数据范围。拼多多2020年这几道题,数据范围设得很有讲究。

以订单匹配为例,如果总商品种类只有100,那么三重循环都没问题。但实际给到1e5,就必须用哈希表把查询从O(L)降到O(1)。果园种树也一样,如果n只有20,可以状态压缩暴力枚举;但n给到1e5,就只能用O(n)的DP。编辑距离的m和n如果都是1000,O(mn)的二维DP没问题,但如果都是5000,就必须考虑滚动数组。

所以我会在草稿纸上先把约束条件圈出来,然后立刻给算法定复杂度。通常经验是:

  • n ≤ 20:可以暴力枚举、状态压缩。
  • n ≤ 5000:O(n^2)可能可以,但要看常数。
  • n ≤ 1e5:必须是O(n log n)或O(n)。
  • n ≤ 1e9:基本是数学题或贪心,需要O(log n)或O(1)。

3.2 读入优化:笔试里的隐形送分项

拼多多笔试用的是标准输入输出,没有本地文件,题目里给的样例只是用来验证思路的。真正跑分的时候,输入可能非常大。Python选手如果使用input()逐行读,很容易在读数据上花掉好几秒,导致超时。

我一直用的模板是:

import sys data = sys.stdin.buffer.read().split() it = iter(data)

然后所有的整数都用int(next(it))读取。这样做的原理是:sys.stdin.buffer直接读取二进制流,比文本流更快;read()一次性读取全部数据,再通过split()按空白字符切分成列表,避免了多次系统调用。对于几十万级别的数据,速度差距可以缩小一个数量级。

如果题目输入格式比较固定,也可以使用sys.stdin.readline,但如果是多行混合、字段数量不定的情况,全量读取read().split()是最稳的。

3.3 边界测试与调试技巧

笔试里最尴尬的情况是:样例通过了,但提交后是0分。这时候大概率是边界条件没有覆盖到。我通常会养成一个习惯,写完代码后先自己构造几组极端输入测试。

对于订单匹配,我会测:

  • 库存为空,但订单里有商品:应该输出0。
  • 订单的商品数量正好等于库存:应该算满足。
  • 库存里有重复商品id:确保数量是累加的。

对于果园种树,我会测:

  • n=1:直接返回a[0]。
  • n=2:返回max(a[0], a[1])。
  • 所有价值都为0:输出0。
  • 价值为负数?一般不会,但如果有,需要约定能否不种,通常题目保证非负。

编辑距离,我会测:

  • 两个空串:0。
  • 一个空串一个非空串:返回长度。
  • 完全相同:0。
  • 只有替换操作的情况:长度相同但字符不同。

骰子期望,我会测:

  • k=0:理论上应该是0,但题目通常k≥1。
  • n=1:输出1。
  • k=1:输出平均数。

把这些边界测完,基本不会出现因为极端输入导致的翻车。调试时不要只盯着逻辑,可以打印中间变量,比如dp数组、库存字典,和手算结果对比,这样定位问题会快很多。

4. 笔试中的常见问题与避坑指南

4.1 输入输出格式导致的“血案”

拼多多在线编译系统对输入输出格式非常严格,多一个空格、少一个换行都可能导致误判。最常见的问题有两个。

第一个是输出精度。对于浮点数,题目如果要求保留六位小数,就一定要用print("{:.6f}".format(ans)),不能直接print(ans)。C++则需要printf("%.6f\n", ans)。有些同学会把样例里没保留的输出也交上去,结果答案错误。

第二个是换行符。很多题目要求“输出多少行就多少行”,最后一组数据后面有没有换行无所谓,但每组数据之间不能有多余空行。最好在写完代码后,用题目给的样例跑一遍,肉眼对比输出结果,尤其是空格和空行。

4.2 边界条件:空串、单元素、极大值

数组类题目最容易在边界上出错。比如果园种树,如果n=0,代码里直接返回0;不写这个判断,后面访问a[0]就会报IndexError。再比如编辑距离,当其中一个串长度为0时,二维数组的初始化代码刚好能覆盖,但如果你用递归写法,可能会栈溢出。

还有一种情况是极大值。题目说价值可能到1e9,如果使用C++的int,结果可能超过2^31-1,必须用long long。Python自然无压力,但如果你用C++,请一定仔细读题中给的数据范围。

我整理了一个笔试速查表,可以对照着检查自己的代码:

问题类型常见原因解决办法
样例通过但提交0分边界条件没覆盖构造空输入、单元素、最大输入测试
超时复杂度太高或输入太慢换成O(n log n)算法,读入改用read().split()
答案错误但逻辑看起来没问题字典键值覆盖了,或类型错误检查重复key,检查int和float混用
输出格式不对多空行、少换行、小数位不对用题目样例严格比对,用格式化输出

4.3 样例通过但提交0分的原因

后面我带学生复盘时发现,十个“样例通过”的人里,至少有四五个是因为同一个坑:读入时没有处理同一个key出现多次的情况。

比如订单匹配里库存输入可能写成:

3 1 10 2 20 1 5

这里商品1出现了两次,总库存应该是15而不是10。如果你存字典时直接stock[pid] = cnt,第二次就会把第一次覆盖,最终结果错误。用stock[pid] = stock.get(pid, 0) + cnt就不会有这个问题。

编辑距离还有一个隐藏坑:题目的字符串可能会包含空格。如果使用sys.stdin.buffer.read().split(),空格会被当作分隔符切掉,字符串就读取不完整。遇到这种情况,应该改用readline()按行读取。所以在读题时一定要看字符串是否可能包含空格,这是一个非常细节但致命的问题。拼多多原题里字符串通常只包含小写字母,但稳妥起见,遇到字符串时我都会单独处理。

5. 校招备战复盘:怎么刷题才有效

5.1 以拼多多为代表的“业务型笔试题”训练法

如果你已经刷了几百道LeetCode,但参加笔试还是觉得题目陌生,很可能是缺少“业务翻译”训练。常规平台上的题目都是裸的数据结构和算法,比如“给定一个数组,求最大子序和”。但拼多多的题目不会这么直白,它会说“多多果园的树要隔一颗种一颗,怎么选价值最大”,或者“一堆订单要匹配库存,怎么判断能否发货”。

训练方法很简单:刷题时多问自己一个问题——“如果这是一道商业题,它会出现在什么业务场景里?” 比如看到最长递增子序列,可以联想成“按时间顺序选择优惠券”;看到区间合并,可以联想成“多个配送员的时间段合并”。这样在真正笔试时,看到一大段业务描述就不会慌,因为你知道它底下包着的一定是某个基础算法。

5.2 刷题优先级与推荐题单

以拼多多2020校招笔试为参照,我建议按照下面的优先级准备:

第一梯队:哈希表、数组模拟、字符串基础操作。这些是送分题,必须全对。可以在LeetCode上刷“Two Sum”“Isomorphic Strings”“Valid Anagram”等基础题。

第二梯队:动态规划入门。打家劫舍、背包问题、编辑距离、最长公共子序列,这些是笔试中高频考察点。建议每种题型做至少5道,直到能默写出模板。

第三梯队:贪心与排序。区间调度、任务安排、最大收益类题目容易和业务场景结合,需要掌握排序后如何决策。

第四梯队:数学期望与概率。这类题不算特别多,但一旦出现就是区分度。建议把“掷骰子期望”“抽卡概率”“随机变量期望”这类经典题弄明白。

LeetCode题目标签可以直接搜“Dynamic Programming”“Greedy”“Hash Table”,按难度从Easy到Medium刷。不用刻意刷Hard,拼多多笔试的压轴题难度最多也就是Medium偏上。

5.3 从笔试到面试:算法思维如何延续

笔试只是校招的第一关,拼多多面试时同样会问算法,而且更注重推导过程。我当时一个很深的感受是:笔试中可以靠刷题堆题感,但面试时必须能讲清楚“为什么用这个方法”“有没有更优解”“能不能优化空间”。

所以在准备笔试时,不要只满足于AC。每道题完成之后,问自己三个问题:

第一个问题,这个状态定义是怎么想到的?要想清楚是经验还是推导。 第二个问题,如果数据扩大10倍,这个解法还成立吗? 第三个问题,能不能把空间复杂度降到O(1)或者把二阶DP转成一阶?

把这三个问题想明白,你就不是在这里做题,而是在建立算法思维。这种思维从笔试一直延续到面试,甚至到实际工作中的代码设计,才是刷题真正有价值的地方。

我在实际带人的过程中发现,很多同学刚刷题时一道Easy都要看题解,但坚持用这种“翻译+复盘”的方式训练两个月后,再做拼多多这类业务包装型题目,基本都能稳定解出三到四题。最后再分享一个亲测有效的小技巧:正式笔试前,自己准备一个固定的代码模板,包含最快的读入方式、常用的包导入、浮点数格式化输出。不要小看这几行代码,它能让你在开局时比别人快出一两分钟,而且能减少紧张带来的低级错误。祝你在下一次笔试里顺利AC。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询