华为机考100分题:滴滴预约单的区间调度贪心解法
2026/9/14 10:18:51 网站建设 项目流程

2025年1月7日华为秋招非AI方向机考,第一题是道100分的送分题:滴滴预约单。题目看着是出行场景,实际就是经典的区间调度贪心。我身边不少同学第一题反而没拿满,不是因为不会贪心,而是卡在输入解析和边界条件上,非常可惜。这篇文章把题目还原、思路推导、Java/C++/Python三套代码,还有我实际踩过的坑一次说清楚。准备通软、嵌软、测试、算法、数据科学方向的同学都适合看完后直接抄作业。

这道题从业务上看是滴滴司机接预约单,本质上考察的是最基础的贪心算法和排序能力。华为机考是ACM模式,需要自己处理输入输出,跟LeetCode那种核心代码模式不一样,所以不少人平时刷题没问题,一上机就栽在BufferedReader和Scanner的选择上。下面我把这道题从头到尾掰开揉碎讲一遍。

1. 真题背景与考点拆解

1.1 华为秋招机考的题型分布

华为的秋招机考一般是三道题,分数分布通常是100分、200分、300分,总分600分。第一题就是那个100分,难度定位是“基础题”,目的是筛掉完全写不了代码的人,而不是拉区分度。真正的区分度在第二题和第三题,但第一题的分数是保底,丢了非常可惜。

非AI方向(通用软件、嵌入式软件、测试、算法、数据科学)用的是同一套笔试题,考察的是通用的算法基本功和代码能力,不会专门偏向某个岗位方向。也就是说,不管你投的是嵌软还是数据科学,第一题都可能碰到这种看起来像业务题、实际上是经典算法模型的题目。

我见过太多人第一题翻车了,原因非常统一:不是不会做,而是不熟悉OJ(在线评测系统)的输入输出模式,或者边界条件没想清楚。比如本场这道“滴滴预约单”,有人把结束时间相同的订单排序方向搞反,有人忘了处理“开始时间等于上一单结束时间”的情况,还有人用Scanner读10万行数据直接超时。这些坑我都会在后面的章节里逐个拆开讲。

1.2 “滴滴预约单”到底在考什么

题目场景还原如下(依据常见真题形态整理):滴滴司机小张一天之内收到了N个预约订单,每个订单有开始时间start和结束时间end,时间单位是分钟,取值在0到1440之间。司机同一时刻只能服务一个订单,问小张这一天最多能完成多少个订单。

输入格式:第一行一个整数N,表示订单数量;接下来N行每行两个整数start和end。输出一个整数,表示最多能完成的订单数量。

这个题目包装了一层出行业务的外壳,脱掉外壳以后就是一个非常经典的“最多不重叠区间数量”问题。会议室预定、课程安排、任务调度全都是同一个模型。华为把这种经典模型套一个业务场景来出题,就是想看你能不能把实际问题抽象成算法问题,这比死记硬背模板要重要得多。

需要注意一个关键约定:如果订单A的结束时间等于订单B的开始时间,比如A是[1,3],B是[3,5],那这两个订单是可以连续接的。司机在3这个时刻已经服务完A,可以立刻开始服务B。所以判断两个订单是否冲突,要看的是next.start >= prev.end,而不是next.start > prev.end。这个细节我后面还会反复强调,因为它在代码里就是一行符号的区别,却是很多人的致命伤。

2. 解题思路与算法设计

2.1 把预约单抽象成区间模型

每个订单都可以表示成一个区间[start, end],由于end时刻订单服务结束,下一个订单可以在end时刻开始,所以区间实际上是左闭右开[start, end)。我们需要从一堆区间里选出尽量多的区间,要求它们两两不重叠。

排序是这类问题绕不开的第一步。但按什么排序?这是最容易纠结的地方。如果按开始时间排序,直观上感觉“开始早的订单先处理”,但这个直觉会出错。举个例子:订单A是[0, 100],订单B是[1, 2],订单C是[2, 3],按开始时间排序会先选A,结果只能接一单;但如果先选B再选C,能接两单。所以按开始时间排序是错的。

正确做法是按结束时间排序。这个逻辑可以用一句话解释清楚:一个订单结束得越早,它给后面留下的时间窗口就越大,优先选结束早的订单,能容纳后续订单的概率就更高。这也是贪心算法在区间调度问题上的标准策略。

