☰
华为OD机考区间连接器全攻略:排序合并与贪心匹配
2026/10/14 6:16:00 网站建设 项目流程

如果你最近在刷华为OD机考双机位C卷的题库,“区间连接器”这道题大概率会出现在你的视野里。它的题干描述不复杂:给你一堆区间和一堆长度不等的连接器,问能不能用这些连接器把区间之间的空隙全补上,让整条坐标轴上的区间最终连成一片。这道题并不需要背复杂数据结构,也没有高深的算法模板,但它把“排序、区间合并、gap计算、贪心匹配”这四件事揉在了一起,恰恰是最能拉开分差的中等题。更关键的是,机考环境支持的语言很多,Java、Python、JS、Go、C++、C都能写,同一个思路换语言实现时却有不少小坑。这篇文章就把题意、完整推导、六种语言的实现、考场细节和容易翻车的边界一次讲透,备考的同学可以直接按这套模板走。

1. 先把题意钉死:输入输出约定和两个版本差异

1.1 我按哪个版本写

“区间连接器”在不同批次的机考里出现过微小变体,流传版本之间主要差在最后要输出什么。我先统一用一个最常规、也最适合做模板的版本:

第一行输入一个整数 n,代表区间的个数;接下来 n 行,每行两个整数 l、r,表示一个闭区间 [l, r]。再输入一行整数 m,代表连接器的个数;最后一行输入 m 个整数,代表每个连接器的长度。

输出要求:如果能够使用连接器把所有的区间连成一个整体(区间之间不能再有缺口),输出最少需要使用的连接器数量;如果无法全部连通,输出 -1。

这里的“连接器”是一个很形象的比喻。两个相邻区间之间的空隙如果大小是 gap,那么一个长度大于等于 gap 的连接器就能把这个空隙补上,使两个区间连起来。每个连接器只能用一次,而且一个空隙只能放一个连接器。

有个变体会把最后问题改成“输出最多能连通多少个区间段”或“在使用连接器数量最少的情况下,最多能接上几个缺口”,这种版本不会改变核心算法,只改变收尾逻辑。我后面在章节 6.1 里单独说怎么切换,正文的代码统一按输出 -1 的版本来。

1.2 一个能直接算出来的最小例子

先手工走一个最简单的输入:

输入:

3 1 2 4 5 7 8 3 1 2 2

区间是 [1,2]、[4,5]、[7,8]。可以看出第一个区间和第二个区间之间少了整数点 3,缺口大小是 4 - 2 - 1 = 1;第二个区间和第三个区间之间少了整数点 6,缺口大小是 7 - 5 - 1 = 1。两个缺口都必须补上,每个缺口必须单独占用一个连接器。连接器长度分别是 1、2、2,长度 1 的补第一个缺口,长度 2 的补第二个缺口,答案就是 2。

这里的“减 1”很关键,很多第一次做这道题的人会在这里犯错。因为区间是闭区间,覆盖的是连续的整数点。比如 [4,5] 覆盖 4 和 5,[7,8] 覆盖 7 和 8,中间真正空出来的只有一个整数点 6,所以 gap = 7 - 5 - 1 = 1,而不是 7 - 5 = 2。这个数学定义就是后面所有代码的基础。

1.3 另一个例子:连不上时要输出什么

再看一个连不上的例子:

输入:

2 1 2 10 20 1 3

区间 [1,2] 和 [10,20] 之间的缺口大小是 10 - 2 - 1 = 7,但手上的连接器长度只有 3,不够补。这时候应该输出 -1,代表无法把区间全部连成一片。

如果 m 个连接器里没有任何一个能覆盖某个缺口,哪怕其他缺口都能补上,整体也会断成两截以上,所以 -1 是合理的。后面实现时,我们用贪心双指针扫描,遇到这种情况直接终止输出 -1。

2. 解题链路拆开看:排序、合并、补缝隙

2.1 为什么区间必须先排序

拿到手的区间不一定是按顺序给的,题目输入完全可能是乱序。如果不先排序,我们根本不知道哪个区间和哪个区间相邻,也没法判断它们之间到底隔着多少缺口。所以第一步是把区间按照左端点从左到右排序,左端点相同时按右端点排序。这一步是后面所有操作的地基。

