算法42:天际线问题(力扣218题)---线段树

2024-02-04 20:44

本文主要是介绍算法42:天际线问题(力扣218题)---线段树,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

218. 天际线问题

城市的 天际线 是从远处观看该城市中所有建筑物形成的轮廓的外部轮廓。给你所有建筑物的位置和高度,请返回 由这些建筑物形成的 天际线 。

每个建筑物的几何信息由数组 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], ...]

示例 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]]

分析:

这一题看起来很复杂,其实掌握了算法40和算法41的知识点以后,分析起来还是很容易的。

1. 首先,我们观察图片发现,天际线搜集的就是每个建筑物的开始坐标和结束坐标。开始坐标就是建筑物的高度。而结束坐标默认搜集高度为0.

2. 如果有第二个建筑物和第一个建筑物有部分重叠,那么第二个建筑物比第一个建筑物高的话,就搜集第二个建筑物开始位置的横坐标和高度;

如果第二个建筑物比第一个建筑物更宽,说明第二个建筑物把第一个建筑物个住当住了,第二个建筑物比第一个建筑物又高又宽,那么直接放弃第一个建筑物搜集的结束点的横坐标和高度信息;搜集第二个建筑物的坐标和高度替换第一个建筑物的结束点信息。当然,第二个建筑物的结束点高度为0.

3. 建筑物给的顺序,是X轴排好序的。因此,每添加一个建筑物,就搜集一下开始点。结束点是需要判断的;

4. 利用线段树的知识点,首先对X轴坐标进行搜集并确认区间;其次,每一个建筑物都有区间,区间的结束点都默认为0;0代表不更新,如果当前区间被之前的建筑物占领了位置,还保留之前的建筑物坐标信息。

5. 以本题第一个案例来分析,首先搜集X轴坐标并划分区间信息:

有了以上信息,我们接下来就是逐步推导的过程了:

由于天际点搜集的是每个区间的开始位置和结束位置;因此,存在连续、重复的信息应该忽略掉后一个重复值。最终搜集的是:

参照上图,根据区间获取X轴坐标值:

1 区间的 10       1区间对应X轴的2, 因此最终是 [2, 10]

2 区间的 15        2区间对应X轴的3, 因此最终是 [3, 15]

4 区间的 12        4区间对应X轴的7, 因此最终是 [7, 12]

6 区间的 0          6区间对应X轴的12, 因此最终是 [12, 0]

7 区间的 10        7区间对应X轴的15, 因此最终是 [15, 10]

9 区间的 8          9区间对应X轴的20, 因此最终是 [20, 8]

10 区间的 0        10区间对应X轴的24, 因此最终是 [24, 0]

最终结果就是 [[2, 10], [3, 15], [7, 12], [12, 0], [15, 10], [20, 8], [24, 0]]

代码实现:

