华为OD机考的双机位C卷里,测试用例执行计划这道题最近出现的频率相当高,Java、Python、JS、GO、C++、C这六种语言在考场里都有人选。题目本身不复杂,核心就是一个带优先级的排序问题:按测试用例优先级从高到低排,优先级相同再按编号从小到大排。但越是这种基础题,越容易在输入输出处理、比较器写法、多组输入这几件事上翻车,我见过不少人在“送分题”上因为一个小细节丢分,非常可惜。
这篇内容就把这道题从题目原型、六种语言实现、排序原理到踩坑点全部展开,适合正在准备华为OD机考的同学,也适合想练ACM模式排序题的人。无论你主力语言是Java还是Python,或者打算用JS/C++/GO/C,这篇都能直接抄作业。我尽量把代码写完整,把“为什么这么写”也讲透,而不是只贴一段能过样例的代码就完事。
1. 题目拆解:C卷里这道题到底在考什么
1.1 华为OD机考C卷的出题特征
华为OD机考不是传统意义上的性格测试或者行测,它考的是纯算法上机,用ACM模式写代码,就是自己处理标准输入输出,而不是像力扣那样直接实现一个函数。C卷是最近一个批次的题目集合,整体难度分布比较稳定,通常有三道题,第一题偏简单,第二题偏排序和字符串处理,第三题偏动态规划和贪心。测试用例执行计划这种题就经常出现在第二题的位置,属于“拉开分数差距前必须先拿稳”的那一类。
从通过率数据来看,这类题其实卡住不少人的地方反而不在排序逻辑,而在三点:一是没有处理多组输入,二是比较器写反了方向,三是自测没问题但提交后格式不对。所以后面我会花不少篇幅讲输入输出和格式细节,这部分才是真正的实战经验。
1.2 测试用例执行计划的题目原型
题目描述大概是这样:在软件测试中,每天只能执行一个测试用例,现在有n个测试用例,编号从1到n,每个用例有一个优先级。需要按照优先级从高到低确定执行顺序,如果两个用例优先级相同,则编号小的先执行。输入第一行给n,第二行给n个整数表示每个用例的优先级,输出一行,是排好的用例编号顺序,用空格分隔。
举个例子,输入:
code复制5
1 3 3 2 5
这里5个用例的优先级分别是1、3、3、2、5,按优先级降序排,编号5的优先级5最大排第一,编号2和3的优先级都是3,编号2更小所以排前面,接着是编号4优先级2,最后是编号1优先级1,输出就是:
code复制5 2 3 4 1
这个例子我建议你在看任何语言的代码之前先自己手推一遍,把“排序键是什么”搞清楚。排序是先看优先级,再看编号,而且两个方向不一样,优先级是降序,编号是升序。这就是所谓多关键字排序,也是这道题唯一的算法考点。
1.3 双机位监考环境下的答题策略
双机位意味着考试时一个摄像头对着你,另一个手机放在侧后方监控你的屏幕和操作,全程不能切屏、不能查资料、不能打开本地笔记。这意味着平时不背模板的话,考场现想IO代码会非常痛苦。尤其是JS的readline、GO的bufio.Scanner、C的qsort比较函数,这些如果不熟,思路再清晰也会卡在环境搭建上。
我建议的策略是:主攻两门语言,一门用于稳过,一门用于备用。比如Java主力,Python查缺补漏。剩下四种语言至少要能看懂、能讲清思路,因为万一抽到的题目你熟悉的语言有某个边界问题,换语言写反而更快。后面给六种语言代码,也是为了方便你在考场上“哪里不会切哪里”。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 六种语言实现方案与核心代码
2.1 Java:Comparator比较器实现
Java在OD机考里的出场率一直很高,主要是因为很多人的第一语言就是Java,而且Java的集合类写排序很顺手。这道题的关键是定义TestCase类,然后用Collections.sort加自定义比较器。
java复制import java.util.*;
import java.io.*;
public class Main {
static class TestCase {
int id;
int priority;
TestCase(int id, int priority) {
this.id = id;
this.priority = priority;
}
}
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String line;
while ((line = br.readLine()) != null) {
int n = Integer.parseInt(line.trim());
String[] parts = br.readLine().trim().split(" ");
List<TestCase> list = new ArrayList<>();
for (int i = 0; i < n; i++) {
list.add(new TestCase(i + 1, Integer.parseInt(parts[i])));
}
Collections.sort(list, (a, b) -> {
if (a.priority != b.priority) {
return b.priority - a.priority;
}
return a.id - b.id;
});
StringBuilder sb = new StringBuilder();
for (int i = 0; i < n; i++) {
if (i > 0) sb.append(" ");
sb.append(list.get(i).id);
}
System.out.println(sb);
}
}
}
这里有个细节:优先级的差可能为负数,所以用 b.priority - a.priority 表示降序没问题,只要优先级取值范围不会导致int溢出就行。如果担心溢出,可以用 Integer.compare(b.priority, a.priority),这个更稳。比较器的方向别搞反,a.priority != b.priority 时返回正数说明a排后面,所以b.priority大于a.priority时返回正数,正好实现降序。
另外,我建议用BufferedReader而不是Scanner。原因很简单,Scanner虽然写起来方便,但遇到n到1万甚至10万的数据量时,性能差距就出来了,BufferedReader更抗造。用一个StringBuilder拼输出,避免频繁println造成不必要的IO开销。
2.2 Python:lambda多关键字排序
Python写这种题代码量最小,逻辑也最直白。核心就是构造一个(id, priority)的列表,然后用lambda指定排序规则:priority降序、id升序。注意Python的sort是稳定的,而且支持多关键字,用元组 (-priority, id) 直接搞定降序和升序两个方向。
python复制import sys
def solve():
data = sys.stdin.read().split()
if not data:
return
idx = 0
while idx < len(data):
n = int(data[idx])
idx += 1
priorities = list(map(int, data[idx:idx + n]))
idx += n
cases = [(i + 1, priorities[i]) for i in range(n)]
cases.sort(key=lambda x: (-x[1], x[0]))
print(" ".join(str(case[0]) for case in cases))
if __name__ == "__main__":
solve()
这里用 sys.stdin.read().split() 一次性读入所有token,再用索引切分每组数据,好处是不用担心多组输入时行尾换行、空行这些脏数据。lambda表达式里的 -x[1] 是实现“优先级降序”的关键技巧,因为默认是升序,取负后相当于升序排负值,那就是原来的降序,第二个元素 x[0] 保持升序。
如果你在本地跑的时候拿到的题目是单组输入,那这个代码也兼容,它会正常处理一组就结束。Python的性能在数据量小的时候无所谓,但n超过10万时注意输出别用循环println,用join拼字符串,这点对Python尤其重要。
2.3 JavaScript:readline处理ACM输入
JS在华为OD机考里能用,但很多人在输入输出上栽跟头。因为Node.js的readline是事件回调式的,不像Java和Python那样同步读取,很多人第一次写不习惯。下面是直接在OD机考环境里能跑的完整代码:
javascript复制const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
let lines = [];
rl.on('line', (line) => {
lines.push(line.trim());
}).on('close', () => {
let idx = 0;
while (idx < lines.length) {
const n = parseInt(lines[idx++]);
const parts = lines[idx++].split(' ').map(Number);
const cases = parts.map((p, i) => ({ id: i + 1, priority: p }));
cases.sort((a, b) => {
if (a.priority !== b.priority) return b.priority - a.priority;
return a.id - b.id;
});
console.log(cases.map(c => c.id).join(' '));
}
});
这段代码的思路是把所有输入先存到lines数组里,等输入结束后再统一处理。这样避免了在回调里写复杂逻辑,逻辑清晰很多。sort里的比较函数,优先级不相等时返回 b.priority - a.priority,这是降序;相等时返回 a.id - b.id,这是编号升序,和Java的Comparator完全一个套路。
JS的一个坑是数组的map方法执行时下标从0开始,所以id是i+1。另外,输出用 console.log 默认带换行,如果题目要求行末不能有多余空格,用join拼好的字符串就不会有多余空格,这个天然满足要求。
2.4 GO、C++、C三件套实现
GO在OD机考里可选,但用的人相对少。主要问题是对bufio.Scanner不熟的人容易在输入上卡壳。这里给一个完整实现:
go复制package main
import (
"bufio"
"fmt"
"os"
"sort"
"strconv"
"strings"
)
type TestCase struct {
id int
priority int
}
func main() {
scanner := bufio.NewScanner(os.Stdin)
scanner.Scan()
for scanner.Text() != "" {
n, _ := strconv.Atoi(scanner.Text())
scanner.Scan()
parts := strings.Fields(scanner.Text())
cases := make([]TestCase, n)
for i := 0; i < n; i++ {
p, _ := strconv.Atoi(parts[i])
cases[i] = TestCase{id: i + 1, priority: p}
}
sort.Slice(cases, func(i, j int) bool {
if cases[i].priority != cases[j].priority {
return cases[i].priority > cases[j].priority
}
return cases[i].id < cases[j].id
})
for i, c := range cases {
if i > 0 {
fmt.Print(" ")
}
fmt.Print(c.id)
}
fmt.Println()
if !scanner.Scan() {
break
}
}
}
GO的sort.Slice用less函数,返回值表示i是否排在j前面,这个和Java的Comparator相反,别搞混。这里 cases[i].priority > cases[j].priority 表示优先级高的排前面,即降序。字符串转数字用strconv.Atoi,记得处理错误。注意GO的scanner默认按行读取,输入最后一行没有换行也能正确结束。
C++的写法和Java很像,用结构体加lambda比较器,但注意C++的sort不是稳定排序,所以不要依赖稳定性来保持编号顺序,而是直接把“(priority降序, id升序)”这个二元组规则写进比较器。
cpp复制#include <bits/stdc++.h>
using namespace std;
struct TestCase {
int id;
int priority;
};
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
while (cin >> n) {
vector<TestCase> cases(n);
for (int i = 0; i < n; i++) {
int p;
cin >> p;
cases[i] = {i + 1, p};
}
sort(cases.begin(), cases.end(), [](const TestCase& a, const TestCase& b) {
if (a.priority != b.priority) return a.priority > b.priority;
return a.id < b.id;
});
for (int i = 0; i < n; i++) {
if (i > 0) cout << " ";
cout << cases[i].id;
}
cout << "\n";
}
return 0;
}
C语言实现本质上一样,只是用qsort加函数指针比较器。因为C语言没有vector和lambda,所以结构体数组用固定大小,或者动态malloc都行。OD机考的n一般不超过1000,用静态数组就足够,我自己习惯开 TestCase cases[1005],稳妥。
c复制#include <stdio.h>
#include <stdlib.h>
typedef struct {
int id;
int priority;
} TestCase;
int cmp(const void* a, const void* b) {
TestCase* ta = (TestCase*)a;
TestCase* tb = (TestCase*)b;
if (ta->priority != tb->priority) {
return tb->priority - ta->priority;
}
return ta->id - tb->id;
}
int main() {
int n;
while (scanf("%d", &n) == 1) {
TestCase cases[1005];
for (int i = 0; i < n; i++) {
int p;
scanf("%d", &p);
cases[i].id = i + 1;
cases[i].priority = p;
}
qsort(cases, n, sizeof(TestCase), cmp);
for (int i = 0; i < n; i++) {
if (i > 0) printf(" ");
printf("%d", cases[i].id);
}
printf("\n");
}
return 0;
}
C的qsort比较函数返回值的含义是:负数表示a排在b前面,正数表示a排在b后面。所以优先级比较时写成 tb->priority - ta->priority,这里和Java的Comparator是一致的。
3. 排序原理与性能分析:为什么这么写稳过
3.1 多关键字排序的底层逻辑
这道题的多关键字排序,本质上是“优先级”做主关键字,“编号”做次关键字。很多第一次接触的同学会想:能不能先按优先级排一遍,再按编号排一遍?答案是如果你两次排序都做了,第二次排序会把第一次的结果打乱,因为你没有把优先级作为次级条件保留。正确做法是像上面代码那样,在一个比较器里同时判断两个条件。
有个非常容易理解的生活化类比:想象你在整理一摞试卷,先按科目分类,再按分数从高到低排。如果你先按科目排好,再单独按分数排一次,整个顺序就乱了。正确做法是:先看科目,科目相同再看分数。这就是多关键字排序的直觉。
在代码层面,就是比较器里先写优先级的比较逻辑,如果优先级相等,再写编号的比较逻辑。六种语言里,Java用Comparator、Python用lambda元组、C++/GO用less函数、C用回调函数,本质上都是同一个规则,只是语法不同。
3.2 复杂度分析与数据规模预估
这道题的时间复杂度是O(n log n),空间复杂度O(n)。把n个用例读入数组,排序,输出,这是最优的思路。如果你想到“每天选一个优先级最高的”,用暴力法每天遍历剩下的用例找最大值,那复杂度是O(n^2),在n=1000时尚可接受,n=10000时开始吃力,n=100000时基本跑不过。
华为OD机考的数据范围一般不会太夸张,n通常在一万以内,O(n log n)完全够用。但你在写代码的时候要有预估数据规模的意识。从输入格式看,第一行给出n,第二行给n个整数,如果n没有明确限制,就按最坏情况来设计,使用排序解法无论如何都够。
关于输出格式,题目要求一行输出编号,用空格分隔。这里有一个容易被忽略的点:行尾允不允许有多余空格?严格来说大多数判题系统会忽略行尾空格,但为了保险,最好在循环里判断:当前元素不是最后一个时才输出空格。上面所有代码都用了“第一个元素前面不加空格,后面每个元素前加空格”的写法,这个思路是最稳的,因为不管行尾是否允许空格,都不会出错。
3.3 输入输出细节为什么是送分点
ACM模式的核心在于“自己处理输入输出”,很多人力扣刷习惯了,切到ACM模式就不适应。测试用例执行计划这道题,输入只有两行,写起来不难,但它代表了一类模板:多组输入。OD机考的判题数据通常包含多组测试用例,就是那种输入文件里可能连续放着好几份n+优先级的数据,你的程序需要循环处理直到文件结束。
Java的 while ((line = br.readLine()) != null)、Python的 sys.stdin.read().split() 配合索引切分、C++的 while (cin >> n)、C的 while (scanf("%d", &n) == 1),都是在处理这个“直到文件结束”的逻辑。如果你只写单组输入,自测样例能过,提交后大概率部分用例会报错,因为判题系统会把多组数据一次性灌进你的程序。
4. 高频踩坑与排查实录
4.1 多组输入的经典坑
我见过最典型的错误是:只处理一组数据就return了。这种代码在自己电脑上跑样例没问题,因为样例确实只有一组。但提交后系统会跑多组数据,你的程序只处理第一组就退出,后面全丢。处理方式就是用循环读,直到EOF。
还有一个相关坑是:读完了第一行的n,但没有处理“可能有多余空白行”的情况。比如输入文件里第一组数据和第二组数据之间有空行,如果你用逐行读取且没有跳过空行的逻辑,可能把空行当成整数导致parse错误。这里我建议优先用 split() 或者 cin >> n 这种按token读取的方式,天然跳过空白,不依赖行结构。
4.2 编号与排序方向
编号到底从0开始还是从1开始,题目里明确写的是从1到n。但代码里数组下标从0开始,所以构造对象时要用 i + 1。很多人这里会写错,输出从0开始,导致全错。
排序方向也是重灾区:优先级降序、编号升序,很多人在紧张状态下只记得“按优先级排序”,忽略了相同优先级还要按编号排。如果忽略第二个关键字,排序结果就会出现:输入 1 3 3 2 5,输出 5 3 2 4 1,但正确答案是 5 2 3 4 1。这种错误在自测时不容易发现,因为很多样例里恰好没有重复优先级。
4.3 输出格式与宿主环境差异
关于输出,两个高频问题:一是忘记分隔符,把编号连在一起输出成 52341;二是用了错误的分隔符,比如用逗号而不是空格。题目明确要求空格分隔,join的时候用空格字符串。
另一个环境差异问题:本地Node.js环境里console.log输出到终端,判题系统里console.log输出到管道。如果你没有调用 rl.close(),进程可能不会退出,导致超时。所以一定记得在输入读完的close回调里输出结果。GO和C++如果不开 ios::sync_with_stdio(false),在大数据量下可能因为IO同步变慢,虽然这道题数据量一般不大,但习惯养成后对其他题也有好处。
4.4 各语言易错操作速查表
| 语言 | 易错点 | 正确姿势 |
|---|---|---|
| Java | 比较器方向写反 | priority不等时返回 b.priority - a.priority,相等返回 a.id - b.id |
| Python | 多组输入处理 | 用 sys.stdin.read().split() 按token读取,配合索引切分组 |
| JavaScript | readline回调逻辑混乱 | 先存lines数组,close后统一处理 |
| GO | sort.Slice的less方向 | less返回true表示i排前面,注意和Comparator相反 |
| C++ | 忘记同步关闭 | 加 ios::sync_with_stdio(false) 和 cin.tie(nullptr) |
| C | qsort比较函数返回值 | 返回正数表示a在b后面,优先级降序用 tb->priority - ta->priority |
这张表我建议考前扫一眼,每一行都是真实考场上容易出问题的地方。
5. 备考建议与个人经验
5.1 我的备考心得
这道题我在多个批次的机考题目里都见过类似版本,变化的只是背景描述,比如“任务调度”“作业排序”,核心都是多关键字排序。所以我把这种题定位为“必须拿满分的题”。按我的经验,刷题的时候不要只刷会做的,而是把每种语言的IO模板练到闭着眼能写出来。考试时时间紧张,如果你还在想 Scanner 还是 BufferedReader 快,说明基本功还没有内化。
我在实际练习中踩过的坑是:一开始只刷力扣,习惯函数式输入,后来做OD机考模拟题时第一题就卡在输入上,那道题本身很简单,我却花了十几分钟调输入。后来我养成了一个习惯:每道题都先用自己熟悉的语言写AC,再用第二语言写一遍,第三语言写一遍,直到六种语言都能5分钟之内敲完模板。熟练之后,不管考场抽到什么题,我至少有三种语言可以随时切换,不会再被语言细节拖后腿。
5.2 后续扩展方向
测试用例执行计划这个模型还可以扩展到很多变体,比如给每个测试用例加一个执行耗时,让你安排执行顺序使得总等待时间最小,这就变成贪心里的调度问题了;或者给每个用例加一个截止时间,让你判断能不能全部按时完成,这就变成排序加贪心判定。掌握了多关键字排序之后,这些都是顺手的事。
最后再分享一个小技巧:考试前把这道题的六种语言代码各手写一遍,不要复制粘贴,而是真的在编辑器里敲。手写能暴露所有你以为自己会但实则不熟的地方,比如GO的scanner.Scan循环、JS的readline事件绑定、C的typedef结构体写法。这些细节在考场上就是决定你能否顺利AC的分水岭。
