1. 题目解析与需求理解
这道LintCode 3889题目要求我们实现一个方法public int[] amountPainted(int[][] paint),用于计算每天绘制新区域的数量。题目描述虽然简短,但蕴含着几个关键信息点需要我们先理解清楚。
首先,输入参数paint是一个二维数组,其中每个元素paint[i]表示第i天的绘制操作,通常包含两个数字表示绘制的起始和结束位置(左闭右开区间)。例如paint = [[1,4],[4,7],[5,8]]表示:
- 第0天绘制区间[1,4)
- 第1天绘制区间[4,7)
- 第2天绘制区间[5,8)
输出需要是一个数组,其中每个元素表示当天新绘制的区域长度。例如对于上述输入,正确输出应该是[3,3,1],因为:
- 第0天绘制了3个单位的新区域(1,2,3)
- 第1天绘制了3个单位的新区域(4,5,6)
- 第2天只绘制了1个单位的新区域(7),因为5和6在第1天已经被绘制过了
注意:区间通常是左闭右开的,即包含起始点但不包含结束点。这是编程中处理区间的常见约定,需要特别注意。
2. 算法思路分析与选择
2.1 暴力解法及其局限性
最直观的解法是使用一个布尔数组painted来记录每个单位是否已被绘制。初始化时所有值为false,表示未被绘制。对于每一天的绘制区间,我们遍历其中的每个单位,如果未被绘制则计数并标记为已绘制。
java复制public int[] amountPainted(int[][] paint) {
int max = 0;
for (int[] p : paint) {
max = Math.max(max, p[1]);
}
boolean[] painted = new boolean[max];
int[] result = new int[paint.length];
for (int i = 0; i < paint.length; i++) {
int start = paint[i][0];
int end = paint[i][1];
int count = 0;
for (int j = start; j < end; j++) {
if (!painted[j]) {
count++;
painted[j] = true;
}
}
result[i] = count;
}
return result;
}
这种解法的时间复杂度是O(N*K),其中N是操作天数,K是平均每次操作的区间长度。当区间很大时(比如上百万),这种解法会非常低效。
2.2 优化思路:区间合并与跳跃查找
更高效的解法需要利用区间合并的思想,并找到一种快速跳过已绘制区域的方法。我们可以使用TreeMap来记录已绘制的区间,并利用其高效的查找能力:
- 初始化一个空的TreeMap来存储已绘制的区间
- 对于每个新的绘制区间[start,end):
- 查找与start和end相邻或重叠的已有区间
- 计算新绘制的区域长度
- 合并重叠区间更新TreeMap
这种方法的优势在于利用了TreeMap的O(logN)查找性能,将整体时间复杂度优化到O(NlogN)。
3. 基于TreeMap的优化实现
3.1 核心数据结构选择
我们选择TreeMap<Integer, Integer>来存储已绘制的区间,其中key是区间起点,value是区间终点。TreeMap的floorEntry和ceilingEntry方法可以帮助我们快速找到与当前区间相邻或重叠的已有区间。
java复制TreeMap<Integer, Integer> intervals = new TreeMap<>();
3.2 算法实现步骤
- 初始化结果数组和TreeMap
- 对于每个绘制区间[start,end):
a. 查找左边界:找到小于等于start的最大区间
b. 查找右边界:找到大于等于start的最小区间
c. 计算实际需要绘制的新区域
d. 合并重叠区间更新TreeMap - 返回结果数组
3.3 完整代码实现
java复制public int[] amountPainted(int[][] paint) {
int n = paint.length;
int[] result = new int[n];
TreeMap<Integer, Integer> intervals = new TreeMap<>();
for (int i = 0; i < n; i++) {
int start = paint[i][0];
int end = paint[i][1];
int actualStart = start;
int actualEnd = end;
// 查找左边界
Map.Entry<Integer, Integer> floor = intervals.floorEntry(start);
if (floor != null) {
if (floor.getValue() >= start) {
// 有重叠,合并
actualStart = floor.getKey();
actualEnd = Math.max(actualEnd, floor.getValue());
intervals.remove(floor.getKey());
}
}
// 查找右边界
Map.Entry<Integer, Integer> ceiling = intervals.ceilingEntry(start);
while (ceiling != null && ceiling.getKey() <= end) {
// 有重叠,合并
actualEnd = Math.max(actualEnd, ceiling.getValue());
intervals.remove(ceiling.getKey());
ceiling = intervals.ceilingEntry(start);
}
// 计算新绘制区域
int painted = 0;
if (actualStart < start) {
painted = Math.max(0, end - Math.max(start, actualStart));
} else {
painted = end - start;
// 减去与左边界的重叠部分
if (floor != null && floor.getValue() > start) {
painted -= Math.min(floor.getValue(), end) - start;
}
// 减去与右边界的重叠部分
if (ceiling != null && ceiling.getKey() < end) {
painted -= end - Math.max(ceiling.getKey(), start);
}
}
result[i] = Math.max(0, painted);
// 更新区间
intervals.put(actualStart, actualEnd);
}
return result;
}
4. 边界条件与测试用例
4.1 常见边界情况
- 空输入:
paint = [],应返回空数组 - 单次绘制:
paint = [[1,5]],应返回[4] - 完全重叠的绘制:
paint = [[1,5],[1,5]],应返回[4,0]
- 部分重叠的绘制:
paint = [[1,5],[3,7]],应返回[4,2]
- 不相邻的绘制:
paint = [[1,3],[5,7]],应返回[2,2]
- 大数测试:
paint = [[0,1000000],[999999,1000000]],应正确处理
4.2 测试代码示例
java复制@Test
public void testAmountPainted() {
Solution solution = new Solution();
// 测试用例1:基本功能
int[][] paint1 = {{1,4},{4,7},{5,8}};
assertArrayEquals(new int[]{3,3,1}, solution.amountPainted(paint1));
// 测试用例2:完全重叠
int[][] paint2 = {{1,5},{1,5}};
assertArrayEquals(new int[]{4,0}, solution.amountPainted(paint2));
// 测试用例3:空输入
int[][] paint3 = {};
assertArrayEquals(new int[]{}, solution.amountPainted(paint3));
// 测试用例4:大数测试
int[][] paint4 = {{0,1000000},{999999,1000000}};
assertArrayEquals(new int[]{1000000,1}, solution.amountPainted(paint4));
}
5. 算法复杂度分析
5.1 时间复杂度
- TreeMap的插入、删除和查找操作都是O(logM),其中M是当前已存储的区间数量
- 对于N次绘制操作,最坏情况下每次操作可能需要合并多个区间
- 总体时间复杂度为O(NlogM),其中M≤N
- 在最坏情况下(每次操作都导致区间合并),时间复杂度为O(NlogN)
5.2 空间复杂度
- 需要存储已绘制的区间,最坏情况下需要存储O(N)个区间
- 因此空间复杂度为O(N)
5.3 与暴力解法的对比
| 指标 | 暴力解法 | TreeMap优化解法 |
|---|---|---|
| 时间复杂度 | O(N*K) | O(NlogN) |
| 空间复杂度 | O(K) | O(N) |
| 适用场景 | 小范围区间 | 大范围区间 |
| 实现难度 | 简单 | 中等 |
其中K表示平均每次操作的区间长度。当K很大时(如百万级别),暴力解法将无法在合理时间内完成,而TreeMap解法仍能高效运行。
6. 实际应用与变种问题
6.1 实际应用场景
这种区间合并算法在实际中有广泛应用:
- 日程安排系统:计算可用时间段
- 资源分配:跟踪已分配和未分配的资源块
- 内存管理:记录已使用和空闲的内存区域
- 图形渲染:跟踪已渲染的像素区域
6.2 常见变种问题
- 反向问题:计算每天擦除的区域数量
- 多颜色绘制:不同颜色有不同的优先级
- 持久化查询:需要查询历史某时刻的绘制状态
- 三维版本:在三维空间中进行类似操作
6.3 扩展思考:线段树解法
对于某些变种问题,线段树(Segment Tree)可能是更优的选择,特别是当需要支持以下操作时:
- 区间更新
- 区间查询
- 动态区间长度
线段树可以将时间复杂度保持在O(NlogK),其中K是坐标范围,适合需要频繁查询的场景。
7. 常见错误与调试技巧
7.1 常见实现错误
- 区间边界处理不当:
- 忘记处理左闭右开
- 合并区间时边界计算错误
- 新区域计算错误:
- 没有正确减去重叠部分
- 负数结果未处理
- TreeMap操作错误:
- 错误使用floorEntry/ceilingEntry
- 未及时删除被合并的区间
7.2 调试技巧
- 打印中间状态:
java复制System.out.println("Day " + i + ": [" + start + "," + end + ")"); System.out.println("Current intervals: " + intervals); System.out.println("Painted: " + painted); - 使用可视化工具:
- 绘制区间图帮助理解
- 使用调试器逐步跟踪
- 从小测试用例开始:
- 先验证简单情况
- 逐步增加复杂度
7.3 性能优化建议
- 对于已知的小范围问题,可以使用数组代替TreeMap
- 如果区间非常密集,可以考虑压缩坐标
- 并行处理独立区间(如果问题允许)
8. 语言特性与实现细节
8.1 Java特定实现技巧
- 使用
NavigableMap接口的方法:floorEntry/ceilingEntry比手动迭代更高效subMap可以快速获取范围内的区间
- 自动装箱/拆箱注意:
- 对于极大数,注意Integer的范围限制
- 必要时使用Long
- 初始化容量:
- 如果知道大概的区间数量,可以预设TreeMap大小
8.2 其他语言实现差异
- C++:
- 使用
std::map或std::set - 需要注意迭代器失效问题
- 使用
- Python:
- 使用
bisect模块维护有序列表 - 字典不如Java的TreeMap方便
- 使用
- JavaScript:
- 没有内置的TreeMap结构
- 需要自己实现或使用第三方库
8.3 代码风格建议
- 方法抽取:
- 将区间合并逻辑抽出为独立方法
- 提高代码可读性
- 注释:
- 明确说明区间合并策略
- 标注复杂逻辑的意图
- 防御性编程:
- 检查输入有效性
- 处理可能的异常情况
9. 面试技巧与相关问题
9.1 面试中如何应对此题
- 问题澄清:
- 确认区间表示法(开闭区间)
- 确认输入范围限制
- 思路阐述:
- 先提出暴力解法
- 分析不足并提出优化方向
- 逐步引入TreeMap解法
- 代码实现:
- 先写框架再填充细节
- 边写边解释思路
- 测试验证:
- 主动提出测试用例
- 包括边界情况
9.2 相关面试问题
- 如何扩展支持撤销操作?
- 如果区间有优先级怎么处理?
- 如何持久化查询历史状态?
- 如何支持多维度区间?
9.3 评估标准
面试官通常会考察:
- 问题分析能力
- 算法设计能力
- 代码实现质量
- 边界情况考虑
- 沟通表达能力
10. 总结与个人经验分享
区间合并是一类经典算法问题,这道LintCode题目很好地考察了对TreeMap数据结构的理解和应用。在实际实现中,我发现以下几点特别重要:
- 区间表示的一致性:坚持使用左闭右开或闭区间,不要混用
- 合并逻辑的完整性:考虑所有可能的重叠情况
- 性能与正确性的平衡:在优化时不要引入新的错误
从个人经验来看,这类问题的难点不在于算法本身,而在于对各种边界情况的处理。建议在实现前先用几个简单例子手动模拟,确保完全理解题目要求。在面试场景中,即使不能立即想到最优解,从暴力解法开始逐步优化的过程也能展示出良好的问题解决能力。
对于想要进一步练习的开发者,我推荐尝试LeetCode上的类似题目,如:
-
- Merge Intervals
-
- Insert Interval
-
- Range Module
-
- Employee Free Time
这些题目都涉及区间操作,但各有不同的侧重点,可以帮助全面掌握这类问题的解决方法。