推导到这里,整个题目的主框架就出来了:先把所有订单按end从小到大排序,然后遍历排序后的订单,维护一个变量lastEnd记录上一个已选订单的结束时间。如果当前订单的start大于等于lastEnd,就选中它,并把lastEnd更新为当前订单的end。

2.2 贪心策略的直观理解与正确性

很多人会问:贪心算法只是“感觉对”,怎么证明它一定对?这里给一个不太严谨但很实用的解释方向:交换论证法。假设存在一个最优解,它选的第一个区间不是所有区间里结束时间最早的那个区间,那么我们可以把这个最优解的第一个区间替换成结束时间最早的区间。替换之后,因为最早结束区间的end不会比原来第一个区间的end更晚,所以不会跟后面的区间产生新的冲突,而且收益数量不变。这样一步步替换下去,可以得到一个包含“结束时间最早区间”的最优解。不断对剩余区间重复这个过程,贪心选择方案就能达到最优解。

这个证明思路不用写在代码里,但它能帮你确认自己不会用错贪心。说实话,我在平时带人刷题的时候发现,很多人遇到区间类问题第一反应是排序+暴力枚举,再优化一点能想到贪心,但动手前从来不验证贪心的正确性。如果是在华为这种机考环境下,一道100分的题你不需要太深的证明,但至少要能举几个反例来验证自己的想法是否成立。

另外要提醒的是,这道题N的范围一般是1到100000,如果用暴力枚举所有组合,复杂度是指数级,直接不可行。排序的复杂度是O(NlogN),遍历一次是O(N),整体O(NlogN),在10万数据量下没有任何压力。这也是为什么第一题通常只会考到排序、贪心、简单的数据结构,而不会直接上复杂动态规划。

2.3 带金额的进阶版本:加权区间调度

如果你在牛客或者其他平台刷到过这道题的变体,可能会发现还有一版是每个订单带一个预估金额,问最多能赚多少钱。这题就完全不一样了,它不再是“最多能接几单”,而是“收益最大化”,贪心直接失效。

举个例子:订单A是[0, 10],金额100;订单B是[0, 1],金额1;订单C是[1, 10],金额1。如果按结束时间排序贪心,会选B和C,总收益2;但最优解是选A,收益100。贪心在这里就眼睁睁地错过最优解了。

带金额的场景需要用到加权区间调度,标准解法是动态规划加二分查找。先把区间按结束时间排序,令dp[i]表示前i个订单能获得的最大收益。转移时有两种选择:不选第i个订单,收益就是dp[i-1];选第i个订单,收益是当前订单金额加上“结束时间不超过当前订单开始时间”的前面所有订单的最大收益,这个位置用二分查找找到。复杂度依然是O(NlogN)。

我在本文的第3章会把不带金额的贪心版本用三种语言写出来,这是100分题的标准解法。带金额的版本我会给出一个Java参考实现,方便想深入一步的同学理解DP和二分如何结合。

3. 三种语言实现与代码逐行解析

3.1 Java 实现与输入优化

Java的实现在思路上很直接,重点是输入输出的写法。华为机考的数据量经常到10万行,用Scanner读取会比较慢,稳妥的做法是用BufferedReader加split切分。下面这版代码我建议直接背下来,它适用于绝大多数笔试环境。

import java.io.*; import java.util.*; public class Main { public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); int n = Integer.parseInt(br.readLine().trim()); int[][] orders = new int[n][2]; for (int i = 0; i < n; i++) { String[] parts = br.readLine().trim().split(" "); orders[i][0] = Integer.parseInt(parts[0]); orders[i][1] = Integer.parseInt(parts[1]); } // 按结束时间升序排序 Arrays.sort(orders, (a, b) -> a[1] - b[1]); int count = 0; int lastEnd = -1; for (int[] order : orders) { if (order[0] >= lastEnd) { count++; lastEnd = order[1]; } } System.out.println(count); } }

这里有几个细节值得说。lastEnd初始化为-1,是因为订单的start最小是0,当第一个订单的start>= -1时必然成立。如果你初始化成0,没问题,因为start>=0恒成立,但语义上不如-1清晰,尤其在修改边界条件的时候容易出错。比较器(a, b) -> a[1] - b[1]表示按数组第二列即end升序排列,千万不要写反成b[1] - a[1],否则排序结果全反了。

此外,如果你平时习惯用int a = Integer.parseInt(br.readLine())逐行读取,这题老老实实一行一行读也够用,不过用split(" ")切分时要注意一行开头结尾是否有空格。华为OJ的测试数据一般没有多余空格,但加一个.trim()是成本极低的保险操作,我建议保留。