排序在每种语言里都是很朴素的调用,但要注意比较器的写法。以 Java 为例,int 二维数组排序要写成:

Arrays.sort(intervals, (a, b) -> a[0] != b[0] ? a[0] - b[0] : a[1] - b[1]);

以 C++ 为例,用 pair 存区间,直接 sort 默认先按 first 再按 second,天然满足要求,这也是用 pair 省事的理由。排序之后,区间就变成了沿坐标轴从左到右排列的一条序列,接下来才能谈合并和补缝。

2.2 合并时最容易错的地方:相邻区间不是缺口

区间排序后,要先把所有“已经连通”的区间合并成一个大段。合并条件不是简单的s <= curEnd,而是要写成s <= curEnd + 1。

为什么多一个 1?因为闭区间 [1,3] 覆盖了 1、2、3;[4,6] 覆盖了 4、5、6。这两个区间虽然不重叠,但它们是相邻的,中间没有空出来的整数点,从“连通性”的角度看,它们本来就是连成一片的,不需要消耗任何连接器。

如果只按重叠判断,把 [1,3] 和 [4,6] 当成两个区间段,就会错误地认为它们之间存在一个 gap = 4 - 3 - 1 = 0 的缺口。虽然长度为 0 的“连接器”理论上也能补,但题目根本不会给你长度为 0 的连接器,而且这样会让后面计算多出一个无意义的匹配项,甚至导致答案错误。所以区间合并的代码必须把“相邻相邻即连续”这一层考虑进去。

合并时只需要维护当前大段的右端点 curEnd。因为区间已经按左端点排序,后面每个新区间的左端点只可能大于等于当前大段的左端点,所以当前大段的左边界我们根本不关心,只要看右边界能伸到哪里就行。遇到新的大段时,把 curEnd 更新成新区间的右端点。

2.3 gap 的数学定义:为什么是 next.start - cur.end - 1

一旦某个新区间的左端点严格大于当前大段右端点加一,说明这里出现了一个真正的缺口。比如当前大段右端点是 curEnd,下一个区间的左端点是 s,那么缺口大小就是:

gap = s - curEnd - 1

可以这样理解:s 是缺口右边第一个被覆盖的点,curEnd 是缺口左边最后一个被覆盖的点。从 curEnd 走到 s,总共相差了 s - curEnd 个“单位步长”,但这个长度里包含了 curEnd 本身的位置,真正中间空出来的点数要再减 1。

举例 [1,3] 和 [5,7]:curEnd = 3,s = 5,gap = 5 - 3 - 1 = 1,中间空出来的正是整数点 4。如果忘记减 1,算成 2,就会把本来长度 1 的连接器误判成不够用,得不偿失。凡是坐标轴上的整数覆盖问题,gap 公式基本都是这个形态,记住“开区间长度减一”就不会错。

2.4 一个同时出现重叠、相邻、缺口的混合例子

把上面的合并逻辑放到一个混合例子里完整推一遍,你就能看到排序、合并、gap 三个动作是怎么协作的。

输入:

5 1 3 2 4 5 6 9 10 12 20 3 1 2 8

排序后区间是 [1,3]、[2,4]、[5,6]、[9,10]、[12,20]。

从第一个区间开始,curEnd = 3。第二个区间 [2,4] 的 s = 2,满足s <= curEnd + 1,所以合并,curEnd = max(3,4) = 4。第三个区间 [5,6] 的 s = 5,满足5 <= 4 + 1,继续合并,curEnd = max(4,6) = 6。这三次合并说明 [1,3]、[2,4]、[5,6] 整体已经是连续覆盖了。

第四个区间 [9,10] 的 s = 9,不满足9 <= 6 + 1,这里出现缺口 gap = 9 - 6 - 1 = 2,然后更新 curEnd = 10。第五个区间 [12,20] 的 s = 12,不满足12 <= 10 + 1,又出现缺口 gap = 12 - 10 - 1 = 1。

所以最终得到两个缺口:[2, 1]。连接器排序后是 [1, 2, 8],从小到大匹配,缺口 1 用长度 1,缺口 2 用长度 2,总共使用 2 个连接器,输出 2。

这个例子的价值在于,重叠、相邻、缺口三种情况同时出现了,代码里合并分支和产生 gap 的分支都走了一遍。做这道题时,强烈建议先在草稿纸或者本地编译器里跑一次这种混合用例,确认自己的合并逻辑没有把相邻区间误判成缺口。