package code04.线段树_02;import java.util.*;//力扣 216,天际线问题 https://leetcode.cn/problems/the-skyline-problem/
public class Code03_SkyLine_2 {class SegmentTree {int[] lines;SegmentTree(int size){lines = new int[size * 4];}//不使用懒更新public void add(int left,int right,int curIndex,int start,int end,int value){//叶子节点if (left == right) {if (left != end) {lines[curIndex] = value > lines[curIndex] ? value : lines[curIndex];}return;}int mid = (left + right)/2;if (start <= mid) {add(left, mid, curIndex * 2, start, end, value);}if (end > mid) {add(mid + 1, right, curIndex * 2 + 1, start, end, value);}}public void query(int left,int right,int curIndex,Map map,List<List<Integer>> list){//叶子节点if (left == right) {/*** 1. 为空直接放入* 2. 不为空,需要判断list最后一个元素*    即最后一个元素的下标为1的位置的值,是否与innerList*    下标为1的值相等。相等则排除,否则加入*/if (list.isEmpty()|| (!list.isEmpty()&& list.get(list.size() - 1) != null&& list.get(list.size() - 1).get(1) != lines[curIndex])) {List<Integer> innerList = new ArrayList<>();//横坐标innerList.add((Integer) map.get(left));//纵坐标innerList.add( lines[curIndex]);list.add(innerList);}return;}int mid = (left + right)/2;query(left, mid, curIndex * 2, map, list);query(mid + 1, right, curIndex * 2 + 1, map, list);}}//根据x轴,按照从左到右、从大到小的顺序编制区间下标public HashMap<Integer, Integer> index(int[][] positions){TreeSet<Integer> pos = new TreeSet<>();//离散化过程,统计开始、结束区间的坐标。//不管数组长度为多少,最终都是落在这些区间中的for (int[] arr : positions) {pos.add(arr[0]);pos.add(arr[1]);}int index = 1;HashMap<Integer, Integer> map = new HashMap<>();//给每个下标编个index,从1开始; 模拟原始线段树的原始数组中给每个元素添加下标的逻辑for (Integer key : pos) {map.put(key, index++);}return map;}//根据区间下标找对应的x轴坐标值public HashMap<Integer, Integer> reverseKeyValue (HashMap<Integer, Integer> map){HashMap reverseMap = new HashMap();for (Iterator iterator = map.keySet().iterator(); iterator.hasNext();) {int key = (int) iterator.next();int value = map.get(key);reverseMap.put(value, key);}return reverseMap;}public List<List<Integer>> getSkyline(int[][] buildings) {//获取到了X轴上对应的下标HashMap<Integer, Integer> map = index(buildings);int size = map.size();SegmentTree tree = new SegmentTree(size);//原始数组的范围int left = 1;int curIndex = 1;int right = size;for (int[] arr : buildings) {//任务的范围int start = map.get(arr[0]);int end = map.get(arr[1]);int value = arr[2];tree.add(left, right, curIndex, start, end, value);}List<List<Integer>> list = new ArrayList<>();HashMap<Integer, Integer> reverseMap = reverseKeyValue(map);tree.query(left, right, curIndex, reverseMap, list);return list;}public static void main(String[] args) {int[][] buildings = {{2,9,10},{3,7,15},{5,12,12},{15,20,10},{19,24,8}};Code03_SkyLine_2 ss = new Code03_SkyLine_2();System.out.println(ss.getSkyline(buildings));}
}

力扣测试结果:

一顿操作猛如虎,结果只打败了 5%,说明代码不够优秀,还需要优化。

优化:

目测我刚刚分析的图片

1、区间的最后一个高度根本就不做考虑,也就是说线段树更新 1 - N,实际上关注的就是 1 到 (N-1)的范围; 这样的话,add方法内部的 

if (left == right) {if (left != end) {lines[curIndex] = value > lines[curIndex] ? value : lines[curIndex];}return;
}

就可以直接去掉  if (left != end)  逻辑判断了。

2. 我们每添加一个建筑物,就递归到子节点。虽然线段树的时间复杂度为O(logN). 但是,执行1次和执行10次这样的时间复杂度方法,时间还是不一样的。因此,需要对目前的add方法进行优化,线段树的懒更新必须加进去

优化代码:

package code04.线段树_02;import java.util.*;//力扣 216,天际线问题 https://leetcode.cn/problems/the-skyline-problem/
public class Code03_SkyLine_2_opt {class SegmentTree {int[] lazy;SegmentTree(int size){lazy = new int[size * 4];}//不使用懒更新public void add(int left,int right,int curIndex,int start,int end,int value){if (start <= left && right <= end) {lazy[curIndex] = value > lazy[curIndex] ? value : lazy[curIndex];return;}int mid = (left + right)/2;pushDown(curIndex);if (start <= mid) {add(left, mid, curIndex * 2, start, end, value);}if (end > mid) {add(mid + 1, right, curIndex * 2 + 1, start, end, value);}}public void pushDown (int curIndex){if (lazy[curIndex] != 0) {lazy[curIndex*2] = lazy[curIndex] > lazy[curIndex * 2] ? lazy[curIndex] : lazy[curIndex * 2] ;lazy[curIndex*2+1] = lazy[curIndex] > lazy[curIndex * 2 + 1] ? lazy[curIndex] : lazy[curIndex * 2 + 1] ;lazy[curIndex] = 0;}}public void query(int left,int right,int curIndex,Map map,List<List<Integer>> list){//叶子节点if (left == right) {if (list.isEmpty()|| (!list.isEmpty()&& list.get(list.size() - 1) != null&& list.get(list.size() - 1).get(1) != lazy[curIndex])) {List<Integer> innerList = new ArrayList<>();//横坐标innerList.add((Integer) map.get(left));//纵坐标innerList.add(lazy[curIndex]);list.add(innerList);}return;}int mid = (left + right)/2;pushDown(curIndex);query(left, mid, curIndex * 2, map, list);query(mid + 1, right, curIndex * 2 + 1, map, list);}}//根据x轴,按照从左到右、从大到小的顺序编制区间下标public HashMap<Integer, Integer> index(int[][] positions){TreeSet<Integer> pos = new TreeSet<>();//离散化过程,统计开始、结束区间的坐标。//不管数组长度为多少,最终都是落在这些区间中的for (int[] arr : positions) {pos.add(arr[0]);pos.add(arr[1]);}int index = 1;HashMap<Integer, Integer> map = new HashMap<>();//给每个下标编个index,从1开始; 模拟原始线段树的原始数组中给每个元素添加下标的逻辑for (Integer key : pos) {map.put(key, index++);}return map;}//根据区间下标找对应的x轴坐标值public HashMap<Integer, Integer> reverseKeyValue (HashMap<Integer, Integer> map){HashMap reverseMap = new HashMap();for (Iterator iterator = map.keySet().iterator(); iterator.hasNext();) {int key = (int) iterator.next();int value = map.get(key);reverseMap.put(value, key);}return reverseMap;}public List<List<Integer>> getSkyline(int[][] buildings) {//获取到了X轴上对应的下标HashMap<Integer, Integer> map = index(buildings);int size = map.size();SegmentTree tree = new SegmentTree(size);//原始数组的范围int left = 1;int curIndex = 1;int right = size;for (int[] arr : buildings) {//任务的范围int start = map.get(arr[0]);int end = map.get(arr[1]);int value = arr[2];//天际线的区间最后一个x坐标的高度信息根本不做考虑,默认就是0.// 因此,start - end的区间,实际考虑的知识 start - (end-1)的范围tree.add(left, right, curIndex, start, end - 1, value);}List<List<Integer>> list = new ArrayList<>();HashMap<Integer, Integer> reverseMap = reverseKeyValue(map);tree.query(left, right, curIndex, reverseMap, list);return list;}public static void main(String[] args) {//int[][] buildings = {{2,9,10},{3,7,15},{5,12,12},{15,20,10},{19,24,8}};//int[][] buildings = {{0,2,3},{2,5,3}};int[][] buildings = {{2,13,10},{10,17,25},{12,20,14}};Code03_SkyLine_2_opt ss = new Code03_SkyLine_2_opt();System.out.println(ss.getSkyline(buildings));}
}

测试结果:打败76%

分析这个问题并且实现第一版代码只花了半天时间,但是优化出第二版代码却花了一整天。

不管是什么算法和数据结构,光掌握原理是远远不够的。熟能生巧,多练、多思考,才能快速写出优秀的代码,这是不可缺少的流程。共勉之!

这篇关于算法42:天际线问题(力扣218题)---线段树的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/678750

相关文章

MySQL 设置AUTO_INCREMENT 无效的问题解决

《MySQL设置AUTO_INCREMENT无效的问题解决》本文主要介绍了MySQL设置AUTO_INCREMENT无效的问题解决,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参... 目录快速设置mysql的auto_increment参数一、修改 AUTO_INCREMENT 的值。

关于跨域无效的问题及解决(java后端方案)

《关于跨域无效的问题及解决(java后端方案)》:本文主要介绍关于跨域无效的问题及解决(java后端方案),具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录通用后端跨域方法1、@CrossOrigin 注解2、springboot2.0 实现WebMvcConfig

Go语言中泄漏缓冲区的问题解决

《Go语言中泄漏缓冲区的问题解决》缓冲区是一种常见的数据结构,常被用于在不同的并发单元之间传递数据,然而,若缓冲区使用不当,就可能引发泄漏缓冲区问题,本文就来介绍一下问题的解决,感兴趣的可以了解一下... 目录引言泄漏缓冲区的基本概念代码示例:泄漏缓冲区的产生项目场景:Web 服务器中的请求缓冲场景描述代码

Java死锁问题解决方案及示例详解

《Java死锁问题解决方案及示例详解》死锁是指两个或多个线程因争夺资源而相互等待,导致所有线程都无法继续执行的一种状态,本文给大家详细介绍了Java死锁问题解决方案详解及实践样例,需要的朋友可以参考下... 目录1、简述死锁的四个必要条件:2、死锁示例代码3、如何检测死锁?3.1 使用 jstack3.2

解决JSONField、JsonProperty不生效的问题

《解决JSONField、JsonProperty不生效的问题》:本文主要介绍解决JSONField、JsonProperty不生效的问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑... 目录jsONField、JsonProperty不生效javascript问题排查总结JSONField

github打不开的问题分析及解决

《github打不开的问题分析及解决》:本文主要介绍github打不开的问题分析及解决,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、找到github.com域名解析的ip地址二、找到github.global.ssl.fastly.net网址解析的ip地址三

MySQL版本问题导致项目无法启动问题的解决方案

《MySQL版本问题导致项目无法启动问题的解决方案》本文记录了一次因MySQL版本不一致导致项目启动失败的经历,详细解析了连接错误的原因,并提供了两种解决方案:调整连接字符串禁用SSL或统一MySQL... 目录本地项目启动报错报错原因:解决方案第一个:第二种:容器启动mysql的坑两种修改时区的方法:本地

springboot加载不到nacos配置中心的配置问题处理

《springboot加载不到nacos配置中心的配置问题处理》:本文主要介绍springboot加载不到nacos配置中心的配置问题处理,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑... 目录springboot加载不到nacos配置中心的配置两种可能Spring Boot 版本Nacos

Java中JSON格式反序列化为Map且保证存取顺序一致的问题

《Java中JSON格式反序列化为Map且保证存取顺序一致的问题》:本文主要介绍Java中JSON格式反序列化为Map且保证存取顺序一致的问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未... 目录背景问题解决方法总结背景做项目涉及两个微服务之间传数据时,需要提供方将Map类型的数据序列化为co

如何解决Druid线程池Cause:java.sql.SQLRecoverableException:IO错误:Socket read timed out的问题

《如何解决Druid线程池Cause:java.sql.SQLRecoverableException:IO错误:Socketreadtimedout的问题》:本文主要介绍解决Druid线程... 目录异常信息触发场景找到版本发布更新的说明从版本更新信息可以看到该默认逻辑已经去除总结异常信息触发场景复