系列文章目录
提示:可能写博客会耗费一定的时间,但是只有我站出来讲,写清楚了,才算自己理解清楚,为了逼迫自己学会这个算法,滤清解题的思路,我坚持写!
天际线问题是LeetCode的题目:
原题链接:天际线轮廓问题
题目:天际线轮廓问题
提示:这是LeetCode中的难题,但是估计面试考到的可能性不大,熟悉解题思路就行:
城市的 天际线 是从远处观看该城市中所有建筑物形成的轮廓的外部轮廓。给你所有建筑物的位置和高度,请返回 由这些建筑物形成的 天际线 。
每个建筑物的几何信息由数组 buildings 表示,其中三元组 buildings[i] = [lefti, righti, heighti] 表示:
lefti 是第 i 座建筑物左边缘的 x 坐标。
righti 是第 i 座建筑物右边缘的 x 坐标。
heighti 是第 i 座建筑物的高度。
你可以假设所有的建筑都是完美的长方形,在高度为 0 的绝对平坦的表面上。
天际线 应该表示为由 “关键点” 组成的列表,格式 [[x1,y1],[x2,y2],…] ,并按 x 坐标 进行 排序 。关键点是水平线段的左端点。列表中最后一个点是最右侧建筑物的终点,y 坐标始终为 0 ,仅用于标记天际线的终点。此外,任何两个相邻建筑物之间的地面都应被视为天际线轮廓的一部分。
注意:输出天际线中不得有连续的相同高度的水平线。例如 […[2 3], [4 5], [7 5], [11 5], [12 7]…] 是不正确的答案;三条高度为 5 的线应该在最终输出中合并为一个:[…[2 3], [4 5], [12 7], …]
提示:审题,想清楚问题要什么结果,那个坐标咋来的?x,y怎么确定该收集它作为结果?
一、审题
遇到算法题,举例,就慢慢摸清题到底要干嘛,而且也知道究竟是什么时候收集结果?
比如给你这么几栋楼,left,right,h分别表示一栋楼的左边x,右边x,高度h
[1,3,3]
[2,7,1]
[3,6,5]
[4,8,2]
它的轮廓天际线的结果怎么确定?
在这里我们搞一直更直接观一点的结果,用一条线来表示结果,而不是关键点,比如start,end,h
显然结果应该是一个列表List<List< Integer >> ans
ans里面是一个3维元组,代表一个关键线条的起点start,终止点end,高度h
做个图看看那个楼房
看一下,四栋楼房,在1–3之间hmax=3,得一条天际线,[1,3,3]
在3–6之间hmax=5,得一条天际线,[3,6,5]
在6–8之间hmax=2,得一条天际线,[6,8,3]
注意3点:
1)前一个结果的end,是后一个结果的start
2)在每个关键点x,我们只在乎所有楼的最高层hmax
3)什么时候开始收集一条新的天际线呢?当hmax变化的时候,比如,
由于在x=3处,hmax从3变到6,需要收集之前1–3的结果,
由于在x=6处,hmax从5变到2,需要收集之前3–6的结果,
由于在x=8处,hmax从52变到0,需要收集之前6–8的结果
且,当之前的高度为0时,咱不需要收集结果;
所以本题要求啥你已经领会了吧?
二、最优解思路
有了上面审题的分析,我们就知道结果应该要哪些?
那么结果怎么来,显然题目要求结果从左到右依次要有序,因此我们在收答案时势必从左往右收
- 那怎么识别hmax突变了呢
经常有楼高低不同,既然都矗立在那,我们肯定要挑选最高那个楼,这很自然需要一个有序表保存这个点处,很多楼的高度,然后取最高楼,即lastKey()。比如下图x=4处,我们要取6作为这段的高
有序表treeMap需要存入key–value对(不妨起个名字叫:mapXH)
key:x,value:hmax
- 另外,一个点x处
你这里有很多楼的边界,有的是楼的右边界,有的又是左边界,你怎么知道,在这点之后,楼是突变高,还是突变低了呢?这就需要我们做一个转化,把楼体离散化为起点,终点,和高度,而且规定:
起点认为是楼增加了h,即新增了一栋楼,
终点是楼减少了h,就是楼从此刻开始消失了。
比如下图:
3–7有一栋高4 的楼,4–6有一栋高为6的楼
在x=3处,认为新增了一栋高为4的楼
在x=4处,认为新增了一栋高为6的楼
在x=6处,认为减少了一栋高为6的楼
在x=8处,认为减少了一栋高为4的楼
这样的话,我们就把整个楼体输入,改为不同操作的点(设计一个数据结构Operator,它有x,表操作的位置x,isAdd:是增加楼吗?如果是true就是增加楼,否则就是false,即减少一栋楼,然后你楼的高度h是多少?);
//将楼的左右边界,转化为建楼和拆楼的操作
public static class Operator{
public int x;//在那个位置干
public boolean isAdd;//是否是建楼true,拆楼,false
public int h;//不管你拆还是建,这个高是多少呢
public Operator(int x, boolean isAdd, int h){
this.x = x;
this.isAdd = isAdd;
this.h = h;
}
}
- 好,现在还有个尴尬的事情
假如加了很多楼,都一样高,你们知道啥时候楼还在,楼不在了?
比如下图
1–7,2–8,3–9三栋楼一样高为4
在x=4处,它最高楼来了三个操作h=4,而且这个操作的h它出现了3次(times),这个times也需要用一个新的有序表存起来。
有序表treeMap需要存入key–value对(不妨起个名字叫:mapHT)
key:hmax,value:times(一个操作的楼高度h出现了几次)
好,现在重新捋一下咱们针对楼干了啥工作:
来了一堆楼,咱们把它离散化为不同的操作点,
然后从x小点开始进行操作,每次操作更新两个有序表,
一个有序表记录x和hmax,另一个有序表记录这个各种操作h出现了几次times。
- 上面还有一个小问题:
一个点x处,你这里有很多楼的边界,有的是楼的右边界(减楼操作),有的又是左边界(新增楼操作),转化为操作时,哪种操作在前面呢?
原则:有新增,也有减楼,那么先增楼,再减楼
这是合理的,你不可能在没有楼的时候先减楼,所以先新增楼;
如下图:
在x=7处,先是减楼2栋,后又新增2栋,不管如何,我们认为是先建起来2栋楼,再减掉2栋楼,这是合理的自然科学规律。
好,基本要注意的地方,我们都摸清楚了
在离散化输入楼体时,x要升序放,那x处出现不同的操作operation,也要升序放,规则就是刚刚说过的那些注意的地方,我么设计一个比较器opComparator ,用来排序各个operation。
这个比较器的效果是啥呢?
当x不同时,自然让x升序排序
当x相同时,增加楼和减少楼谁先来?增加楼先来
如果x相同,操作全是增加楼,或者操作全是减少楼,谁先来都行,无所谓。
public static class opComparator implements Comparator<Operator> {
@Override
public int compare(Operator o1, Operator o2){
//这个函数深度理解,当返回-1时,o1排在前面,升序,小根堆
//当返回1时,o2排在前面,降序,大根堆
//当返回0,说明两个数一样,谁先后无所谓
if (o1.x != o2.x) return o1.x - o2.x;//如果x不同,那按照x升序排列
//如果x一样,先加,再减
if (o1.isAdd != o2.isAdd) return o1.isAdd ? -1 : 1;//若果x同,但是加减不一,先加
//x同,都是加,或都是减,谁先无所谓
return 0;
}
}
- 现在写解题的大流程:
遍历一遍楼体,将其离散化为operator
然后拿比较器opComparator ,排序这些operator
然后依次遍历这些operator,填写更新好俩有序表mapHT和mapXH
先根据操作填mapHT(mapHT:记录着x处很多操作的h出现了几次?),
每次mapHT填了之后,才能去更新mapXH(mapXH:记录这x和hmax)。(因为同一个x可能出现很多加楼减楼的操作,得把那些mapHT搞定,然后更新mapXH,这个hmax自然是取mapHT中的lastKey()最高高度)
完成上述操作之后,我们有了一个非常完整的有序表:
mapXH:记录这x和hmax
最后,根据这个mapXH来收集和产生我们要的天际线结果:
我们之前说过,hmax突变时,就可以收集答案,因此需要记录之前的x作为一个结果的start,当前的x作为结果的end,之前的高度preH作为高度,收入ans中。
不断地遍历mapXH,一旦遇到hmax突变,就收集一个结果。
//直接大流程
public static List<List<Integer>> getSkyLine(int[][] matrix){
if (matrix == null || matrix.length == 0 || matrix[0].length == 0) return null;
int N = matrix.length;//多少个楼,2倍的操作
//先转化为操作对象x--操作
Operator[] op = new Operator[N << 1];//两个操作
for (int i = 0; i < N; i++) {
op[i << 1] = new Operator(matrix[i][0], true, matrix[i][2]);//建楼
op[(i << 1) + 1] = new Operator(matrix[i][1], false, matrix[i][2]);//拆
}
Arrays.sort(op, new opComparator());
//再根据每一个操作,更新mapHTimes表【高,出现的次数,按照key升序】
//再根据mapHTimes表,更新mapXMaxH表,决定我当前x能看到的最高高度【x,最高高度】
TreeMap<Integer, Integer> mapHTimes = new TreeMap<>();//key:当前遇到的高度,它出现的次数--先填这个有办法填下边这个表
TreeMap<Integer, Integer> mapXMaxH = new TreeMap<>();//key:当前来到了x,目前看到的有效的最高高度
for (int i = 0; i < op.length; i++) {
//更新mapHTimes表,先更新减楼的操作,统计x处,这些操作都出现了多少次?
if (op[i].isAdd){
//如果是需要建楼
if (!mapHTimes.containsKey(op[i].h)) mapHTimes.put(op[i].h, 1);//没有见过这个高
else mapHTimes.put(op[i].h, mapHTimes.get(op[i].h) + 1);//见过
}else {
//如果是需要拆楼
if (mapHTimes.get(op[i].h) == 1) mapHTimes.remove(op[i].h);//如果只剩一栋楼,拆完就没有了
else mapHTimes.put(op[i].h, mapHTimes.get(op[i].h) - 1);//否则降低这个高
}
//此时,mapHTimes已经按照高度升序排列了,你见到的最高高度就是最后一个,也是我们记录的x的最高高度
//更新mapXMaxH表---建楼,拆楼结束后,我们看到的最高高度,这个是记录x处有效最高高度的有序表。
if (mapHTimes.isEmpty()) mapXMaxH.put(op[i].x, 0);//当楼拆完后,高度自然是0
else mapXMaxH.put(op[i].x, mapHTimes.lastKey());//每次操作x之后都取当前能见到的最高楼来安排
}
//然后根据mapXMaxH表,发现高度突变时,增加一段结果
List<List<Integer>> ans = new ArrayList<>();
int start = 0;
int preH = 0;//上次的起点,上次最高楼
for(Map.Entry<Integer, Integer> entry:mapXMaxH.entrySet()){
//用mapXMaxH收集结果:key:x,value:hmax
//看上次与这次高度的变化
int curX = entry.getKey();
int curH = entry.getValue();
if (curH != preH){
//高度突变了,可以收集结果了,而且上次高度不是0才能更新结果
if (preH != 0) ans.add(new ArrayList(Arrays.asList(start, curX, preH)));//第一个地平线不管
//新技巧,之前我都是从新搞一个临时List变量呢,现在不用了,直接仨数,用内置函数变就行了
//每次加完结果,更新
preH = curH;
start = curX;
}
}
return ans;
}
测试:
public static void test(){
int[][] matrix = {
{0,2,3},
{2,5,3}
};
List<List<Integer>> ans = getSkyLine(matrix);
for(List<Integer> line:ans){
for(Integer i : line) System.out.print(i +" ");
System.out.println();
}
}
public static void main(String[] args) {
test();
}
总结
提示:重要知识点:
1)将楼体离散化为各种操作
2)操作的排序原则是x升序,同一个x则增加操作在前面,减少操作在后面
3)要知道结果怎么收集?结果一定死根据mapXH中hmax突变来收集的
4)而mapXH中统计的是最高楼,而统计operation时,mapHT统计了各操作的都出现了多少次,最终当然我们只要最高楼(即lastKey())
5)收集结果时,发现mapXH中的hmax只要突变就可以收集结果