3. 贪心必须把缺口也排序:双指针匹配的完整证明

3.1 不排序缺口会怎样:一个反例

拿到缺口数组后,很多人第一反应是:直接从小到大遍历连接器,遇到能补的缺口就补上。这个方案有一个隐藏问题——如果缺口没有先排序,贪心顺序就错了。

看这个构造出来的例子。三个区间段分别是 [1,2]、[5,6]、[8,9]。第一次出现的缺口是 [1,2] 到 [5,6] 之间,gap = 5 - 2 - 1 = 2;第二次出现的缺口是 [5,6] 到 [8,9] 之间,gap = 8 - 6 - 1 = 1。连接器是 [1, 2]。

如果按照缺口出现的顺序 [2, 1] 去匹配连接器,连接器升序是 [1, 2]。先处理缺口 2,跳过长度 1 的连接器,用掉长度 2 的连接器;再处理缺口 1 时,已经没有剩余连接器了,输出 -1。但显然正确答案是缺口 1 用长度 1,缺口 2 用长度 2,输出 2。

区别在哪里?区别在于贪心策略必须是“从最小的缺口开始吃”。缺口大小顺序和输入顺序没有任何关系,所以缺口数组也要像连接器一样排序。排序之后,小缺口优先用小连接器填,大连接器留下来应对可能出现的大缺口,这样才不会把小连接器浪费在大缺口上。这就是这道题的贪心核心。

3.2 最小可行替换的贪心证明

为什么“从最小缺口开始,每次使用能满足它的最短连接器”一定是最优解?这里可以用一个标准的交换论证来解释。

假设当前最小的缺口是 x,当前从排序后的连接器里找到的第一个能满足 x 的连接器长度是 l,也就是说 l 是可用连接器里最小的一个。现在假设某个最优解里,给缺口 x 分配的是一个更长的连接器 L,而把 l 留给了后面某个缺口 y,那么必然有 l >= y(因为 l 被派给了 y)。

我们把这两个分配交换一下:用 l 去补 x,因为 l >= x,可以补上;用 L 去补 y,因为 L >= l 且 l >= y,所以 L >= y,也能补上。交换之后,连接器使用个数不变,可行性不被破坏,所以“给当前最小缺口用最小可行连接器”的贪心选择永远可以被纳入某个最优解。

这个证明听起来有点绕,用一句话总结就是:小连接器能办的事,大连接器一定也能办;大连接器能办的某些事,小连接器未必能办。所以为了保留小连接器去处理小缺口,让大连接器处理大缺口,我们对当前最小的缺口做出选择时,一定不会吃亏。

3.3 匹配失败就终止的边界条件

实现时用双指针:缺口数组升序,连接器数组升序。对于当前的 gap,用一个指针 p 在连接器数组上向右移动,跳过所有小于 gap 的连接器,直到找到第一个长度大于等于 gap 的连接器。

这里有一个很容易忽略的优化:如果当前 gap 找不到可用的连接器,也就是 p 已经越过数组末尾了,可以直接输出 -1 退出,不需要再看后面的缺口。因为缺口数组是升序的,当前处理的 gap 已经是剩余缺口里最小的一个,最小的都补不上,更大的更不可能用剩余连接器补上。此时整体区间已经被永久切断,不存在翻盘可能。

如果题目要求输出“最多能连多少”而不是 -1,那么匹配失败时不要退出,跳过这个缺口继续看下一个就行。核心的贪心匹配逻辑完全一样,只是收尾动作不同。

3.4 复杂度结论

区间排序 O(n log n),连接器排序 O(m log m),缺口数组排序 O(k log k),其中 k 是缺口数量,最多是 n-1。双指针扫一圈 O(k + m)。整体复杂度由排序主导,在 n 和 m 都到十万级别时完全能扛住。这道题对时间和空间的要求都不苛刻,真正的失分点全在逻辑边界,不在性能。

4. 六种语言的实现对照

下面的六份代码共享同一套核心逻辑:排序区间 -> 合并相邻区间并收集缺口 -> 缺口和连接器都升序 -> 双指针贪心匹配。为了贴合机考的 ACM 模式,全部从标准输入读取数据,结果输出到标准输出。代码可以直接抄到机考环境里用。

