LeetCode(218):天际线问题 The Skyline Problem(Java)

2019.9.18 #程序员笔试必备# LeetCode 从零单刷个人笔记整理(持续更新)

这道智力题乍一看可以用动态规划处理,楼高从左至右类似一个入栈出栈的过程(但实际并不能用栈,因为出栈的楼高不一定在栈顶),而且每一次使用的“栈顶元素”是当前最高的楼高。那么只要能想到一种数据结构使之满足这种功能,那就可以想出动态规划的思路了。

动态规划

动态规划算法图解

这种数据结构可以是最大堆(删除复杂度o(n))或者是红黑树(删除复杂度o(logn))。

1.将所有结点高度保存至序列,用负正来区分左右端点。将所有结点按x正序与y正序排序,对于x相同的点,高度较大的点排在较前。

2.建立最大堆,将0入堆(可以用红黑树TreeMap优化remove过程,用计数器标识相同高度即可)。依次遍历所有高度,如果遍历到左端点,将楼高入队;如果遍历到右端点,则移除该高度(该楼遍历结束)。如果当前最大楼高与之前的楼高不相等,作为天际点加入结果。如果当前楼遍历结束,则cur等于0且与pre不同,会将最右侧的(x, 0)加入结果。

归并算法

归并算法图解

这道题还有一种方法可以用归并做,把问题分成若干个子问题。

1.只有一栋楼时,返回该楼左上角顶点和右下角顶点作为天际线坐标。

2.将问题转换为左右两个子问题的归并。归并思想类似排序,将所有天际线结点按横坐标从小到大归并排序。

3.归并左右天际线的所有结点,将横坐标从小到大进行排序,每次选定一个较小的横坐标,更新其纵坐标值(取两楼较大者)。如果当前的y值与上一个天际线点的y值相同,说明该点对应的天际线在前楼投影的内部,不能构成天际线结点,反之可以将其加入结果序列。

4.如果待加入结点的x值与上一个结点的x相同,则只更新y值(避免加入重复结点)。


传送门:天际线问题

A city’s skyline is the outer contour of the silhouette formed by all the buildings in that city when viewed from a distance. Now suppose you are given the locations and height of all the buildings as shown on a cityscape photo (Figure A), write a program to output the skyline formed by these buildings collectively (Figure B).

Buildings Skyline Contour
The geometric information of each building is represented by a triplet of integers [Li, Ri, Hi], where Li and Ri are the x coordinates of the left and right edge of the ith building, respectively, and Hi is its height. It is guaranteed that 0 ≤ Li, Ri ≤ INT_MAX, 0 < Hi ≤ INT_MAX, and Ri - Li > 0. You may assume all buildings are perfect rectangles grounded on an absolutely flat surface at height 0.

For instance, the dimensions of all buildings in Figure A are recorded as: [ [2 9 10], [3 7 15], [5 12 12], [15 20 10], [19 24 8] ] .

The output is a list of “key points” (red dots in Figure B) in the format of [ [x1,y1], [x2, y2], [x3, y3], … ] that uniquely defines a skyline. A key point is the left endpoint of a horizontal line segment. Note that the last key point, where the rightmost building ends, is merely used to mark the termination of the skyline, and always has zero height. Also, the ground in between any two adjacent buildings should be considered part of the skyline contour.

For instance, the skyline in Figure B should be represented as:[ [2 10], [3 15], [7 12], [12 0], [15 10], [20 8], [24, 0] ].

Notes:

·The number of buildings in any input list is guaranteed to be in the range [0, 10000].

·The input list is already sorted in ascending order by the left x position Li.

·The output list must be sorted by the x position.

·There must be no consecutive horizontal lines of equal height in the output skyline.

·For instance, […[2 3], [4 5], [7 5], [11 5], [12 7]…] is not acceptable; the three lines of height 5 should be merged into one in the final output as such: […[2 3], [4 5], [12 7], …]

城市的天际线是从远处观看该城市中所有建筑物形成的轮廓的外部轮廓。现在,假设您获得了城市风光照片(图A)上显示的所有建筑物的位置和高度,请编写一个程序以输出由这些建筑物形成的天际线(图B)。

每个建筑物的几何信息用三元组 [Li,Ri,Hi] 表示,其中 Li 和 Ri 分别是第 i 座建筑物左右边缘的 x 坐标,Hi 是其高度。可以保证 0 ≤ Li, Ri ≤ INT_MAX, 0 < Hi ≤ INT_MAX 和 Ri - Li > 0。您可以假设所有建筑物都是在绝对平坦且高度为 0 的表面上的完美矩形。

