信息
力扣上的算法题,都是使用JAVA来完成的,为了复习而写,和答案的思路可能会不一样,错了概不负责,新手,代码也不够简练。
题目描述
标题
天际线问题
难度
困难
描述
城市的天际线是从远处观看该城市中所有建筑物形成的轮廓的外部轮廓。给你所有建筑物的位置和高度,请返回由这些建筑物形成的 天际线
。
每个建筑物的几何信息由数组 buildings
表示,其中三元组 buildings[i] = [lefti, righti, heighti]
表示:
lefti
是第 i
座建筑物左边缘的 x
坐标。
righti
是第 i
座建筑物右边缘的 x
坐标。
heighti
是第 i
座建筑物的高度。
天际线
应该表示为由 “关键点” 组成的列表,格式 [[x1,y1],[x2,y2],...]
,并按 x
坐标 进行 排序 。关键点是水平线段的左端点
。列表中最后一个点是最右侧建筑物的终点,y
坐标始终为 0
,仅用于标记天际线的终点。此外,任何两个相邻建筑物之间的地面都应被视为天际线轮廓的一部分。
注意:输出天际线中不得有连续的相同高度的水平线。例如 [...[2 3], [4 5], [7 5], [11 5], [12 7]...]
是不正确的答案;三条高度为 5 的线应该在最终输出中合并为一个:[...[2 3], [4 5], [12 7], ...]
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/the-skyline-problem
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
示例1
输入:buildings = [[2,9,10],[3,7,15],[5,12,12],[15,20,10],[19,24,8]]
输出:[[2,10],[3,15],[7,12],[12,0],[15,10],[20,8],[24,0]]
解释:
图 A 显示输入的所有建筑物的位置和高度,
图 B 显示由这些建筑物形成的天际线。图 B 中的红点表示输出列表中的关键点。
示例2
输入:buildings = [[0,2,3],[2,5,3]]
输出:[[0,3],[5,0]]
提示
- 1 <= buildings.length <= 104
- 0 <= lefti < righti <= 231 - 1
- 1 <= heighti <= 231 - 1
- buildings 按 lefti 非递减排序
思路
所有楼形成的实际结果,例如例1的图B:
寻找图B和所求天际线有关的信息,我们只需要关注区间[2,12]
,和[15,24]
区域内的情况,天际线实际上和每个x坐标的建筑最高高度有关,所以我们只需要记录已经存在的区域的每点的建筑高度信息即可.比如图B,(x,y,h)
:表示左端点,右端点,高,则只要按顺序记录(2,3,10)
;(3,6,15)
;(6,12,12)
;(15,20,10)
;(20,25,9)
;这样从左开始遍历,即可得到边际线。
设right
为当前所有楼所形成区域的最右端坐标,则当下栋楼i
的左端点buildings[i][0]>right
时候,以后的楼的左边界>=buildings[i][0]
>right
,已经和当前的楼没有任何关联了,所以这时候就可以把当前所有天际线加入结果,清空,再开始处理下面的楼。
为了记录每点的建筑高度信息,可以使用一个链表Tree
表示,
class Tree {
int left;
int right;
int height;
Tree next;
}
一个Tree
对象表示一段区域[left,right]
内的建筑高度均为height
.
其中若t1.next=t2
.则t1
和t2
要表示两个相邻的区域,则,t1.right=t2.left.t1.left<t2.left
.设一串链表的头为head
,尾为tail
。则此串链表表示[head.left, tail.right]
区域内的每点的高度信息.
每个新加入的楼i
对当前已存在的Tree
的影响,比如对Tree
对象t1
的影响,
当buildings[i][0]>=t1.right || buildings[i][1]<=t1.left
时候[buildings[i][0],buildings[i][1]]
和[t1.left, t1.right]
没交集,当楼i
的高度buildings[i][2]<=t1.height
时候,对t1所表示区域的高度也没影响,否则分为4种情况讨论:
情形1
此时t1
所表示区域中被楼i
影响到的只有区域[buildings[i][0],buildings[i][1]]
,这时需要把t1
的右边界缩短为buildings[i][0]
,然后新增两个Tree
即t2
,t3
在t1
后面,t2.left=buildings[i][0]
,t2.right=buildings[i][1]
,t2.height=buildings[i][2]
,t3.left=buildings[i][1]
,t3.right=t1.right
,t3.height=t1.height
.
剩下三中情况就不详细赘述了.
情形2
此时t1
被楼i
覆盖,t1
所表示区域的所有点高度都应变为buildings[i][2]
,只要改变t1.height
即可.
情形3
此时t1
区域内[left,buildings[i][1]]
高度变化,t1
右侧区域没变化.可以选择改变t1
的right
和height
,并增加一个Tree
元素表示区域[buildings[i][1],right]
.
情形4
此种情况原理同情形3类似。
情形5
还有种情况,当楼i
右端点即buildings[i][1]
>此时的右边界时,[right,buildings[i][1]]
这段区间还没有Tree
元素来表示,需在链表末尾添加一个Tree
元素.
所以代码处理流程为:fori
循环处理每栋楼,当当前楼和原来楼没交集,即buildings[i][0]
>right
,处理当前链表,所得天际线存到结果集中;否则,从链表头head
往下(也相当于按照x坐标从左向右),寻找和当前楼有交集的Tree
元素,进行处理.直到流程结束.
代码
import java.util.ArrayList;
import java.util.List;
class Solution {
// 按照每片有交联的楼进行处理。
// 一栋楼一栋楼的添加天际线,一个Tree元素表示一段区域的建筑形成的最高高度,即区间[left, right]上的建筑最高高度为height,为了按照left排序,用链表形式存储数据.
// 这时一片连续区域每点的建筑高度都在Tree元素中了。此时相邻两个Tree元素t1和t2,t1.next=t2.且t1.right=t2.left,t2.left>t1.left;
// 当下一栋楼的左边界即buildings[i][0] > 当前的右边界时,以后楼的左边界都会>=buildings[i][0]>当前右边界,故以后的楼不会影响现在的
// 故可以把当前的天际线存入结果中,相邻的两个Tree元素t1和t2,如果t1.height=t2.height,则应该把new ArrayList<>({t1.left, t1.height})存入结果中,t2的不存,
// 因为t1和t2能合并。
// 然后建立新的链表开始新的合并。直到最后。
private class Tree {
int left;
int right;
int height;
Tree next;
public Tree(int[] building) {
this.left = building[0];
this.right = building[1];
this.height = building[2];
}
public Tree(int left, int right, int height) {
this.left = left;
this.right = right;
this.height = height;
}
}
public List<List<Integer>> getSkyline(int[][] buildings) {
// 由于buildings.length>=1,所以一定有第一栋楼,此时的右边界right即为buildings[0][1]
int right = buildings[0][1];
// 由于开始只有一栋楼,所以现在的区域内[buildings[0][0], buildings[0][1]]内的建筑高度皆为buildings[0][2],一个Tree即可表示
Tree head = new Tree(buildings[0]);
List<List<Integer>> result = new ArrayList<>();
for (int i = 1; i < buildings.length; i++) {
// 当接下来要处理的楼的左边界buildings[i][0] > 此时的右边界right时,以后的楼和现已存在的没关联了,已存的天际线就可加入加过了,并清空链表.
// 加快处理时间
if (buildings[i][0] > right) {
// 当前Tree元素的上一个Tree元素的height,目的是当前Tree元素的高和上一个Tree元素相同时,应该是一个天际线,当前Tree不应该加入结果,
// 由于开始Tree无上一个元素,height=buildings[i][2]>=1,故初始preHeight=0不影响逻辑判断.
int preHeight = 0;
// 循环处理所有Tree,从前向后表示按照Left排序加入结果
while (head != null) {
if (preHeight != head.height) {
List<Integer> tmp = new ArrayList<>();
tmp.add(head.left);
tmp.add(head.height);
result.add(tmp);
}
preHeight = head.height;
head = head.next;
}
// 此时这片楼的右边界也应加入到结果集中
List<Integer> tmp = new ArrayList<>();
tmp.add(right);
tmp.add(0);
result.add(tmp);
// 此前的楼已处理完成,当前楼为下片楼的开始,右边界为当前楼的右边界
right = buildings[i][1];
// 由于开始只有一栋楼,所以现在的区域内[buildings[0][0], buildings[0][1]]内的建筑高度皆为buildings[0][2],一个Tree即可表示
head = new Tree(buildings[i]);
// buildings[i][0] <= right,表示此时这栋楼和原来的楼有关联,需要一个一个的Tree元素判断当前楼对每个Tree所表示的[left, right]区域里的建筑高度的影响
} else {
// 当前处理的元素
Tree ele = head;
// 为了得到最后一个元素,为可能多出来的区域需要添加新的Tree做准备
Tree pre = head;
// 循环处理所有Tree元素,当此时ele.left >= 当前楼的右边界buildings[i][1]时,以后的ele.left也会大于当前楼右边界,即当前楼对ele以后的Tree所表示的区域
// 没影响,因为[buildings[i][0], buildings[i][1]]与区域[ele.left, ele.right]不相交,可以中断循环处理了,缩短处理时间
while (ele != null && ele.left < buildings[i][1]) {
// ele.right <= buildings[i][0]也意味者不相交,当前元素不需要处理,没影响,跳过
// ele.height >= buildings[i][2]时候,当前元素的高度小于ele所表示区域高度,即当前楼对这片区域索看到的建筑高度没影响,跳过
if (ele.right > buildings[i][0] && ele.height < buildings[i][2]) {
// 分四种情况讨论
// 1.当当前楼区域包含ele所表示区域时候,表示ele区域所有点的建筑高度都要更新为buildings[i][2],此时改变ele.height即可达到目的
if (ele.left >= buildings[i][0] && ele.right <= buildings[i][1]) {
ele.height = buildings[i][2];
// 2. 当当前楼的右侧与ele左侧区域相交时候,需要把ele的左侧区域高度变为buildings[i][2],右侧区域高度不变,此时的处理办法为
// ele的高度和右边界改变,同时添加一个Tree元素来表示ele右侧区域
} else if (ele.left >= buildings[i][0]) {
Tree tmp = new Tree(buildings[i][1], ele.right, ele.height);
tmp.next = ele.next;
ele.next = tmp;
ele.height = buildings[i][2];
ele.right = buildings[i][1];
// 3. 原理类似情形2
} else if (ele.right <= buildings[i][1]) {
Tree tmp = new Tree(buildings[i][0], ele.right, buildings[i][2]);
tmp.next = ele.next;
ele.next = tmp;
ele.right = buildings[i][0];
// 4. 当ele所表示区域包含当前楼时,此ele被分为了三部分,所以再增加两个Tree元素表示中间部分和右边部分
} else {
// 右边区域
Tree tmp = new Tree(buildings[i][1], ele.right, ele.height);
tmp.next = ele.next;
// 中间区域
Tree tmp2 = new Tree(buildings[i]);
tmp2.next = tmp;
ele.next = tmp2;
// ele表示左边区域,右边界发生了变化
ele.right = buildings[i][0];
}
}
pre = ele;
ele = ele.next;
}
// 当当前楼的右边界大于当前右边界时,[right, buildings[i][1]]这片区域还没有Tree表示,需要新添加,高度即为buildings[i][2]
if (buildings[i][1] > right) {
pre.next = new Tree(right, buildings[i][1], buildings[i][2]);
right = buildings[i][1];
}
}
}
// 最后一片楼需要处理
int preHeight = 0;
while (head != null) {
if (preHeight != head.height) {
List<Integer> tmp = new ArrayList<>();
tmp.add(head.left);
tmp.add(head.height);
result.add(tmp);
}
preHeight = head.height;
head = head.next;
}
List<Integer> tmp = new ArrayList<>();
tmp.add(right);
tmp.add(0);
result.add(tmp);
return result;
}
}
执行用时和内存消耗还可以.