4.1 Python 版

Python 是做这种题最舒服的语言,不需要声明复杂类型,sys.stdin.read().split()一把梭把全部数字读进来,然后按顺序取。gap 用列表收集,排序后匹配。

import sys def solve(): data = list(map(int, sys.stdin.read().split())) idx = 0 n = data[idx] idx += 1 intervals = [] for _ in range(n): l, r = data[idx], data[idx + 1] intervals.append((l, r)) idx += 2 m = data[idx] idx += 1 connectors = data[idx:idx + m] if n == 0: print(0) return intervals.sort(key=lambda x: (x[0], x[1])) connectors.sort() gaps = [] cur_end = intervals[0][1] for i in range(1, n): s, e = intervals[i] if s <= cur_end + 1: if e > cur_end: cur_end = e else: gaps.append(s - cur_end - 1) cur_end = e gaps.sort() p = 0 used = 0 for gap in gaps: while p < m and connectors[p] < gap: p += 1 if p >= m: print(-1) return used += 1 p += 1 print(used) if __name__ == "__main__": solve()

Python 的 int 是任意精度,完全不用担心坐标数据超过 32 位整数的问题。机考环境如果允许 Python,这道题用 Python 写是最省时间的。

4.2 Java 版

Java 是不少机考同学的主力语言。需要注意两点:类名必须叫Main;二维数组排序需要自己写 lambda 比较器。读入用 Scanner,数据量不大时完全够用。

import java.util.*; public class Main { public static void main(String[] args) { Scanner sc = new Scanner(System.in); int n = sc.nextInt(); int[][] intervals = new int[n][2]; for (int i = 0; i < n; i++) { intervals[i][0] = sc.nextInt(); intervals[i][1] = sc.nextInt(); } int m = sc.nextInt(); int[] connectors = new int[m]; for (int i = 0; i < m; i++) { connectors[i] = sc.nextInt(); } if (n == 0) { System.out.println(0); return; } Arrays.sort(intervals, (a, b) -> a[0] != b[0] ? a[0] - b[0] : a[1] - b[1]); Arrays.sort(connectors); List<Integer> gaps = new ArrayList<>(); long curEnd = intervals[0][1]; for (int i = 1; i < n; i++) { long s = intervals[i][0]; long e = intervals[i][1]; if (s <= curEnd + 1) { if (e > curEnd) { curEnd = e; } } else { gaps.add((int) (s - curEnd - 1)); curEnd = e; } } Collections.sort(gaps); int p = 0; int used = 0; for (int gap : gaps) { while (p < m && connectors[p] < gap) { p++; } if (p >= m) { System.out.println(-1); return; } used++; p++; } System.out.println(used); } }

这里把 curEnd、s、e 都定义成 long,是为了避免区间端点在极端情况下相加溢出。比如端点范围到 10^9 时,s - curEnd - 1可能接近 2×10^9,int 最大值是 2147483647,已经非常危险。真正机考时可能遇到更大的数值范围,用 long 是一劳永逸的写法。

4.3 JavaScript(Node.js)版

JavaScript 在机考环境中通常以 Node.js 运行。读取标准输入最稳妥的方式是用fs.readFileSync('/dev/stdin', 'utf8'),在 Linux 类系统里一直可用。把所有数字按空白字符切出来,再转成 Number。顶层不要直接写return,把逻辑包在函数里更安全。

const fs = require('fs'); function solve() { const input = fs.readFileSync('/dev/stdin', 'utf8').trim().split(/\s+/).map(Number); let idx = 0; const n = input[idx++]; const intervals = []; for (let i = 0; i < n; i++) { intervals.push([input[idx++], input[idx++]]); } const m = input[idx++]; const connectors = []; for (let i = 0; i < m; i++) { connectors.push(input[idx++]); } if (n === 0) { console.log(0); return; } intervals.sort((a, b) => a[0] - b[0] || a[1] - b[1]); connectors.sort((a, b) => a - b); const gaps = []; let curEnd = intervals[0][1]; for (let i = 1; i < n; i++) { const s = intervals[i][0]; const e = intervals[i][1]; if (s <= curEnd + 1) { if (e > curEnd) { curEnd = e; } } else { gaps.push(s - curEnd - 1); curEnd = e; } } gaps.sort((a, b) => a - b); let p = 0; let used = 0; for (const gap of gaps) { while (p < m && connectors[p] < gap) { p++; } if (p >= m) { console.log(-1); return; } used++; p++; } console.log(used); } solve();