例如,图A中所有建筑物的尺寸记录为:[ [2 9 10], [3 7 15], [5 12 12], [15 20 10], [19 24 8] ] 。

输出是以 [ [x1,y1], [x2, y2], [x3, y3], … ] 格式的“关键点”(图B中的红点)的列表,它们唯一地定义了天际线。关键点是水平线段的左端点。请注意,最右侧建筑物的最后一个关键点仅用于标记天际线的终点,并始终为零高度。此外,任何两个相邻建筑物之间的地面都应被视为天际线轮廓的一部分。

例如,图B中的天际线应该表示为:[ [2 10], [3 15], [7 12], [12 0], [15 10], [20 8], [24, 0] ]。

说明:

任何输入列表中的建筑物数量保证在 [0, 10000] 范围内。

输入列表已经按左 x 坐标 Li 进行升序排列。

输出列表必须按 x 位排序。

输出天际线中不得有连续的相同高度的水平线。例如 […[2 3], [4 5], [7 5], [11 5], [12 7]…] 是不正确的答案;三条高度为 5 的线应该在最终输出中合并为一个:[…[2 3], [4 5], [12 7], …]



import java.util.*;

/**
 *
 * https://leetcode.com/problems/the-skyline-problem/
 * https://leetcode-cn.com/problems/the-skyline-problem/
 * 城市的天际线是从远处观看该城市中所有建筑物形成的轮廓的外部轮廓。现在,假设您获得了城市风光照片(图A)上显示的所有建筑物的位置和高度,请编写一个程序以输出由这些建筑物形成的天际线(图B)。
 * 例如,图A中所有建筑物的尺寸记录为:[ [2 9 10], [3 7 15], [5 12 12], [15 20 10], [19 24 8] ] 。
 * 输出是以 [ [x1,y1], [x2, y2], [x3, y3], ... ] 格式的“关键点”(图B中的红点)的列表,它们唯一地定义了天际线。关键点是水平线段的左端点。
 * 请注意,最右侧建筑物的最后一个关键点仅用于标记天际线的终点,并始终为零高度。此外,任何两个相邻建筑物之间的地面都应被视为天际线轮廓的一部分。
 * 图B中的天际线应该表示为:[ [2 10], [3 15], [7 12], [12 0], [15 10], [20 8], [24, 0] ]。
 *
 */

public class TheSkylineProblem {
    //归并算法
    public List<List<Integer>> getSkyline(int[][] buildings) {
        int len = buildings.length;
        List<List<Integer>> result = new ArrayList<>();
        if(len == 0){
            return result;
        }

        //只有一栋楼时,返回该楼左上角顶点和右下角顶点作为天际线坐标
        if(len == 1){
            result.add(new ArrayList<Integer>(){{add(buildings[0][0]); add(buildings[0][2]);}});
            result.add(new ArrayList<Integer>(){{add(buildings[0][1]); add(0);}});
            return result;
        }

        //将问题转换为左右两个子问题的归并
        List<List<Integer>> left = getSkyline(Arrays.copyOfRange(buildings, 0, len >> 1));
        List<List<Integer>> right = getSkyline(Arrays.copyOfRange(buildings, len >> 1, len));
        return mergeSkylines(left, right);
    }

    //左右子问题的所有天际线结点归并,归并思想类似排序,将所有天际线结点按横坐标从小到大归并排序
    public List<List<Integer>> mergeSkylines(List<List<Integer>> left, List<List<Integer>> right){
        int pLeft = 0, pRight = 0;
        int preY = 0, leftY = 0, rightY = 0;
        List<List<Integer>> output = new ArrayList<>();

        //归并左右天际线的所有结点,将横坐标从小到大进行排序,每次选定一个较小的横坐标,更新其纵坐标值(取两楼较大者)
        while(pLeft < left.size() && pRight < right.size()){
            List<Integer> pointLeft = left.get(pLeft);
            List<Integer> pointRight = right.get(pRight);
            int newX, newY;
            if(pointLeft.get(0) < pointRight.get(0)){
                newX = pointLeft.get(0);
                leftY = pointLeft.get(1);
                pLeft++;
            }else{
                newX = pointRight.get(0);
                rightY = pointRight.get(1);
                pRight++;
            }
            newY = leftY > rightY ? leftY : rightY;
            //如果当前的y值与上一个天际线点的y值相同,说明该点对应的天际线在前楼投影的内部,不能构成天际线结点
            if(preY != newY){
                updateOutput(output, newX, newY);
                preY = newY;
            }
        }

        appendSkyline(output, left, pLeft, left.size(), preY);
        appendSkyline(output, right, pRight, right.size(), preY);
        return output;
    }

