区间问题是算法竞赛和工程实践中非常常见的题型。从会议安排、资源分配到基因序列比对,区间合并与调度算法的应用无处不在。本文将深入讲解区间合并的核心思想,用Java实现经典算法,并扩展到区间调度、区间覆盖等变体问题。
一、问题定义与场景分析
1.1 区间合并问题
给定一组闭区间 [start, end],要求合并所有重叠的区间,返回互不重叠的区间集合。
例如:
– 输入:[[1,3], [2,6], [8,10], [15,18]]
– 输出:[[1,6], [8,10], [15,18]]
重叠判定:两个区间 [a,b] 和 [c,d] 重叠,当且仅当 a <= d && c <= b。
1.2 区间调度问题
给定一组区间,选择尽可能多的互不重叠区间。这是经典的贪心问题,最优策略是按结束时间排序,每次选择结束最早且不与已选区间冲突的区间。
1.3 区间覆盖问题
给定目标区间 [L,R] 和一组候选区间,选择最少数量的候选区间完全覆盖目标区间。
二、核心算法设计
2.1 区间合并算法
- 按区间左端点升序排序
- 维护当前合并区间
[currStart, currEnd] - 遍历排序后的区间:
- 若当前区间与合并区间重叠,扩展合并区间的右端点
- 若不重叠,将合并区间加入结果,开始新的合并区间
- 遍历结束后加入最后一个合并区间
时间复杂度:O(n log n),主要来自排序
2.2 区间调度贪心证明
按结束时间排序后,设第一个区间为最优解的一部分。对于剩余区间,选择结束最早且不冲突的区间不会比任何其他选择更差。形式化证明使用交换论证:假设最优解未选该区间,则可用该区间替换最优解中与之冲突的区间,得到另一个最优解。
2.3 区间覆盖贪心算法
- 筛选出与目标区间
[L,R]有交集的候选区间 - 按左端点排序
- 在每次选择中,从当前覆盖点出发,选择右端点最远且左端点不超过当前覆盖点的区间
- 更新当前覆盖点为所选区间的右端点
- 重复直到覆盖
[L,R]或无法继续
三、Java完整实现
import java.util.*;
/**
* 区间合并与调度算法实现
* 包含区间合并、区间调度、区间覆盖等经典问题
*/
public class IntervalMerge {
/**
* 区间类,使用不可变设计
*/
static class Interval implements Comparable<Interval> {
final int start;
final int end;
Interval(int start, int end) {
this.start = start;
this.end = end;
}
/**
* 判断两个区间是否重叠
*/
boolean overlaps(Interval other) {
return this.start <= other.end && other.start <= this.end;
}
/**
* 合并两个重叠区间
*/
Interval merge(Interval other) {
return new Interval(
Math.min(this.start, other.start),
Math.max(this.end, other.end)
);
}
@Override
public int compareTo(Interval other) {
return Integer.compare(this.start, other.start);
}
@Override
public String toString() {
return "[" + start + ", " + end + "]";
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Interval)) return false;
Interval that = (Interval) o;
return start == that.start && end == that.end;
}
@Override
public int hashCode() {
return Objects.hash(start, end);
}
}
/**
* 区间合并:合并所有重叠区间
* @param intervals 输入区间列表
* @return 合并后的区间列表
*/
public static List<Interval> mergeIntervals(List<Interval> intervals) {
if (intervals == null || intervals.size() <= 1) {
return new ArrayList<>(intervals);
}
// 按左端点排序
List<Interval> sorted = new ArrayList<>(intervals);
Collections.sort(sorted);
List<Interval> result = new ArrayList<>();
Interval current = sorted.get(0);
for (int i = 1; i < sorted.size(); i++) {
Interval next = sorted.get(i);
if (current.overlaps(next)) {
// 重叠,扩展当前区间
current = current.merge(next);
} else {
// 不重叠,保存当前区间,开始新区间
result.add(current);
current = next;
}
}
result.add(current); // 加入最后一个区间
return result;
}
/**
* 区间调度:选择尽可能多的互不重叠区间
* 贪心策略:按结束时间排序,每次选择结束最早且不冲突的区间
*/
public static List<Interval> maxNonOverlapping(List<Interval> intervals) {
if (intervals == null || intervals.isEmpty()) {
return new ArrayList<>();
}
// 按结束时间升序排序
List<Interval> sorted = new ArrayList<>(intervals);
sorted.sort(Comparator.comparingInt((Interval i) -> i.end)
.thenComparingInt(i -> i.start));
List<Interval> result = new ArrayList<>();
Interval lastSelected = null;
for (Interval interval : sorted) {
if (lastSelected == null || interval.start > lastSelected.end) {
result.add(interval);
lastSelected = interval;
}
}
return result;
}
/**
* 区间覆盖:用最少数量的区间覆盖目标区间 [targetStart, targetEnd]
* @return 最少区间数,若无法覆盖返回 -1
*/
public static int minIntervalsToCover(List<Interval> intervals,
int targetStart, int targetEnd) {
// 筛选与目标区间有交集的候选区间,并按左端点排序
List<Interval> candidates = new ArrayList<>();
for (Interval iv : intervals) {
if (iv.end >= targetStart && iv.start <= targetEnd) {
candidates.add(iv);
}
}
candidates.sort(Comparator.comparingInt((Interval i) -> i.start)
.thenComparingInt(i -> -i.end)); // 左端点相同选右端点大的
int count = 0;
int currentEnd = targetStart;
int i = 0;
int n = candidates.size();
while (currentEnd < targetEnd) {
int maxReach = currentEnd;
// 选择所有左端点不超过currentEnd的区间中,右端点最远的
while (i < n && candidates.get(i).start <= currentEnd) {
maxReach = Math.max(maxReach, candidates.get(i).end);
i++;
}
if (maxReach == currentEnd) {
return -1; // 无法继续覆盖
}
currentEnd = maxReach;
count++;
}
return count;
}
/**
* 查找所有区间中的交集区间(公共重叠部分)
*/
public static List<Interval> findIntersections(List<Interval> intervals) {
if (intervals == null || intervals.size() < 2) {
return new ArrayList<>();
}
// 使用扫描线算法
List<int[]> events = new ArrayList<>();
for (Interval iv : intervals) {
events.add(new int[]{iv.start, 1}); // 开始事件
events.add(new int[]{iv.end, -1}); // 结束事件
}
events.sort((a, b) -> {
if (a[0] != b[0]) return Integer.compare(a[0], b[0]);
return Integer.compare(b[1], a[1]); // 结束事件优先于开始事件(处理点区间)
});
List<Interval> intersections = new ArrayList<>();
int active = 0;
int intersectStart = -1;
for (int[] event : events) {
if (event[1] == 1) {
active++;
if (active == intervals.size()) {
intersectStart = event[0];
}
} else {
if (active == intervals.size()) {
intersections.add(new Interval(intersectStart, event[0]));
}
active--;
}
}
return intersections;
}
/**
* 插入新区间到已合并的区间列表中
*/
public static List<Interval> insertInterval(List<Interval> intervals, Interval newInterval) {
List<Interval> result = new ArrayList<>();
int i = 0;
int n = intervals.size();
// 添加所有在newInterval之前的区间
while (i < n && intervals.get(i).end < newInterval.start) {
result.add(intervals.get(i));
i++;
}
// 合并所有与newInterval重叠的区间
Interval merged = newInterval;
while (i < n && intervals.get(i).start <= merged.end) {
merged = merged.merge(intervals.get(i));
i++;
}
result.add(merged);
// 添加剩余的区间
while (i < n) {
result.add(intervals.get(i));
i++;
}
return result;
}
public static void main(String[] args) {
System.out.println("=== 区间合并与调度算法演示 ===\n");
// 1. 区间合并
List<Interval> intervals = Arrays.asList(
new Interval(1, 3),
new Interval(2, 6),
new Interval(8, 10),
new Interval(15, 18),
new Interval(5, 9)
);
System.out.println("原始区间: " + intervals);
List<Interval> merged = mergeIntervals(intervals);
System.out.println("合并结果: " + merged + "\n");
// 2. 区间调度
List<Interval> scheduleIntervals = Arrays.asList(
new Interval(1, 3),
new Interval(2, 4),
new Interval(3, 5),
new Interval(6, 8),
new Interval(8, 10)
);
System.out.println("调度候选: " + scheduleIntervals);
List<Interval> selected = maxNonOverlapping(scheduleIntervals);
System.out.println("最多可选 " + selected.size() + " 个区间: " + selected + "\n");
// 3. 区间覆盖
List<Interval> coverIntervals = Arrays.asList(
new Interval(1, 4),
new Interval(2, 5),
new Interval(6, 8),
new Interval(7, 10),
new Interval(9, 12)
);
int minCover = minIntervalsToCover(coverIntervals, 1, 12);
System.out.println("覆盖 [1, 12] 最少需要 " + minCover + " 个区间\n");
// 4. 区间交集
List<Interval> intersectTest = Arrays.asList(
new Interval(1, 10),
new Interval(3, 8),
new Interval(5, 12)
);
List<Interval> common = findIntersections(intersectTest);
System.out.println("三个区间的公共交集: " + common + "\n");
// 5. 插入区间
List<Interval> mergedList = Arrays.asList(
new Interval(1, 2),
new Interval(3, 5),
new Interval(6, 7),
new Interval(8, 10),
new Interval(12, 16)
);
Interval toInsert = new Interval(4, 9);
List<Interval> afterInsert = insertInterval(mergedList, toInsert);
System.out.println("在 " + mergedList + " 中插入 " + toInsert);
System.out.println("结果: " + afterInsert);
}
}
四、算法复杂度分析
| 问题 | 时间复杂度 | 空间复杂度 | 核心策略 |
|---|---|---|---|
| 区间合并 | O(n log n) | O(n) | 按起点排序,线性扫描 |
| 区间调度 | O(n log n) | O(n) | 按终点贪心选择 |
| 区间覆盖 | O(n log n) | O(n) | 每次选覆盖最远端点 |
| 区间交集 | O(n log n) | O(n) | 扫描线+事件驱动 |
| 区间插入 | O(n) | O(n) | 线性定位合并范围 |
五、工程应用场景
- 会议管理系统:合并连续会议,找出空闲时间段
- 基因序列分析:合并重叠的基因片段,找出保守区域
- CDN缓存预热:合并相邻的缓存失效区间,批量预热
- 数据库分区维护:合并相邻的小分区,优化查询性能
- 任务调度系统:在资源约束下最大化并行任务数量
六、总结
区间问题是贪心算法的经典应用场景。本文实现的核心算法包括:
- 区间合并:排序后线性扫描,O(n log n) 时间合并所有重叠区间
- 区间调度:按结束时间贪心选择,证明其最优性
- 区间覆盖:每次选择能覆盖最远端点的区间,达到最少数量
- 扫描线算法:通过事件驱动处理多区间交集问题
掌握这些基础算法后,可以应对绝大多数区间相关问题。关键在于将实际问题抽象为区间模型,并选择恰当的贪心策略。