JavaScript 的 Number 是双精度浮点数,安全整数范围到 2^53 左右,这道题的坐标范围一般不会超过它。需要注意数组 sort 默认按字典序排序,所以数字数组必须显式传入比较函数(a, b) => a - b,这个是 JS 特有的坑。

4.4 Go 版

Go 的输入处理用bufio.Scanner加ScanWords模式最省心,可以避免逐行读取的换行符问题。区间存成结构体切片,排序时用sort.Slice写比较函数。

package main import ( "bufio" "fmt" "os" "sort" "strconv" ) type Interval struct { s, e int } func main() { scanner := bufio.NewScanner(os.Stdin) scanner.Split(bufio.ScanWords) readInt := func() int { scanner.Scan() v, _ := strconv.Atoi(scanner.Text()) return v } n := readInt() intervals := make([]Interval, n) for i := 0; i < n; i++ { intervals[i] = Interval{readInt(), readInt()} } m := readInt() connectors := make([]int, m) for i := 0; i < m; i++ { connectors[i] = readInt() } if n == 0 { fmt.Println(0) return } sort.Slice(intervals, func(i, j int) bool { if intervals[i].s != intervals[j].s { return intervals[i].s < intervals[j].s } return intervals[i].e < intervals[j].e }) sort.Ints(connectors) gaps := make([]int, 0, n) curEnd := intervals[0].e for i := 1; i < n; i++ { s, e := intervals[i].s, intervals[i].e if s <= curEnd+1 { if e > curEnd { curEnd = e } } else { gaps = append(gaps, s-curEnd-1) curEnd = e } } sort.Ints(gaps) p := 0 used := 0 for _, gap := range gaps { for p < m && connectors[p] < gap { p++ } if p >= m { fmt.Println(-1) return } used++ p++ } fmt.Println(used) }

Go 的 int 在 64 位平台上是 64 位整数,所以存坐标和 gap 都安全。机考环境下 Go 的版本一般都比较新,sort.Slice可以直接用。

4.5 C++ 版

C++ 写这类题非常标准,pair 直接排序可以省去自定义比较器的代码。坐标和 gap 用 long long,避免 int 溢出。代码里用#include <bits/stdc++.h>在 Linux 机考环境下一般没问题,追求稳也可以用标准头文件。

#include <bits/stdc++.h> using namespace std; int main() { int n; if (!(cin >> n)) return 0; vector<pair<long long, long long>> intervals(n); for (int i = 0; i < n; i++) { cin >> intervals[i].first >> intervals[i].second; } int m; cin >> m; vector<long long> connectors(m); for (int i = 0; i < m; i++) { cin >> connectors[i]; } if (n == 0) { cout << 0 << endl; return 0; } sort(intervals.begin(), intervals.end()); sort(connectors.begin(), connectors.end()); vector<long long> gaps; long long curEnd = intervals[0].second; for (int i = 1; i < n; i++) { long long s = intervals[i].first; long long e = intervals[i].second; if (s <= curEnd + 1) { curEnd = max(curEnd, e); } else { gaps.push_back(s - curEnd - 1); curEnd = e; } } sort(gaps.begin(), gaps.end()); int p = 0; int used = 0; for (long long gap : gaps) { while (p < m && connectors[p] < gap) { p++; } if (p >= m) { cout << -1 << endl; return 0; } used++; p++; } cout << used << endl; return 0; }

C++ 里 pair 排序默认按 first 升序,再按 second 升序,和题目要求的区间排序完全一致。vector 动态扩容也不需要预先指定大小,代码最接近伪代码。

4.6 C 版

C 语言要自己管理内存和排序,代码量是六种语言里最大的。区间用结构体数组存,动态分配长度,排序用 qsort,比较函数里用三目运算防止直接做减法时溢出。

