1. 问题背景与核心挑战
今天我们要啃的是一道LeetCode上的贪心算法难题——757.设置交集大小至少为2。题目要求我们找到一个最小的集合S,使得给定的一组区间中,每个区间与S的交集大小至少为2。这听起来简单,但实际处理起来却暗藏玄机。
先看题目给出的函数签名:
java复制public int intersectionSizeTwo(int[][] intervals)
其中intervals数组的每个元素表示一个区间,比如[[1,3],[2,5],[4,6]]。我们的任务是返回满足条件的最小集合S的大小。
这个问题的实际应用场景很广泛。比如在日程安排中,要确保每个会议都有至少两个备用时间;在资源分配时,保证每个任务都有两个备选资源;在网络安全领域,要求关键系统至少有两个备份节点等。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 贪心算法的选择依据
为什么这道题适合用贪心算法解决?贪心算法通常适用于具有"最优子结构"的问题——即全局最优解可以通过一系列局部最优选择达到。在这个问题中,我们需要在每一步做出当前看起来最优的选择(即选择哪些数字放入集合S),这些局部选择最终能导向全局最优解。
与动态规划相比,贪心算法不需要存储子问题的解,因此通常更高效。但贪心算法的难点在于如何证明其正确性——为什么局部最优能导致全局最优?这往往需要严谨的数学证明。
对于本题,我们可以这样思考:如果我们按照特定顺序处理区间,并在每个步骤中选择最有利于后续处理的数字,就能逐步构建出最优解。这种"当前最优+不影响后续"的特性,正是贪心算法的典型特征。
3. 关键解题步骤详解
3.1 区间排序预处理
第一步是对所有区间进行排序。这里有个关键决策:应该按区间起点还是终点排序?经过分析,我们应该按区间终点升序排序,终点相同的按起点降序排序。这样排序后,我们可以确保处理每个区间时,已经处理过的区间不会对当前选择产生干扰。
排序的实现示例:
java复制Arrays.sort(intervals, (a, b) -> {
if (a[1] != b[1]) return a[1] - b[1];
return b[0] - a[0];
});
这种排序方式确保了:
- 终点较小的区间先处理
- 对于终点相同的区间,起点较大的先处理(即区间范围较小的先处理)
3.2 贪心选择策略
维护两个变量:left和right,表示当前集合S中最后加入的两个元素。初始时设为-2和-1(任意足够小的值)。
对于每个区间[start, end],我们需要检查:
- 如果
start > right:说明当前区间与S无交集,需要新增两个元素。最优选择是取区间的最后两个数字(end-1和end),这样最有可能与后面的区间产生交集。 - 如果
start <= left:说明已有至少两个交集,无需操作。 - 如果
left < start <= right:说明只有一个交集,需要新增一个元素。最优选择是取区间的最后一个数字(end)。
核心代码逻辑:
java复制int left = -2, right = -1;
int res = 0;
for (int[] interval : intervals) {
if (interval[0] > right) {
res += 2;
right = interval[1];
left = right - 1;
} else if (interval[0] > left) {
res += 1;
left = right;
right = interval[1];
}
}
return res;
3.3 正确性证明
为什么这个贪心策略是正确的?关键在于我们总是选择尽可能大的数字(靠近区间终点),这样这些数字有最大概率与后续区间产生交集。具体来说:
- 当需要新增两个元素时,选择
end-1和end能最大化与后续区间的交集可能性,因为后续区间的终点都≥当前区间的终点(由于排序)。 - 当需要新增一个元素时,选择
end同样基于这个原理。
这种策略确保了每个新增的数字都能为尽可能多的后续区间服务,从而最小化集合S的总大小。
4. 边界条件与特殊案例
4.1 空输入处理
虽然题目保证输入非空,但好的编程习惯应该处理空输入:
java复制if (intervals == null || intervals.length == 0) return 0;
4.2 单元素区间
虽然题目描述暗示区间长度≥2,但实际测试案例可能包含[a,a]这样的单元素区间。我们的算法已经能正确处理这种情况,因为:
- 排序后,
[a,a]会排在相同终点的区间最前面(因为起点=终点) - 处理时会识别为需要两个交集,因此会添加a和a(实际上只添加一个)
4.3 大数测试案例
当区间数字很大时(如接近Integer.MAX_VALUE),需要注意:
- 排序比较器中的减法可能导致溢出,应使用Integer.compare:
java复制Arrays.sort(intervals, (a, b) -> {
int cmp = Integer.compare(a[1], b[1]);
return cmp != 0 ? cmp : Integer.compare(b[0], a[0]);
});
- 初始的left和right应设为足够小的值,但不能太小以至于减法溢出
5. 算法复杂度分析
时间复杂度:
- 排序:O(n log n),n为区间数量
- 贪心处理:O(n)
- 总体:O(n log n)
空间复杂度:
- 排序:O(log n)(Java的TimSort使用栈空间)
- 其他:O(1)
- 总体:O(log n)
6. 实际编码中的优化技巧
6.1 避免重复计算
在循环中,多次访问interval[0]和interval[1],可以提取为局部变量:
java复制for (int[] interval : intervals) {
int start = interval[0], end = interval[1];
// 使用start和end代替多次访问interval[0]和interval[1]
}
6.2 提前终止条件
如果发现某个区间需要新增两个元素,且其终点已经大于所有后续区间的起点,可以提前终止:
java复制if (res > intervals.length * 2) break; // 不可能超过2n
6.3 调试输出
在开发过程中,可以添加调试输出验证算法:
java复制System.out.println("Processing [" + start + "," + end + "] left=" + left + " right=" + right);
7. 与其他相似问题的对比
7.1 与经典区间覆盖问题对比
经典的区间覆盖问题(如LeetCode 452.用最少数量的箭引爆气球)只需要交集大小≥1,而本题要求≥2。这使得问题复杂度显著增加:
- 不能简单地跟踪最后一个点
- 需要维护两个点的信息
- 决策逻辑更复杂(三种情况而非两种)
7.2 与集合覆盖问题对比
集合覆盖问题是NP难的,而本题可以在多项式时间内解决,因为:
- 我们对区间进行了排序
- 利用了特定的贪心性质
- 交集大小要求固定(为2)
8. 测试案例设计指南
要全面测试这个算法,应该考虑以下类型的测试案例:
- 常规案例:
java复制[[1,3],[2,5],[4,6]] → 4 (e.g. {2,3,4,5})
- 完全不相交的区间:
java复制[[1,2],[3,4],[5,6]] → 6 (每个区间需要两个独立元素)
- 完全重叠的区间:
java复制[[1,5],[1,5],[1,5]] → 2 (e.g. {4,5})
- 边界重叠:
java复制[[1,2],[2,3],[3,4]] → 4 (e.g. {1,2,3,4})
- 单元素区间:
java复制[[1,1],[2,2],[3,3]] → 6
- 大数测试:
java复制[[Integer.MAX_VALUE-1, Integer.MAX_VALUE]] → 2
9. 常见错误与调试技巧
9.1 排序顺序错误
如果错误地按起点排序,会导致贪心策略失效。例如:
java复制[[1,5],[2,3],[4,6]]
按终点排序应为[[2,3],[1,5],[4,6]],如果按起点排序会导致错误结果。
调试方法:打印排序后的区间数组,确认排序顺序正确。
9.2 贪心逻辑遗漏
容易遗漏三种情况中的某一种,特别是start <= left的情况。这会导致重复计数。
调试方法:对于每个区间,打印出left和right的值,以及决策过程。
9.3 初始值设置不当
如果初始left和right设置不当(如都设为0),可能会错误处理包含0的区间。
调试方法:选择明显不会出现在输入中的初始值(如负数)。
10. 扩展思考
10.1 如果要求交集大小为k怎么办?
这个问题可以推广到交集大小至少为k的一般情况。解决方案类似:
- 维护一个大小为k的窗口
- 对于每个区间,检查已有交集中有几个元素
- 补充不足的元素,总是选择最大的可能值
10.2 如果区间包含负数或浮点数?
算法本身不依赖于数字的具体值,只依赖于比较操作。因此可以处理负数。对于浮点数,需要考虑精度问题,但基本逻辑不变。
10.3 并行化可能性
由于区间已排序,且处理顺序固定,这个算法难以并行化。但可以考虑:
- 分段处理不相交的区间块
- 合并处理结果
在实际编码面试中,遇到这类问题时,建议先清晰地阐述解题思路,特别是贪心策略的选择理由。然后逐步实现代码,注意处理边界条件。最后通过测试案例验证算法的正确性。这道题很好地考察了对贪心算法的理解和实现能力,是准备技术面试的绝佳练习题。
