【扫描线】LeetCode 218. 天际线问题
题目链接
思路
参考宫水三叶大佬题解
求最大值
观察上面的图可以发现,我们需要求的其实就只是一个一个小矩形,做的其实相当于一个矩阵分割。并且求得还得是每一段的最大高度。
那么在不断变化的各种数值之间求最大数值,很容易想到使用优先队列,这样在动态的过程中始终能找到最大值。
接下来就是如何遍历的问题了。
排序
扫描线问题遍历的一个基本思路是将左右两个端点进行拆分,再进行遍历。
什么意思呢?
比如说题目中的格式是 \(\{r_i, l_i, h_i\}\),那么我们需要拆分成 \(\{l_i, h_i\}\) 和 \(\{r_i, h_i\}\)。
因为要从左到右进行遍历,所以需要按坐标对所有的建筑进行排序,如果两个建筑有坐标重合了,便让左边的建筑物排前面,右边的排后面。
为了方便区分左右端点,我们将本应拆分成的 \(\{l_i, h_i\}\) 变为 \(\{l_i, -h_i\}\)。这样在坐标相等的时候,只需要再对高度进行一个从小到大排序就行了。
遍历
经过上面的排序预处理,我们可以从左到右排序,遇到左端点就将高度加入队列,遇到右端点就将高度移除队列。
每遍历一个节点都需要判断一下堆顶有没有变化,可以使用单边量 prev
记录遍历上一个节点后的堆顶值。如果有变化则进行记录。
这时候又会遇到一个问题,还是看上面的图
在 10 到 15 之间有一小段空白,这也意味着在绿色建筑出队之后,队列为空,还需要对堆为空进行一个特判。那怎么节省这段特判呢?
有一个小技巧是将空白视为高度为0的建筑,这样相当于在队列中永远有一个数值 0 来兜底,保证队列不为空,也就不需要特判了。
代码
class Solution {
public List<List<Integer>> getSkyline(int[][] buildings) {
List<List<Integer>> result = new ArrayList<>();
List<int[]> path = new ArrayList<>();
for(int[] building : buildings){
int left = building[0];
int right = building[1];
int height = building[2];
// 方便排序,让左端点高度为负数;同时能识别当前点为左端点还是右端点
path.add(new int[]{left, -height});
path.add(new int[]{right, height});
}
Collections.sort(path, (a, b) -> {
if(a[0] != b[0]){
return a[0] - b[0];
}
return a[1] - b[1];
});
PriorityQueue<Integer> queue = new PriorityQueue<>((a, b) -> b - a);
int prev = 0;
queue.add(prev);
for(int i = 0; i < path.size(); i++){
int[] temp = path.get(i);
// 如果小于0,说明是左端点,需要入队
if(temp[1] < 0){
queue.add(-temp[1]);
}else{
queue.remove(temp[1]);
}
int current = queue.peek();
if(current != prev){
prev = current;
result.add(Arrays.asList(new Integer[]{temp[0], current}));
}
}
return result;
}
}