#include <stdio.h> #include <stdlib.h> typedef struct { long long s; long long e; } Interval; int cmpInterval(const void *a, const void *b) { const Interval *x = (const Interval *)a; const Interval *y = (const Interval *)b; if (x->s != y->s) { return (x->s > y->s) ? 1 : -1; } if (x->e != y->e) { return (x->e > y->e) ? 1 : -1; } return 0; } int cmpLongLong(const void *a, const void *b) { long long x = *(const long long *)a; long long y = *(const long long *)b; if (x < y) return -1; if (x > y) return 1; return 0; } int main() { int n; if (scanf("%d", &n) != 1) return 0; Interval *intervals = (Interval *)malloc(sizeof(Interval) * n); for (int i = 0; i < n; i++) { scanf("%lld %lld", &intervals[i].s, &intervals[i].e); } int m; scanf("%d", &m); long long *connectors = (long long *)malloc(sizeof(long long) * m); for (int i = 0; i < m; i++) { scanf("%lld", &connectors[i]); } if (n == 0) { printf("0\n"); free(intervals); free(connectors); return 0; } qsort(intervals, n, sizeof(Interval), cmpInterval); qsort(connectors, m, sizeof(long long), cmpLongLong); long long *gaps = (long long *)malloc(sizeof(long long) * n); int gapCnt = 0; long long curEnd = intervals[0].e; for (int i = 1; i < n; i++) { long long s = intervals[i].s; long long e = intervals[i].e; if (s <= curEnd + 1) { if (e > curEnd) { curEnd = e; } } else { gaps[gapCnt++] = s - curEnd - 1; curEnd = e; } } qsort(gaps, gapCnt, sizeof(long long), cmpLongLong); int p = 0; int used = 0; for (int i = 0; i < gapCnt; i++) { long long gap = gaps[i]; while (p < m && connectors[p] < gap) { p++; } if (p >= m) { printf("-1\n"); free(intervals); free(connectors); free(gaps); return 0; } used++; p++; } printf("%d\n", used); free(intervals); free(connectors); free(gaps); return 0; }

C 语言的输出里,长整型用%lld,这是和 int 最主要的区别。虽然 ACM 模式下不释放内存也不会被判错,但写 C 最好还是把 malloc 出来的内存都 free。另外,如果用 C 提交发现 grep 到奇怪的问题,先检查 qsort 比较函数是不是写成return a - b了,这个写法在极端值下会溢出。

5. 双机位C卷考场最值得盯住的五个细节

5.1 ACM模式:别再找类和函数的空壳

区间连接器不是那种给你一个类名让你补函数的题目,机考环境是完整 ACM 模式:从标准输入读数据,往标准输出写答案。Java 的公开类必须是Main,C++ 必须有main函数,Python 要有顶层读入逻辑,JS 要把逻辑写成完整脚本而不是导出一个函数。这看起来很简单,但每年都有同学在熟悉的环境里写习惯了核心代码模式,提交时却忘了补输入输出,导致整题白做。

建议平时练习就按 ACM 模式的完整代码来写。多语言混刷时尤其要在本地把输入输出骨架固定住,考试时直接套自己熟悉的模板,而不是临场回忆怎么读取。

5.2 数据范围的坑:别用 int 存缝隙

区间端点如果给到 10^9 量级,两个端点相减再减一,完全可能逼近 int 上限。Java int 最大 2147483647,C++ int 同理。如果端点再给到 10^10 或更大,int 必炸。

稳妥方案是 Java 用 long,C++ 用 long long,C 语言用 long long,Go 直接用 int(64 位环境安全),Python 和 JS 不用担心精度问题。排序和 gap 计算都用长整型,唯一可能需要 int 的只有连接器数量、区间数量这些计数变量。

5.3 三个必须手工验证的边界

第一个边界:区间完全重叠。比如 [1,5] 和 [3,8] 输入进来,合并后只剩一个区间,gaps 为空,输出 0。很多同学会忘记这种情况,代码在访问intervals[0]后没有循环处理,结果输出 0 正确但心里没底,自测时应该专门跑一次。

第二个边界:区间恰好相邻。比如 [1,3] 和 [4,6],中间没有空点,合并条件s <= curEnd + 1必须把它们合并成一段,而不是生成一个 gap=0。前面章节强调过的相邻区间陷阱就在这里体现。

第三个边界:m 个连接器数量不足。比如有两个 gap,但只有一个连接器,即使这个连接器很长,也只能补一个 gap,另一个补不上,输出 -1。代码里while p < m和if p >= m这两个判断要写好,尤其是 p 自增的位置不能放错。