3.2 C++ 实现与代码细节

C++的实现里,最常见的做法是用vector<pair<int, int>>存储订单,pair的first存start,second存end,排序时用lambda表达式按second升序排列。这里要特别提醒,sort默认按pair的first排序,所以如果直接sort(orders.begin(), orders.end()),你其实是按开始时间排序,结果就错了,必须自己写比较逻辑。

#include <bits/stdc++.h> using namespace std; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n; cin >> n; vector<pair<int, int>> orders(n); for (int i = 0; i < n; ++i) { cin >> orders[i].first >> orders[i].second; } // 按结束时间升序排序 sort(orders.begin(), orders.end(), [](const pair<int, int>& a, const pair<int, int>& b) { return a.second < b.second; }); int count = 0; int lastEnd = -1; for (auto& order : orders) { if (order.first >= lastEnd) { ++count; lastEnd = order.second; } } cout << count << endl; return 0; }

ios::sync_with_stdio(false);cin.tie(nullptr);这两行是C++选手在OJ上必须养成的习惯,它们能显著加快cincout的速度。不加这两行,在某些数据量较大的用例里可能会超时;加了以后性能和scanf/printf基本持平。第一次见这段代码的同学可能会疑惑为什么要这样写,简单说就是C++的输入输出流为了兼容C标准IO默认做了同步,关闭这个同步可以让输入输出更快。

还有一个细节是lambda表达式的参数建议用const pair<int, int>&,避免无谓的拷贝。虽然这道题的数据量不至于因为拷贝超时,但这是一个很好的代码习惯。最后,lastEndcountint就足够了,因为N最大10万,答案不可能超过N,不会溢出。

3.3 Python 实现与性能说明

Python最需要注意的是输入读取方式,直接用input()在循环里读10万行是可行的,但偏慢,稳妥做法是用sys.stdin.buffer.read()一次性读入全部数据再统一切分。这个技巧在牛客和华为OJ上都非常实用。

import sys def main(): data = sys.stdin.buffer.read().split() if not data: return n = int(data[0]) orders = [] idx = 1 for _ in range(n): start = int(data[idx]) end = int(data[idx + 1]) idx += 2 # 把end放在元组前面,排序时可以直接按end升序 orders.append((end, start)) orders.sort() count = 0 last_end = -1 for end, start in orders: if start >= last_end: count += 1 last_end = end print(count) if __name__ == "__main__": main()

这里最巧妙的地方是把(end, start)作为元组存储。Python元组排序默认按第一个元素升序,再把end放在第一位,这样一行orders.sort()就完成了按结束时间排序,不需要写key参数。如果你非要用(start, end)的格式,也可以写orders.sort(key=lambda x: x[1]),效果一样,但会多一次lambda调用。在大数据量下,直接把end放前面是更Pythonic也更快一点的做法。

按end取出来之后,遍历时用for end, start in orders解包,注意变量顺序对应的是元组里的end和start,写反了判断条件就错了。这是很多Python选手容易犯的低级错误。

3.4 三语言实现与核心差异

三份代码解决的是同一个问题,核心逻辑完全一样:按结束时间排序,遍历时维护lastEnd,选出start>=lastEnd的订单。差异主要集中在这几个方面:

语言输入优化方式排序写法常见风险
JavaBufferedReader + splitArrays.sort + 自定义比较器比较器方向写反
C++ios::sync_with_stdio(false)sort + lambdapair默认按first排序
Pythonsys.stdin.buffer.read().split()orders.sort() 配合元组结构调整解包时变量顺序写反

从我自己的刷题经验看,如果你擅长C++,笔试时用C++是最稳的,代码量短、性能好;Java的优势在于熟悉的同学多,但要注意输入输出别拖后腿;Python写起来最省事,适合快速验证思路,但在某些要求高性能的场景下要谨慎。好消息是这道题的复杂度是O(NlogN),Python完全扛得住,不会出现Python被卡常的情况。

如果答题时间充裕,我建议你用Python先跑一遍思路验证正确性,再用自己最熟练的语言写正式提交版本。这样做看起来多花了时间,实际上能帮你提前发现边界条件上的逻辑漏洞,反而省下反复提交试错的成本。

4. 在线测试与踩坑实录

4.1 边界条件:这些用例最容易翻车

