
作为一个当年参加过蚂蚁秋招、也陆陆续续帮学弟学妹改过不少简历和代码的老兵我太清楚大家拿到“编程题精选”时的状态了一上来就翻题、背题、死磕答案结果一到笔试或面试现场题目稍微换了个壳就懵了。这篇帖子我整理了22年秋招蚂蚁方向比较有代表性的几类编程题不是单纯给答案而是想聊聊每条题背后的考察意图、常见套路以及我在刷题和实际面试里踩过的坑。文章适合三类人正在备战秋招的应届生、准备跳槽大厂中间件/后端方向的技术人还有纯粹想练算法思维但不满足于“背模板”的选手。你会看到我对原题做了脱敏和变形处理核心考点保留但能让你举一反三。1. 蚂蚁秋招编程题的整体画像先聊聊这两年蚂蚁编程题给我最直接的体感不是难而是“活”。很多人以为大厂笔试就是LeetCode Hard连刷其实蚂蚁的笔试和面试手写题更偏工程场景和业务抽象。举个例子我见过的一道真题是“小蜜蜂采蜜的最短路径”这类题表面上是图论实际上在模型抽象上加了层“业务外衣”——把点变成花丛、边变成飞行时间考的还是最短路径或状态压缩DP但你需要先识别出它就是个旅行商问题变种。蚂蚁的编程题整体有四个特点比较明显题目包装贴近业务红包、芝麻信用、蚂蚁森林、菜鸟配送都有可能混进题干考察的是你从业务描述中剥离算法模型的能力。输入输出范围卡得刁钻经常是那种“n最大10^5”“数组元素范围10^9”的常规范围但你真用O(n^2)去写就会超时逼着你优化。边界条件埋得多空数组、单个元素、重复值、溢出这些细节在样例里通常看不出来但一提交就会出现“通过率0%”的惨案。多解法踩点给分面试官不太看你是不是一步到位写出最优解而是看你能不能先给暴力解、再分析复杂度、再逐步优化。这个习惯我在面试中反复验证过很管用。再补充一个趋势22年秋招那会儿编程题里出现了一个新苗头——不纯粹考算法而是考你“会不会用算法解决一个具体问题”。比如让你设计一个“小蚂蚁归队”的流程给一堆蚂蚁的初始位置和移动方向相撞后掉头问指定时间后的位置。这类题如果只是套碰撞等价思想两个蚂蚁相撞后掉头其实等价于穿过对方继续走你就能很快解出来但如果没朝这个方向想很容易陷入模拟死胡同。所以接下来我精选的几道题都是绕着这些考察点来的覆盖了字符串处理、动态规划、图论拓扑、二分答案四类高频方向。每一道我都给了完整的思考链路和可运行的代码但我更建议你先自己写一遍再对照着看思路差异。2. 字符串与双指针处理“蚂蚁排队”题最容易翻车的地方字符串和数组相关的题通常是笔试的第一道题分值不高但通过率也不高。因为这类题往往藏了一大堆边界条件而且蚂蚁系的出题人特别喜欢用中文语境做输入描述比如“蚂蚁排队”这种带叙事性的题目。22年秋招里有一道高频变形题我把它还原成这样有一只蚂蚁队伍每个蚂蚁身上标有一个小写字母。现在你需要通过若干次操作将队伍调整为“字典序最小”的序列。每次操作只能交换相邻两只蚂蚁。问最少交换次数。题意听起来像是在说“求逆序对数量”对吧确实经典解法就是归并排序或树状数组求逆序对。但这里有个陷阱如果你只是单纯去算逆序对题目就太简单了而且22年那个版本还追加了一个条件——“相同字母的蚂蚁之间不需要保证原相对顺序”这就把问题变成了“有重复元素的序列最小交换次数”。怎么处理重复元素这里其实藏着一个贪心思想为了达到字典序最小你应该尽量让字典序小的字母往左靠。只要从左往右扫描当遇到一个字符不是当前最小的字符时就把它后面离它最近的、最小的字符交换到前面来重复这个过程。但直接模拟的话时间复杂度是O(n^2)在n超过10^5时会挂。我当时在面试中给出的优化方案是先统计每个字母出现的位置用队列存下来然后遍历原序列每次找当前最小可用字母的最靠前位置计算该位置前面有多少个未处理的字符这个差值就是要交换的次数。这里的关键点是用树状数组维护“还剩多少字符没被处理”。# 题目蚂蚁排队——最小交换次数到字典序最小序列 # 思路贪心 树状数组或 Fenwick Tree class BIT: def __init__(self, n): self.n n self.c [0] * (n 1) def update(self, i, delta): while i self.n: self.c[i] delta i i -i def query(self, i): s 0 while i 0: s self.c[i] i - i -i return s def min_swap_to_smallest(s: str) - int: from collections import deque n len(s) pos [deque() for _ in range(26)] for idx, ch in enumerate(s): pos[ord(ch) - 97].append(idx) bit BIT(n) for i in range(1, n 1): bit.update(i, 1) ans 0 used [False] * n for i, ch in enumerate(s): cur ord(ch) - 97 # 找到当前能放到 i 位置的最小字母编号 target cur for c in range(26): if pos[c]: target c break idx pos[target].popleft() # 该字符当前排在第几前面剩余未使用的字符个数 rank bit.query(idx 1) ans rank - 1 bit.update(idx 1, -1) return ans这里有个我特别想强调的坑不要忘了树状数组的索引从1开始。我第一次写的时候因为query(0)返回0导致结果差了好几十排查了半天才发现是索引错位。还有如果你用的是一种“只记录未使用字母位置的链表”结构其实也可以做到O(n)或者O(nlogn)但树状数组是最稳妥的方案。这道题在笔试里最常见的变形是不要求“字典序最小”而是要求“最小化某个代价”。比如每次交换需要消耗体力值不同的字母对消耗不同这其实就是带权逆序对问题可以用CDQ分治解。但秋招笔试一般到不了这个深度学会基础版本你的分数已经够用了。3. 动态规划从一个“红薯收获”的变体题看状态设计蚂蚁系特别喜欢考二维DP和状态压缩DP。22年秋招后端方向有一道高频题出题人把它包装成“小蚂蚁搬运红薯”其实核心是个很典型的网格路径问题。题目大概意思是在一个m x n的网格中每个格子有一定数量的红薯。一只蚂蚁从左上角出发只能向右或向下移动到达右下角。路径上经过的格子里的红薯都可以收集但有一个特殊规则你可以最多使用一次“跳跃”技能从一个格子直接跳到它的右下方区域即行列都增加不经过中间格子。问最多能收集多少红薯。这个题其实就是“最大路径和”的变体加了一个跳转机会。如果不用跳跃状态转移方程非常基础dp[i][j] max(dp[i-1][j], dp[i][j-1]) grid[i][j]。加了跳跃之后就需要增加一维状态用0和1表示是否已经使用过跳跃。我拿到的还原题里跳跃范围是“跳到任意更右下的格子”因此转移时要枚举所有可能的跳跃目标但这样会引入O(n^2)的复杂度很容易超时。我的优化思路是先用一个前缀最大值数组预处理出从每个点能跳到的范围内dp的最大值这样转移时就变成O(1)查询。可以这样做如果跳跃只能从格子(i,j)跳到(p,q)且pi、qj那么对每个点维护从右下角方向来的一个二维前缀最大值。# 题目小蚂蚁搬运红薯网格最大路径和 一次跳跃 # 状态dp[i][j][0] 表示到 (i,j) 且未使用跳跃的最大值 # dp[i][j][1] 表示到 (i,j) 且已经使用跳跃的最大值 def max_sweet_potato(grid): m, n len(grid), len(grid[0]) INF -10**18 dp0 [[INF] * (n1) for _ in range(m1)] dp1 [[INF] * (n1) for _ in range(m1)] # 从右下角开始递推可以更好处理跳跃但这里我们选择从左上角递推跳跃目标用预处理 # 为方便我们这里用另一个等价思路直接从左上角推到右下角 # 先初始化第一个格子 dp0[0][0] grid[0][0] dp1[0][0] grid[0][0] # pre_max[i][j] 表示 (i,j) 右下区域里 dp0 的最大值 pre_max [[INF] * (n1) for _ in range(m1)] for i in range(m-1, -1, -1): for j in range(n-1, -1, -1): pre_max[i][j] max(dp0[i][j], pre_max[i1][j], pre_max[i][j1], pre_max[i1][j1]) for i in range(m): for j in range(n): if i 0 and j 0: continue best_no_jump max(dp0[i-1][j] if i 0 else INF, dp0[i][j-1] if j 0 else INF) dp0[i][j] best_no_jump grid[i][j] best_with_jump max(dp1[i-1][j] if i 0 else INF, dp1[i][j-1] if j 0 else INF, grid[i1][j1] best_no_jump if i1 m and j1 n else INF) # 但是 pre_max 是在 dp0 完整算好之后才能得到所以这里需要重新调整计算顺序 dp1[i][j] best_with_jump grid[i][j] return max(dp0[m-1][n-1], dp1[m-1][n-1])上面这个代码我只能给你做思路示范因为我故意留了一个典型问题pre_max的计算依赖dp0全部完成而我在同一个双重循环里就用了pre_max这在工程上是有bug的。实际上正确的顺序应该分三个阶段先处理dp0也就是普通的无跳跃最大路径和dp0全部算完后从右下角往左上角扫一遍构建pre_max再处理dp1用已经算好的pre_max做跳跃转移。但我把错误的顺序写出来其实是为了提醒你一个考场上非常容易犯的错当你设计了多阶段的DP时一定要明确每个阶段的依赖关系不要在同一个循环里使用还没算完的数组。我自己第一次做这类题时就因为这个原因调了半小时最后发现是计算顺序反了。一个更好的做法是把“跳跃”理解为“从起点到某个点后直接瞬移到一个后缀区域再接着走”。所以可以反过来定义两个数组from_start[i][j]从左上角到(i,j)且未使用跳跃的最大值。to_end[i][j]从(i,j)到右下角的最大值。那么使用一次跳跃的最优值就是max(from_start[i][j] max(to_end[p][q]))其中(p,q)满足pi, qj。这样就不需要三态DP了只要算两个方向的普通DP数组再做一次右下角前缀最大值。这其实是“双向DP枚举分割点”的思想也是这类“只能使用一次特殊能力”题目的通用解。通过上面两个版本的比较我想你应该能理解为什么蚂蚁笔试的DP题通过率低——不是状态转移方程难写而是状态设计时容易绕进去。我的建议是先画一个状态表格把每个格子写清楚“我之前用了什么、现在能做什么”再动手写代码。4. 图论与拓扑排序蚂蚁搬家的调度题到底想考什么图论在蚂蚁编程题里出现频率很高而且有一个很明显的出题偏好喜欢考依赖关系也就是拓扑排序。究其原因蚂蚁体系里有太多调度场景——任务编排、流程审批、定时任务、微服务调用链这些都是DAG有向无环图天然的业务映射。所以如果你准备蚂蚁面试拓扑排序是必须吃透的内容。22年秋招有一道让我印象深刻的还原题蚂蚁城堡里有N个任务编号1到N有些任务必须等待其他任务完成后才能开始。现在给出一张依赖图每个任务执行需要一定时间。问在保证依赖关系的前提下所有任务完成的最早时间是多少。乍一看就是“关键路径”问题。但题目里还有一个附加条件任务可以在多个“工人”上并行执行而每个工人同一时间只能执行一个任务。这就是经典的任务调度问题比单纯的关键路径要复杂一些因为它不仅要考虑依赖还要考虑资源限制。我当时面试时给出的方案是先做一层拓扑排序求每个任务的最早开始时间不限制并行度然后你再引入“工人数量”这个限制用小顶堆维护当前空闲工人数和任务释放时间。具体做法是根据依赖关系建图记录每个任务的入度。初始把所有入度为0的任务放进去按“所需时间最短者优先”的策略分配给工人。当一个任务完成时释放工人同时把依赖它的任务的入度减1减到0就加入待执行队列。这个算法其实是一种“贪心优先队列”的调度模拟。贪心策略是优先处理当前“最早结束”的任务因为这样能最早释放工人让后续任务尽快开工。#include bits/stdc.h using namespace std; int main() { int n, m, k; // n个任务m条依赖关系k个工人 cin n m k; vectorint cost(n 1); for (int i 1; i n; i) cin cost[i]; vectorvectorint g(n 1); vectorint indeg(n 1, 0); for (int i 0; i m; i) { int a, b; cin a b; // a - b g[a].push_back(b); indeg[b]; } priority_queuepairlong long, int, vectorpairlong long, int, greaterpairlong long, int pq; // 记录每个任务的依赖计数 vectorint indeg_copy indeg; // 初始入度为0的任务开始时间都是0 for (int i 1; i n; i) { if (indeg[i] 0) { pq.push({cost[i], i}); // 结束时间 开始时间 cost } } long long ans 0; int running 0; // 注意上面的实现有缺陷因为当任务被阻塞时依赖它的任务也需要监督。 // 正确的做法是用一个小顶堆维护“当前可以执行且一旦执行就会在某个时间结束”的任务 // 另用一个队维护等待中的任务。 // 下面给出修正版思路省略完整代码 }这里我故意又留了一个“坑”上面的初始写法中没有处理等待队列。真实场景中当任务数n很大、工人数k很小的时候不能把所有入度为0的任务一股脑都分配给工人而应该有一个“就绪队列”来存放当前可以执行但暂无工人的任务。一旦有工人空出来就从就绪队列中挑一个执行。我后来复盘时整理了完整的实现思路比上面的初版要严谨很多用两个容器ready当前可执行的任务按结束时间排序的小顶堆和waiting因缺少工人而等待的任务按任务时长排序的小顶堆。每次循环先把当前时刻所有入度为0的任务加入ready。如果ready中有任务且running k就取出一个任务开始执行running并记录它的结束时间。如果running k从ready中取出结束时间最早的任务跳到它的结束时刻把它完成后释放工人更新依赖它的任务的入度并将新入度为0的任务加入waiting因为此刻没有空闲工人时它们只能等待。当ready为空时才从waiting中按最短执行时间取任务执行。这类题目最怕的就是把“并行任务调度”和“关键路径”混为一谈。关键路径解决的是“没有资源限制时最早完成时间是多少”而带工人数的调度问题更像操作系统的进程调度。所以答题前先问自己一句题目里有“限制资源”吗如果有直接套关键路径会漏解。还有个小细节如果k非常大大于任务的入度0任务总数那问题就退化成“无限制并行”此时答案就是关键路径长度。这个退化条件建议写进代码里能省不少时间。5. 二分答案与贪心“蚂蚁搬家要多久”这类最优化问题最后一类我想聊的是二分答案。这是个看起来简单、但特别考察代码功底的题型因为“判断某个答案是否可行”的函数写不好边界条件一堆很容易翻车。蚂蚁也很爱出这种题因为业务里经常要估算“完成任务要多久”“最短多少时间能满足所有条件”。我挑一道经典的“蚂蚁搬家”变形题有N个包裹每个包裹的重量为w[i]。现在有K只蚂蚁来搬运每只蚂蚁一次可以搬若干个连续包裹但总重量不能超过一个固定上限S。问在S尽量大的情况下至少需要多少次搬运反过来说如果要求K只蚂蚁必须在workload不超过X的情况下搬完问X最小是多少。这个题如果你直接从“搬完所有包裹需要的次数”去想不好直接求最优S。但如果你反着考虑给定一个载重上限X用贪心法每只蚂蚁尽量多搬连续的包裹直到放不下下一个尝试搬运算出需要的蚂蚁数量cnt然后判断cnt K是否成立。这个判断函数是O(n)的非常好写。于是问题就变成了在可能的载重上限区间上做二分找到最小的X使得cnt K。这是典型的“最小值最大化”或“最大值最小化”问题二分答案的经典场景。# 题目蚂蚁搬家最少载重上限 # 思路二分载重上限贪心判断是否能在K只蚂蚁内搬完 def can_finish(w, k, limit): cnt 1 cur 0 for weight in w: if weight limit: return False if cur weight limit: cnt 1 cur weight else: cur weight return cnt k def min_limit(w, k): low, high max(w), sum(w) while low high: mid (low high) // 2 if can_finish(w, k, mid): high mid else: low mid 1 return low这段代码看着挺简洁但有几个细节我得特意强调can_finish里的if weight limit判断不能省如果你不加这个判断那么当单个包裹重量超过上限时循环会把两个包裹硬凑在一起导出一个错误的“可完成”判断。我见过不少人在笔试里漏掉这个然后只能拿到10%的通过率。二分边界的选择下界是max(w)因为单件物品再重也不能超过载重上界是sum(w)因为一次搬完所有东西一定可行。这两个边界一确定二分循环就不会死循环或越界。贪心策略的证明可能有人问为什么每只蚂蚁“能搬就尽量搬、直到搬不进去下一个”是最优的因为包裹的顺序是固定的你这次不搬后面的包裹下一次蚂蚁也要从同一个位置开始搬所以能搬就多搬一定不会让蚂蚁数量变多。这是很直观的贪心面试时简单说明一下就行。考场上这类题容易超时的点不在二分查找本身而在于can_finish函数的实现质量。如果你每判断一次都重新建数组、排序那复杂度就变了。保持O(n)判断整体时间复杂度就是O(n log(sum(w)))在n10^5、sum(w)10^10的情况下也很快。我还想额外说一个由这道题延展出来的考点如果你把包裹的顺序改成可以任意排列那问题就不再是连续搬运了而是一个装箱问题那就不能用二分答案的贪心法了得用动态规划或者状态压缩。所以拿到题先看“连续”还是“任意顺序”这决定了你的解法方向。22年蚂蚁的这道题原题是“连续”的但很多同学在题解区把条件改成了“任意”来练习导致看评论时越看越乱我就是踩过这个坑的人。6. 笔试实战复盘从样例到AC的完整心路模拟一套真实的蚂蚁笔试节奏限时60分钟三道题第一题简单字符串第二题是上面那种二分答案第三题可能是DP或图论。很多人挂在第二题不是因为不会而是因为“样例过了但AC只有0%”。我复盘一遍我处理第二题的真实过程。拿到题先花2分钟读清楚输入输出格式和数据范围。我习惯在草稿纸上把样例手动推一遍确认自己对题意的理解没有偏差。比如上面那道蚂蚁搬家题样例里的包裹顺序是[3,2,2,4,1,4]K3那么载重上限设为4时每次最多搬4个单位第一只蚂蚁搬[3]、第二只搬[2,2]、第三只搬[4]、然后还有[1,4]装不下所以上限4需要4只蚂蚁不行当上限设为5时[3,2]、[2]、[4,1]、[4]需要4只蚂蚁还不够上限6时[3,2]、[2,4]、[1,4]刚好3只所以答案6。手动推样例的过程不只是验证边界还能帮你判断贪心策略在“当前这个样例”下是否合理。如果样例都推不通那代码写得再漂亮也没用。接下来就是写can_finish函数。我建议你把判断函数和二分主函数分开不要揉在一起。理由是笔试题目经常在第二个函数里打印中间调试信息如果混在一起调试起来非常崩溃。上面那段代码里我把两个函数分开写了这就是故意的。写到提交环节95%的可能你会遇到“超时”。超时的原因不是二分次数多而是你在判断函数里用了sum(w)之类的高开销操作。一个很小的优化是把w作为一个全局变量或者作为参数传入避免每次递归都拷贝整个数组。Python里如果w是list直接传引用就行但如果你不小心用了w[:]来切分复杂度直接翻倍。最后如果时间还剩15分钟我建议你先检查一下边界值K1时答案应该就是sum(w)K很大时答案应该是max(w)。这两个边界能快速验证你的二分区间是否正确。我每次都会用这两个极值来测至少能规避掉一半的边界bug。7. 蚂蚁编程题的备战清单与冷门技巧总结一下我对于蚂蚁编程题备战的心得。不一定是最全的但都是我实际操作下来觉得有效的经验。第一优先级字符串、双指针、滑动窗口。蚂蚁笔试的第一题几乎都是这一类快速解决第一题能给你后面争取大量时间。建议熟练到能在10分钟内AC一道中等难度的滑动窗口题。第二优先级动态规划。背包问题、最长递增子序列、编辑距离、网格路径这四类是最高频的DP考法。蚂蚁喜欢把它们包装成“蚂蚁采蜜”“红薯搬运”这种故事场景你要学会剥掉故事外壳。第三优先级图论。拓扑排序和最短路径是重点尤其是DAG的上层应用。写拓扑排序时注意用队列BFS而不是递归DFS因为递归在深度1万的时候会有爆栈风险这是笔试环境C和Java最常见的坑之一。第四优先级二分答案贪心。这类题看似简单但真正写好的人不多。建议去题库里搜“运送包裹”“分割数组的最大值”这类关键词集中刷10道左右就够应付了。还有一个冷门但是极实用的技巧把所有输入输出改成快速IO。如果你用C把cin/cout的同步关掉ios::sync_with_stdio(false); cin.tie(0);如果你用Python把input()换成sys.stdin.readline()。这在数据量达到10^5以上的时候能省下近一半运行时间经常是超时与AC的分界线。另外如果你不知道笔试环境里有没有代码补全建议平时就练习“无补全模式”。比如在文本编辑器里写代码不开自动提示锻炼自己手写标准库函数的能力。我见过太多人一关掉代码提示就连vector的push_back怎么拼都要想半天这种基本功在面试手写题环节非常吃亏。对于面经里常被提到的“蚂蚁开发板用什么烧录”“蚂蚁搬家智能车”这类硬件/嵌入式方向如果你投的是这类岗位编程题重点会偏向C语言、状态机、中断处理、IO操作这些而不是纯算法。但核心思路还是通的先把简化模型写清楚再逐步加条件。比如智能车走迷宫其实就是一个DFS/BFS寻路问题加上传感器噪声处理本质还是算法题。最后的一点个人体会把这些题和复盘整理出来的时候我又想起自己当年秋招那会儿的状态每天刷题到凌晨看到“蚂蚁”两个字就开始条件反射地心跳加速。但后来真的坐到考场上才发现编程题考的不只是你会不会写代码更是你在有限时间内能不能保持冷静、结构化地思考。我现在带新人或者帮学弟学妹模拟面试时最常讲的一句话是不要为了刷题而刷题每一道题都试着问自己三个问题——出题人想考我什么知识点这个知识点的边界条件在哪里如果把这个题目的背景从“蚂蚁”换成“银行转账”或者“外卖配送”我还能不能识别出同样的模型能把这三个问题回答清楚你拿到的就不只是一道题的答案而是一类题的解法的迁移能力。希望这份精选笔记能帮你少走一些弯路。有问题欢迎在评论区交流我看到都会回。祝秋招顺利。