5.4 自测用例表

我把这道题最值得跑的五组用例放在下面,提交前按这个表过一遍,基本能把逻辑边界都覆盖到。

场景输入摘要期望输出说明
全部重叠n=2, [1,5] [3,8], m=00合并后一段,无需连接器
恰好相邻n=2, [1,3] [4,6], m=1, [5]0相邻区间不是缺口,连接器不需要用
标准场景n=3, [1,2] [4,5] [7,8], m=3, [1,2,2]2两个缺口各用一个连接器
无法连通n=2, [1,2] [10,20], m=1, [3]-1连接器长度不够覆盖缺口
缺口大于连接器n=3, [1,2] [5,6] [8,9], m=2, [1,2]-1按输入顺序贪心会错,必须缺口排序后匹配

第五个用例就是前面 3.1 节讲过的反例。它表面上是验证贪心,实际是在验证缺口数组有没有排序。如果你实现的逻辑没有对 gaps 排序,这一组自测用例就能直接暴露问题。

5.5 双机位之下,减少试错提交的策略

双机位监考意味着考试过程中不能随便切屏幕、不能反复打开外部编译器查资料,试错成本变得很高。最现实的做法是:本地把所有样例和上面 5.4 表的边界用例都跑通后,再一次性提交。毕竟机考判题一般只看最终通过率,不会因为你提交次数少就加分。

如果实在卡在某个样例上,不要急着反复提交试探,先在草稿纸上把区间画出来,标出每个 gap,再对照连接器长度手算一遍。这种区间题,图一画出来,逻辑基本就通了。

6. 备考私货:做这题时的顺序、判断和一条最深的体会

6.1 开题先确认输出协议

区间连接器的输出协议在不同题源里确实有差别。有的版本问“最少使用几个连接器能全部连通,不能连通就输出 -1”,有的版本问“最多能把多少段区间连接起来”,还有的版本问“最多能使用多少个连接器”。这三个问题共享 80% 的代码,唯一的区别就是 else 分支里是print(-1)还是continue。

所以读题前两分钟,先确认三件事:区间的闭区间开口方向、连接器数量 m 是否可能为 0、输出不连通时是固定值还是统计值。如果输出最大连通数量,就把匹配失败改成跳过缺口继续匹配,最后输出 used 或已连通段数。确认输出协议之后再动手写,能避免一半以上的无效提交。

6.2 卡住时的处理顺序

机考时间一般给得比较宽裕,但题目量大是事实。如果这道题卡住了,我建议按这个顺序排查:

  1. 先确认合并条件是不是“相邻即合并”,这是最容易丢分的地方。
  2. 再确认缺口数组和连接器数组是不是都升序排序了,缺口不排序是反例里最常见的错误。
  3. 然后检查双指针里 p 的推进位置,是不是在匹配成功后才p++,有没有把跳过小连接器的p++和消耗连接器的p++写混。
  4. 最后用 5.4 表里的用例跑一遍,特别是那个必须输出 -1 的用例。

这道题代码量不大,排错时间主要花在思路上。如果五分钟还没找到问题,先把代码恢复到这个题的核心骨架,重新从排序合并开始推,往往比盯着一行代码纠结更有效。

6.3 从会做到稳拿满分的最后一公里

区间连接器这类题,真正做到“稳拿满分”的标准不是会写代码,而是能在两分钟内判断出它的算法类型,并且把所有边界一次性写对。平时刷题时,我建议每做完一道题,就把它归纳进自己的“题型档案”。这道题的档案标签是:排序 + 区间合并 + 贪心双指针 + 边界处理。

复习时只看档案不看代码,自己默写一遍完整流程,再对照文章里的实现查漏补缺。等你能不借助任何资料,在白纸上把输入到输出的全流程推出来,这道题在考场上对你来说就是稳稳的拿分题。

这题里我个人最深的体会是:不要让“简单题”这三个字放松警惕。区间题目表面就是排序合并,真正拉分的地方往往就是那个s <= curEnd + 1,或者 gap 算式中那不起眼的减一。把这些细节刻进肌肉记忆,比临时记住某个固定模板更可靠。

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

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

立即咨询