我整理了几个我实际带人刷题时经常会遇到的测试用例。建议你把每份代码都跑一遍这些用例,对照期望输出检查自己的逻辑。

输入期望输出说明
1 对 start=0, end=5 的单个订单1最小规模,验证基本逻辑
订单依次为 [1,3] [3,5] [5,7]3后单开始时间等于前单结束时间,可连续接
订单依次为 [1,4] [2,3] [3,5] [4,6]2有部分重叠,贪心应选 [2,3] 和 [4,6] 或类似组合
所有订单都重叠,如 [1,10] [2,11] [3,12]1只能选一个
大量订单结束时间相同取决于开始时间排序稳定性不影响结果,但要注意遍历顺序

最后一种情况值得多说两句。比如订单是[1,5]、[2,5]、[3,5],结束时间都是5,排序后它们之间的顺序无所谓,因为只要选了第一个,lastEnd变成5,后面两个的start分别是2和3,都小于5,全部被跳过。结果还是1,不会受影响。所以你不需要操心同结束时间的订单内部如何排列,只要保证整体按结束时间升序即可。

4.2 常见错误与修正思路

我总结了几类常见错误,都是我在实际辅导中反复见到的,每条都对应真实的翻车场景。

第一类是排序方向错误。有人会想当然按开始时间排序,然后输出一个看起来很合理的答案。建议遇到区间调度问题,先默念三遍“按结束时间排序”,这个条件反射能救你不少分。

第二类是边界条件判断错误。判断条件写成order[0] > lastEnd而不是order[0] >= lastEnd,这样会漏掉“开始时间等于上一单结束时间”的可接订单。是否应该取等号,一定要在看题时确认清楚,不要凭感觉。

第三类是输入处理问题。Java的Scanner在10万行输入下会慢,Python的input()在循环里多次调用也会慢。这类问题平时在本地IDE跑完全测不出来,一上OJ就容易超时,所以从现在开始就养成用高速读入的习惯。

第四类是忘记处理N=0。虽然题目通常会说1 <= N <= 100000,但如果你在本地自测时敲一个空行,程序可能直接数组越界或者解析异常。建议在代码开头加一个if (n == 0) return;的防御性判断,成本极低,但能避免一些奇怪的运行时错误。

4.3 如何在在线OJ上验证你的代码

华为机考系统里没有本地调试器,你在编辑器里写完代码直接提交。这种情况下,最稳妥的验证流程是:先在本地把示例输入跑一遍,再跑几个自己构造的边界用例,最后再提交。很多人省掉第二步直接提交,结果就是反复编译错误、运行错误、答案错误,白白浪费提交次数。

在线OJ上遇到“答案错误”时,千万不要盲目改代码。第一步先检查自己的输出格式,会不会多了空格或换行;第二步检查排序和判断条件,用最小用例在草稿纸上手算一遍,对比程序输出;第三步再考虑算法思路是否正确。这三步能覆盖绝大多数错误场景。

我平时练习时还有一个习惯:写一个暴力解法作为对数器,随机生成小规模数据,把贪心解和暴力解的结果对比。虽然笔试时没有条件这么做,但平时用这个方法来验证贪心思路是否正确特别有效。等你练到对贪心模型足够熟悉,考场上一眼就能判断这类题能不能用贪心,自然就不需要每次都对数器验证了。

关于代码风格,华为OJ对Java主类名要求是Main,C++的main函数返回值必须是int,Python则没有特殊要求。这些细节看起来不起眼,真到考场上忘了就会直接编译失败,非常影响心态。建议在正式机考前,用牛客或者华为官方模拟环境完整走一遍流程,熟悉从打开题目到提交代码的每个步骤。

回到这道“滴滴预约单”,我个人在实际操作中的体会是:第一题最大的敌人不是算法,而是粗心。这题考的知识点你在任何一本算法书里都能找到,但能把边界条件、输入输出、排序方向全部处理对,才体现出真实的工程习惯。华为这类大厂的笔试,第一题拼的不是智力,而是稳定性和细节把控。平时刷题时多花30秒检查一遍边界条件,比考场上多试错三次要划算得多。

最后再分享一个小技巧:区间调度类题目,如果题目里出现了“最多能完成多少个”“最多能安排几场”这类描述,十有八九是贪心加排序;如果出现了“最大收益”“最大价值”这类描述,通常要往动态规划方向想。把这个规律记在脑子里,下次遇到类似的业务包装题,你就不会慌。

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

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

立即咨询