    //在结果中加入天际线结点(x,y),如果结点的x值与上一个结点的x相同,则更新y值
    public void updateOutput(List<List<Integer>> output, int x, int y){
        if(output.isEmpty() || output.get(output.size() - 1).get(0) != x){
            output.add(new ArrayList<Integer>(){{add(x); add(y);}});
        }else{
            output.get(output.size() - 1).set(1, y);
        }
    }

    //将剩余序列中不在前楼投影内部的结点归并入结果
    public void appendSkyline(List<List<Integer>> output, List<List<Integer>> skyline, int loc, int len, int preY){
        while(loc < len){
            List<Integer> point = skyline.get(loc++);
            if(preY != point.get(1)){
                updateOutput(output, point.get(0), point.get(1));
                preY = point.get(1);
            }
        }
    }

    //动态规划法
    //https://briangordon.github.io/2014/08/the-skyline-problem.html
    public List<List<Integer>> getSkyline2(int[][] buildings) {
        List<List<Integer>> result = new ArrayList<>();
        List<List<Integer>> height = new ArrayList<>();
        //将所有结点高度保存至序列,用负正来区分左右端点
        for(int[] b : buildings){
            height.add(new ArrayList<Integer>(){{add(b[0]); add(-b[2]);}});
            height.add(new ArrayList<Integer>(){{add(b[1]); add(b[2]);}});
        }
        //将所有结点按x正序与y正序排序,对于x相同的点,高度较大的点排在较前
        Collections.sort(height, new Comparator<List<Integer>>() {
            @Override
            public int compare(List<Integer> o1, List<Integer> o2) {
                if(o1.get(0).equals(o2.get(0))){
                   return o1.get(1) - o2.get(1);
                }
                return o1.get(0) - o2.get(0);
            }
        });

        //建立最大堆,将0入堆。(可以用红黑树TreeMap优化remove过程,用计数器标识相同高度即可)
        Queue<Integer> queue = new PriorityQueue<>(Collections.reverseOrder());
        queue.add(0);
        int pre = 0;
        for(List<Integer> h : height){
            //如果遍历到左端点,将楼高入队;如果遍历到右端点,则移除该高度(该楼遍历结束)
            if(h.get(1) < 0){
                queue.add(-h.get(1));
            }else{
                queue.remove(h.get(1));
            }
            //如果当前最大楼高与之前的楼高不相等,作为天际点加入结果。如果当前楼遍历结束,则cur等于0且与pre不同,会将最右侧的(x, 0)加入结果。
            int cur = queue.peek();
            if(pre != cur){
                result.add(new ArrayList<Integer>(){{add(h.get(0)); add(cur);}});
                pre = cur;
            }
        }

        return result;
    }

    //TreeMap改进版
    public List<int[]> getSkyline3(int[][] buildings) {
        List<int[]> heights = new ArrayList<>();
        for (int[] b : buildings) {
            heights.add(new int[]{b[0], -b[2]});
            heights.add(new int[]{b[1], b[2]});
        }
        Collections.sort(heights, (a, b) -> (a[0] == b[0]) ? a[1] - b[1] : a[0] - b[0]);
        TreeMap<Integer, Integer> heightMap = new TreeMap<>(Collections.reverseOrder());
        heightMap.put(0, 1);
        int prevHeight = 0;
        List<int[]> skyLine = new LinkedList<>();
        for (int[] h : heights) {
            if (h[1] < 0) {
                Integer cnt = heightMap.get(-h[1]);
                cnt = (cnt == null) ? 1 : cnt + 1;
                heightMap.put(-h[1], cnt);
            } else {
                Integer cnt = heightMap.get(h[1]);
                if (cnt == 1) {
                    heightMap.remove(h[1]);
                } else {
                    heightMap.put(h[1], cnt - 1);
                }
            }
            int currHeight = heightMap.firstKey();
            if (prevHeight != currHeight) {
                skyLine.add(new int[]{h[0], currHeight});
                prevHeight = currHeight;
            }
        }
        return skyLine;
    }
}




#Coding一小时,Copying一秒钟。留个言点个赞呗,谢谢你#

你可能感兴趣的:(JAVA,数据结构与算法,LeetCode)