1. 两种模式到底在说什么
先抛结论:核心代码模式,就是平台已经帮你把输入读好了、输出检查也准备好了,你在编辑器里只需要写完那个函数体;ACM模式,则是把整个题目从标准输入到标准输出的链路全部交给你,数据要自己读进来,结果要自己打印出去,评测系统再对你的完整输出做比对。
一句话概括:核心代码模式只考算法逻辑,ACM模式额外考你处理输入输出的基本功。
这个区别,刷题刷得少的人可能感受不深。比如长期用LeetCode刷题,一上来就是class Solution,函数签名都给你摆好了,你只填中间那几行逻辑,提交完事,整个过程很舒服。但一旦到了某些公司的机试环节,题目打开一看,模板是空的,连main函数都要自己写,还得手写Scanner或者BufferedReader,如果平时根本没碰过这种模式,哪怕算法会,人也容易当场懵掉。这几年很多银行、国企、互联网大厂的技术岗笔试,以及华为OD这类外包岗机试,都明确要求ACM模式。两种模式都不陌生,才能稳着上考场。
1.1 核心代码模式:你只需要写完函数
核心代码模式的代表就是LeetCode。你打开一道题,页面左边是题目描述,右边是一个已经定义好参数和返回值的函数:
class Solution { public: vector<int> twoSum(vector<int>& nums, int target) { } };你不用操心数据是从哪来的,也不用管结果要怎么输出。平台会在后台悄悄调用你这个函数,把测试数据传进去,再把函数的返回值和标准答案做比对,一致就给过。
这种模式的本质,是把“算法题评测”做成了一次远程函数调用。你提交的代码对评测系统来说就是个黑盒子:入参传进去,出参返回回来,逻辑对了就通过。好的一面是写起来干净,适合一门心思搞算法、练思路,不用反复去做繁琐的IO体操。
坏的一面也很明显:长期只在这种模式下刷题的人,很容易产生一种错觉,以为写算法就是“写那几个函数”;但真实工程中,数据不会自己变成函数参数,结果也不会自动被交到别人手里,尤其在机试环境里,这些活儿全得你来。
1.2 ACM模式:从输入到输出全包
ACM模式这个名字源于ACM国际大学生程序设计竞赛,真实比赛里没有任何“模板”,一切从零开始。选手拿到题目之后,自己决定数据结构、自己写读取逻辑、自己写输出逻辑。评测系统会起一个进程跑你编译出来的程序,把你的输出和标准输出逐字符比较,差一个空格、差一个换行,都可能是 Wrong Answer。
举个例子,同样是“两数之和”这道题,ACM模式的题干会写得特别具体:
第一行输入一个正整数 n,表示数组长度;第二行输入 n 个整数,代表数组;第三行输入一个目标值 target。输出两个整数下标,中间用空格分隔。
那么你要提交的代码就长这样:
import java.util.Scanner; public class Main { public static void main(String[] args) { Scanner sc = new Scanner(System.in); int n = sc.nextInt(); int[] nums = new int[n]; for (int i = 0; i < n; i++) { nums[i] = sc.nextInt(); } int target = sc.nextInt(); Map<Integer, Integer> map = new HashMap<>(); for (int i = 0; i < n; i++) { int diff = target - nums[i]; if (map.containsKey(diff)) { System.out.println(map.get(diff) + " " + i); return; } map.put(nums[i], i); } } }看到没有,除了那几行算法核心逻辑,你还得额外处理:包名问题、类名问题、输入没给对怎么办、数组读取顺序是不是和题干一致等等。
很多基础不错但没练过ACM模式的同学,栽就栽在这层额外的“包装”上。
1.3 两种模式解决的其实是同一个问题
把话说透一点:两种模式不管怎么变,背后的算法需求是一样的。你在核心代码模式里会写的二分查找、滑动窗口、动态规划,到了ACM模式一样要会。核心差异只有两个点:数据怎么读进来、结果怎么交出去。
所以不要把它们理解成两套完全不同的武功,它们只是同一个“解题过程”的两种不同外壳。你核心代码模式写得溜,拿到ACM模式,算法逻辑一样能用;反过来,你ACM模式写得多,去面试写核心代码模式,反而会觉得更轻松,因为函数签名已经帮你处理掉了最烦人的输入格式问题。
真正该练的,是一眼识别出“这道题在核心代码模式下要反什么数、在ACM模式下要读什么数据”,然后用一种最不容易出错的方式把它写出来。下面我逐一展开。
2. 为什么刷题平台会同时保留这两种模式
很多同学会好奇:既然核心代码模式用起来这么舒服,为什么还会有平台坚持用ACM模式?为什么面试笔试的时候,那些公司偏偏要选看起来更麻烦的那种?这里面有历史原因,也有非常现实的用人考量。
2.1 核心代码模式的由来与好处
核心代码模式是随着LeetCode这类在线刷题平台兴起而普及的。它的目标用户是很明确的一群人:准备面试的人。面试考算法的时候,面试官想看的是你能不能快速想到解法、能不能把代码写干净,而不是看你浪费10分钟在那边处理输入输出。于是平台把IO全部封装起来,给你一个函数签名,你只要聚焦在算法本身。
这种模式在“刷题练习”和“面试模拟”这两个场景下特别好用。因为刷题高频场景是“一天刷好几道”,如果把时间都花在读输入上,人很快就会疲劳,效率也低。核心代码模式把每个题目的“坑”尽量留在算法层面,该考察你链表操作就考链表操作,该考动态规划就考动态规划,不会被无关细节干扰。
另外,从平台技术上来说,核心代码模式也更容易做自动判定。平台可以直接调用你提交的代码,把返回值拿过来比较;不需要启动完整进程、比较标准输出,判定效率和稳定性都更高。
2.2 ACM模式为什么到现在还没退场
那ACM模式为什么到现在还很常见?我觉得有三个原因:
第一,它更贴近真实竞赛和部分公司机试的传统。很多公司做技术笔试时,出的题本来就是从竞赛题改编过来的,题库沿用早期ACM/ICPC或蓝桥杯风格。直接把题目带数据格式迁移过来最省事,用核心代码模式反而要额外改造一遍,得不偿失。
第二,ACM模式能筛掉一部分“只背了模板”的人。这话说起来不好听,但实际筛选效果确实存在。有些候选人核心代码模式刷得滚瓜烂熟,但让他自己写个带标准输入输出的完整程序,他连BufferedReader为什么比Scanner快都说不出来,读一行字符串的时候还总被nextLine()的换行坑到。企业招人想找的是能写完整工程代码的人,不是只会写函数片段的人,所以更愿意用ACM模式来筛。
第三,一些特定领域的要求,比如华为OD机试、银行技术岗笔试,多年以来一直沿用ACM模式,已经形成了固定的出题风格和判题系统。改变成本高,双方(出题方和应试方)也都习惯了,所以短时间内很难被替代。
2.3 两种模式整体对比
| 对比维度 | 核心代码模式 | ACM模式 |
|---|---|---|
| 代表平台 | LeetCode | 牛客、华为OD机试、蓝桥杯、ACM竞赛 |
| 代码结构 | 只写类方法 | 完整可运行程序(class Main + main方法) |
| 输入处理 | 平台自动注入 | 自己从 stdin 读取 |
| 输出处理 | 平台代收返回值 | 自己精确控制 stdout 格式 |
| 刷题效率 | 高,聚焦算法 | 低,需额外处理IO |
| 面试还原度 | 偏向算法面试 | 偏向机试、工程素养考查 |
| 常见坑 | 对函数签名理解不到位 | 输入读取错误、输出格式错误 |
这张表值得存一下。每次遇到不同类型的题目环境,先想一想自己现在处于表格的哪一行,再决定写代码的重心放在哪里。
3. 同一道题用两种模式各写一遍
光说不练假把式。这里我拿一道很经典的“两数之和”变体,把两种模式完整走一遍。你最好跟着把代码在本地敲一遍,重点感受两种写法之间的差异。
3.1 题目描述
给出一个整数数组nums和一个目标值target,在数组中找出和为target的两个数,返回它们的下标。
核心代码模式版本:
给定一个整数数组
nums和一个整数目标值target,请你在该数组中找出和为目标值的两个整数,并返回它们的下标。你可以假设每种输入只会对应一个答案,返回任意顺序即可。
ACM模式常见变体版:
第一行一个正整数 n,表示数组长度;第二行 n 个整数,表示数组;第三行一个整数 target。输出一行,包含两个整数,分别是两个数的下标,中间用空格隔开,顺序无所谓。
注意,ACM版本里“n”必须自己读,这是和核心代码模式最大的区别。在LeetCode版本中,数组长度早就隐藏在nums.length里了,你根本不需要读。
3.2 核心代码模式写法
Java版本:
import java.util.HashMap; import java.util.Map; class Solution { public int[] twoSum(int[] nums, int target) { Map<Integer, Integer> map = new HashMap<>(); for (int i = 0; i < nums.length; i++) { int diff = target - nums[i]; if (map.containsKey(diff)) { return new int[]{map.get(diff), i}; } map.put(nums[i], i); } return new int[0]; } }核心代码模式下,这个方法写完就结束了。当你写return new int[]{map.get(diff), i};的时候,平台自动知道你要返回什么,它会拿这个数组和预期答案做对比。你的职责到 return 就为止了。就算本地你忘了写main,提交上去照样能过,因为评测端根本不跑你的main。
3.3 ACM模式写法
Java版本:
import java.util.HashMap; import java.util.Map; import java.util.Scanner; public class Main { public static void main(String[] args) { Scanner sc = new Scanner(System.in); int n = sc.nextInt(); int[] nums = new int[n]; for (int i = 0; i < n; i++) { nums[i] = sc.nextInt(); } int target = sc.nextInt(); Map<Integer, Integer> map = new HashMap<>(); for (int i = 0; i < n; i++) { int diff = target - nums[i]; if (map.containsKey(diff)) { System.out.println(map.get(diff) + " " + i); return; } map.put(nums[i], i); } sc.close(); } }Python版本:
import sys def main(): data = sys.stdin.read().strip().split() if not data: return idx = 0 n = int(data[idx]) idx += 1 nums = [] for _ in range(n): nums.append(int(data[idx])) idx += 1 target = int(data[idx]) seen = {} for i, num in enumerate(nums): diff = target - num if diff in seen: print(seen[diff], i) return seen[num] = i if __name__ == "__main__": main()注意Python这个写法:用sys.stdin.read()一次性把整个输入读进来,再统一分割处理。这比input()一行行读更稳,在数据量大的时候也更快。
ACM模式下,你的代码是一个完整的程序。评测系统会像你在命令行里执行java Main一样去运行它,把准备好的测试数据放到标准输入里,然后抓取你的标准输出做比对。稍有差池,整个程序可能直接报错,或者输出对不上。
3.4 输入边界情况一定要想清楚
这道题如果ACM模式给的n和你实际读到的数组元素数量不一致,会发生什么?假如第一行写了3,第二行只有2个数字,那么sc.nextInt()第二次的时候就会抛出异常。反过来,如果第一行写3,第二行给了4个数字,多出来的那个数字会被下一行读取逻辑吞掉,造成莫名其妙的结果。
这几乎是ACM模式新手最容易踩的坑。核心代码模式下,这类问题完全不存在,因为平台已经把入参封装好了,不可能出现“读多了”或“读少了”的诡异状态。
所以,ACM模式写多了,你会自然养成一个习惯:先看清楚输入格式有几行,每行几个数,用不用处理“多组测试用例”的情况,用不用处理字符串里的空格。这些看起来是“细枝末节”,但实际考试里,往往就是这些细节决定你是一遍过还是改到天荒地老。
4. 实战中最容易翻车的地方
现在进入本文最有价值的部分。我把这几年在网上、周围朋友以及自己刷题过程中遇到的高频翻车场景,一条一条整理出来。很多坑不是因为算法难,纯粹是模式切换不熟练造成的,非常可惜。
4.1 本地运行正常,切换到ACM模式就报错
最常见的表象:在IDE里用核心代码模式写好了方法,测试也测了,然后把方法复制到ACM模式的编辑器里,外面套了一个main,结果一跑就各种报错。
排查方向通常有三个:
第一,类名和包名。ACM模式的判题系统一般要求public class Main,类名必须精确。你要是复制代码的时候顺手把原来的class Solution留着,或者文件名和类名不一致,编译直接失败。Java尤其严格,带package声明也是大忌。
第二,函数签名发生变化。核心代码模式的函数签名是由平台定死的,比如public int[] twoSum(int[] nums, int target);但ACM模式里,函数名、形参名都没人管你,重要的是你自己main里面别把数据的顺序读错。最常见的错误是:先读target再读数组,或者是把n和数组元素搞混。
第三,运行方式差异。你在IDE里点击运行,IDE可能已经自动帮你处理了工作目录、环境变量、编码格式;到了OJ环境,所有东西都是裸的。最简单也最有效的做法是:在本地自己开一个命令行终端,手动编译运行一次,把输入用管道喂进去,看输出是否符合预期。这能模拟出80%的OJ环境。
4.2 读字符串最容易踩的坑
next()和nextLine()的区别,每次说完都有人拍大腿。
next()读取下一个被空白字符分隔的“单词”,遇到空格、Tab、换行就停。nextLine()读取一整行,直到遇到换行符。
问题出在混用。比如你先调nextInt()读了整数,紧接着调nextLine()想读一行字符串,结果往往拿到的是空字符串。为什么?因为nextInt()读到整数后,光标正好停在“换行符之前的空白”位置,nextLine()读到的就是那个残留的换行符,直接返回空行。
解决办法有三个:
- 在
nextInt()后面再补一个nextLine(),把这个残留换行消费掉; - 全部用
nextLine()读进来,再手动用Integer.parseInt转换; - 避免在同一个程序里混用这两类方法,能统一就统一。
第三种是最省心的。很多老选手的习惯是:要么全程Scanner的next系方法处理所有输入,要么干脆用BufferedReader配合split一次性处理所有行,根本不给混用留机会。
4.3 输出格式和空白字符的处理
ACM模式的输出要求严到让人抓狂。少一个空格、多一个换行、末尾多一个Tab,都可能导致Wrong Answer。最经典的是“这是道要输出n行的题,但中间某行结尾多了一个空格”。
我的一个笨办法是:如果题目要求“输出每个结果占一行”,那我就用StringBuilder把结果拼好,最后统一输出,最后再加一个彻底trim()或者精确控制分隔符。例如要求输出数字之间用空格分隔,我就写成:
StringBuilder sb = new StringBuilder(); for (int i = 0; i < n; i++) { if (i > 0) { sb.append(" "); } sb.append(arr[i]); } System.out.println(sb);这样永远不会出现最后一个元素后面多加空格的问题。
还有一类题是“输出结果也可能是一个特殊符号”,例如-1表示找不到,那么你就老老实实输出那一个数字就好,不要在中间穿插任何调试信息。调试的时候可以System.out.println随便打,但提交前一定要把调试输出删干净。
另外提醒一句:不管代码里是System.out.println("答案是" + ans)还是print(ans + "\n"),OJ都只看最终标准输出。很多人自查半天没发现问题,最后发现是调试信息没删干净,白白浪费好几次提交机会。
4.4 高效读入:Scanner还是BufferedReader
如果是几十上百个数的输入,Scanner完全够用。但笔试里偶尔会出现那种丧心病狂的数据规模:比如读入10万个整数,再做查询操作。这时候Scanner就非常吃力了,可能会超时。而使用BufferedReader一次性读入,再用split解析,速度会有很明显的提升。
import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; public class Main { public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); String line; while ((line = br.readLine()) != null) { String[] parts = line.trim().split("\\s+"); // 处理 parts } br.close(); } }经验法则:除非题目数据量特别大,比如n到了10^5甚至10^6级别,或者循环查询次数多到离谱,否则Scanner也够用。但如果真的对性能没把握,直接用BufferedReader一定更安全。只是要注意抛异常和资源关闭的问题,别忙中出错。
Python这边也是同理,能用sys.stdin.read()就别用input()逐行读。input()在数据量大时慢得离谱,一次性读入再拆分是更好的方案。
5. 两种模式都要稳,日常怎么练
想笔试不慌,光看文章没用,关键还是得练。但练也有练的方法。我是建议“两条腿走路”:核心代码模式保持手感,ACM模式定期熟悉,不要等到考前几天才临时抱佛脚。
5.1 刷题时的练习组合
如果你平时用LeetCode刷每日一题,我的建议是:每周至少挑两到三道题,手动改写成ACM模式,在牛客网的在线笔试系统或者自己的IDE里跑一遍完整的输入输出。
具体做法是:
- 从LeetCode找一道题,先按核心代码模式把解法写出来;
- 看题解的“输入输出示例”,自己设计ACM模式的输入格式;
- 新建一个
Main类,把解法逻辑改造成从标准输入读取、标准输出打印的完整程序; - 用几个样例自测,包括空数组、边界值、大数、重复元素等;
- 有条件的话,拿到一个支持自定义输入的OJ上提交一遍,看是否通过。
这个流程大概每次多花15分钟。但坚持两周后,你对ACM模式的恐惧会明显消失,因为你已经形成了固定的“模板记忆”。
我自己常用的一组固定模板是这样的:先写好read input的部分,再写好solve逻辑,最后是output。这样就算题目变了,模板骨架不动,只需要改中间的处理逻辑。
import java.util.*; public class Main { public static void main(String[] args) { Scanner sc = new Scanner(System.in); while (sc.hasNext()) { // 1. 读入 int n = sc.nextInt(); int[] arr = new int[n]; for (int i = 0; i < n; i++) { arr[i] = sc.nextInt(); } // 2. 计算 int result = solve(arr); // 3. 输出 System.out.println(result); } sc.close(); } static int solve(int[] arr) { // 算法逻辑 return 0; } }这套“读入-计算-输出”三段式,是ACM模式最稳的结构。遇到复杂输入格式多改几行模板就好,核心逻辑永远集中在solve里。
5.2 笔试机试前的环境准备
考前一定去目标公司或平台模拟一遍笔试环境。有些平台的IDE是不带代码补全的,或者编译用的JDK版本和你本地不一样。曾经有人因为本地是JDK 17,用了List.of()等新特性,考试平台是JDK 8,一提交编译直接失败。这种事最冤。
我还建议你养成一个习惯:写ACM代码时,类名统一用Main,不要加包名,不要写package。所有封装的输入逻辑都自己写,不依赖任何第三方库。有些企业笔试环境比较简陋,除了标准库什么都不给,你要是有奇奇怪怪的依赖,肯定跑不过去。
另外,提前准备好几段速查模板,比如:读取单行整数数组、读取多行不定长整数、读取字符串数组、输出带分隔符的一行结果。这些片段就像英语作文里的万能句式,上了考场直接改数据就行,非常省时间。
5.3 考试时的策略与心态
ACM模式笔试通常题目多、时间紧,别按顺序硬刷,先花两分钟把所有题都看一遍,按难度排序。
第一原则:先把第一题做出来。笔试的第一题通常是最简单的,只要能AC,心态稳一半。第二原则:遇到读半天没读明白的输入格式,直接跳过,先做后面的;不要在一道题上浪费超过20分钟。
第三原则:如果算法思路不完整,先把输入输出写对,拿部分分。很多OJ是“分点得分”的,你输出格式对了、样例能过,也能捞到一点分。最怕的是那种“代码写了一半,卡在输入读取上,连样例都跑不过”的局面。所以随手把分段读入、分段处理的结构学好,真的很值。
还有一点:提交前务必看清题目的输出说明。让输出索引从0开始还是从1开始,排序要求是升序还是降序,是不是要处理多组测试用例直到EOF。这些坑,比算法bug更致命。
6. 常见问题速查
这一节整理几个关于两种模式的高频疑问,遇到类似情况可以快速查阅。
6.1 核心代码模式刷多了,会不会导致ACM模式写不动代码
很多人担心,长期用LeetCode刷题,是不是会让自己的工程编码能力退步。我的看法是:不至于,但确实会“手感生疏”。核心代码模式把输入输出封装掉了,你长期不碰IO操作,真到了要自己写的时候,速度会慢,容易出小错。解决办法就是上面说的,每周找个两三天把题改写成ACM模式练一遍,保持手感。核心代码模式本身不是问题,问题是你有没有刻意去做模式切换的练习。
6.2 遇到没有给测试用例数量的输入,怎么处理
有一种输入格式是:有多行数据,但没告诉你到底有几行,读到文件末尾为止。这时候千万别想着for (int i = 0; i < n; i++),得用while (sc.hasNext())或者while ((line = br.readLine()) != null)。
例如题目要求:每行输入两个数a和b,输出它们的和,直到输入结束。
Scanner sc = new Scanner(System.in); while (sc.hasNext()) { int a = sc.nextInt(); int b = sc.nextInt(); System.out.println(a + b); } sc.close();这种写法要牢记,它是ACM模式的一个特高频考点。
6.3 我应该用哪种语言应对ACM模式更好
就我接触的经验,Java和C++在ACM模式下都很常见,Python现在也越来越多地被许多机试平台接受了。选语言的核心标准不是“哪个最强”,而是“哪个你写得更熟、更不容易出错”。
如果你已经对Java比较熟,建议继续用Java。Java的Scanner虽然比C++的cin慢,但非极限数据完全够用;用BufferedReader就能覆盖大数据场景。C++的效率高,但指针和内存管理的坑也更多,平时不熟的话别临时换。Python写起来最快,但如果你对sys.stdin.read()和列表推导式不熟悉,反而容易写出效率极低的代码。一句话:别在考场上换枪。
最后说点实在的。我自己最早也是只刷核心代码模式,第一次参加某平台机试时,光输入输出格式就折腾了半天,最后一道很有把握的题都没写完。后来学乖了,每次刷题都顺手想想“这题如果改成ACM模式,输入该怎么读”,慢慢就形成了条件反射。现在看到任何一道题,第一反应不再是算法代码本身,而是先把数据流在脑子里过一遍:入口在哪、出口在哪、中间要跑什么逻辑。这个习惯,我觉得比单纯记住“两种模式的区别”更有用,也建议你尽早养成。