天际线,楼宇轮廓问题

系列文章目录

提示:可能写博客会耗费一定的时间,但是只有我站出来讲,写清楚了,才算自己理解清楚,为了逼迫自己学会这个算法,滤清解题的思路,我坚持写!
天际线问题是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
看一下,四栋楼房,在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
图2

  • 另外,一个点x处

你这里有很多楼的边界,有的是楼的右边界,有的又是左边界,你怎么知道,在这点之后,楼是突变高,还是突变低了呢?这就需要我们做一个转化,把楼体离散化为起点,终点,和高度,而且规定:
起点认为是楼增加了h,即新增了一栋楼
终点是楼减少了h,就是楼从此刻开始消失了
比如下图:
图3
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;
        }
    }
  • 好,现在还有个尴尬的事情

假如加了很多楼,都一样高,你们知道啥时候楼还在,楼不在了?
比如下图
图4
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处,你这里有很多楼的边界,有的是楼的右边界(减楼操作),有的又是左边界(新增楼操作),转化为操作时,哪种操作在前面呢?
原则:有新增,也有减楼,那么先增楼,再减楼
这是合理的,你不可能在没有楼的时候先减楼,所以先新增楼;
如下图:
图5
在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只要突变就可以收集结果

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

冰露可乐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值