解题报告:LeetCode The Skyline Problem(画天际线)

题目出处:https://leetcode.com/submissions/detail/47013144/
题意描述:

给定一系列矩形的左边坐标Li,右边坐标Ri,和高度Hi(其中Li按照从小到大的顺序排列)。代表城市中一座座高楼。求这些矩形代表的高楼行成的天际线。天际线的定义为:在远处看这些所有的高楼时看到的轮廓。

数据输入:

[ [L1, R1, H1], [Li, Ri, Hi]...]为元素类型为三维向量的向量,其中Li,Ri,Hi的含义见题意描述。且输入数据保证: 0 ≤ Li, Ri ≤ INT_MAX0 < Hi ≤ INT_MAX, 且 Ri - Li > 0

结果输出:

[[x1, y1], [x2, y2]...]为元素类型为pair的向量,其中xi代表天际线中第i个水平线段的端点x坐标值,yi代表该水平线段的高度。值得注意的是,由于城市中地面也行成的天际线也是城市的天际线的一部分,因此输出的最后一个pair的y值必为0,代表天际线从此以后的y值均为0。

解决思路:

 思路一:

  首先,看到这道题,第一个最最简单的思路是直接遍历每个x值,并寻找每个覆盖x值的区间并求出最大值,可是,很明显,高达O(m) 的空间复杂度和O(nm)的时间复杂度(m为最大x值,n为区间个数)肯定会跪,于是我们需要换种想法。

思路二:

  既然我们只关心一段一段的线段,于是联想到可以通过线段树来实现这道题,对于输入的线段进行遍历处理一遍即可。可是,繁琐的建树过程对于我们这些还有无数个大作业小作业的码农来说简直不能忍(其实真实原因只是只知线段树其名,从来没有亲自实现过,不敢贸然入坑),有兴趣的同学可以看看附1的代码实现。当然,线段树还是比较重要的一种数据结构,算法竞赛中都会偶有涉及,对线段树有兴趣的同学可以自行百度或谷歌相关资料。这里只附上noip2012年考过的一道竞赛题“借教室”的结题报告(别问我为何记那么清楚,因为我当初就是采用思路一的那种简单粗暴的方法结果只考了30分)。

思路三:

  既然这两种方法都跪了,那还有什么好点的方法呢?思路二中将线段看为一个整体的思路值得借鉴,虽然不想自己建树,但是可以直接利用C++中现有的树呀。毕竟stl中的map,multimap,set,muiltset可都是一棵棵现成的红黑树。

  于是我们就开始了漫长的查文档的道路,希望从这些模板数据结构中找到可以用到这道题的东西。首先,由于Li,Ri均可能达到INT_MAX的规模,于是我们需要将数据规模为0~INT_MAX的一系列坐标映射到一个合适的表中,即建立一个散列表。multimap就可以实现将线段中端点的x值,y值映射到内部的键值中。于是我们只需要将Li,Ri都扔到muitimap中就可以了。可是接下来问题产生了,将Li,Ri都扔到multimap后我们没法区分那个x值是属于一个矩形的左边的值,那个属于右边的值。一个比较简单的想法是在将Li,Ri扔入muitimap时,同时绑定其高度信息,若为Li,则绑定Hi,若为Ri,则绑定-Hi(感谢朋神提供这个完美的思路给我,手动@GDP)。于是,映射的问题解决了,数据规模变成了我们可接受的20000。

  接下来我们需要解决求解每段线段的高度的问题,一种方法是使用一个优先级队列储存当前x值下能覆盖到该点的所有线段的高度。具体做法如下,首先初始化在优先级队列中插入元素0,接着遍历每个端点(包括左端点和右端点),若为左端点,则将当前对应的高度Hi加入队列中,若为右端点,则将其对应的的高度-Hi的绝对值Hi对应的元素从队列中删除,添加或删除后,即可在O(1)的时间内获取优先级队列中的最大值height,此最大值即为我们所求的当前点的高度。可是,让人失望的是,优先级队列支持了插入操作,不支持任意位置的删除操作,于是我们需要寻找一种既可以自动排序,又可以快速删除的一个模板类来代替优先级队列。显然multiset是个很好的选择,于是我们有了如下的代码(是在不知道multimap应该取什么名字,于是随意取了个A,囧~):

 1 class Solution {
 2 public:
 3     vector<pair<int, int>> getSkyline(vector<vector<int>>& buildings) {
 4         multimap<int, int> A;
 5         vector<pair<int, int>> ans;
 6         for(int i = 0; i < buildings.size(); i ++)
 7         {
 8             A.emplace(buildings[i][0], buildings[i][2]);
 9             A.emplace(buildings[i][1], -buildings[i][2]);
10         };
11 
12         multiset<int> h;
13         h.insert(0);
14         for (std::multimap<int,int>::iterator it = A.begin(); it!=A.end(); ++it)
15         {
16             if(it->second > 0)
17             {
18                 h.insert(it->second);
19             }
20             else
21             {
22                 std::multiset<int>::iterator it_tmp = h.find(-it->second);
23                 h.erase(it_tmp);
24             }
25             int height = *h.rbegin();
26             ans.push_back(pair<int, int>(it->first, height));
27         }
28         return ans;
29     };
30 };

欣喜的在OJ上提交了代码后发现,虽然高度的确求对了,但是并没有进行任何异常处理,比如在输入[ [2 9 10], [3 7 15], [5 12 12], [15 20 10], [19 24 8] ]时,这段代码将给出[ [2 10],[3 15],[5 15],[7 12],[9 12],[12 0],[15 10],[19 10],[20 8],[24 0] ]的输出,显然很多高度相同的地方没有处理就直接输出了。

于是我们需要判定,若当前位置的高度和之前的高度相一致,则不需要压入ans中,于是我们可将第26行代码做如下修改:

if(ans.size() == 0 || height != ans.back().second)
      ans.push_back(pair<int, int>(it->first, height));

做了这项修改后,之前的输入的确对了,可是在输入变为[ [1 3 2],[3, 4, 4] ]时,输出为[ [1 2],[3 0],[3 3],[3 0] ],显然也是错误的,于是我开始了一些列的特判,一些列的尝试,比如对第26行做了如下的修改(代码实在丑的不能看了):

1 if(ans.size() == 0 || height != ans.back().second)
2 {
3     if(ans.size() != 0 && ans.back().first == it->first && ans.back().second < height)
4         ans.pop_back();
5     if(ans.size() == 0 || ((ans.back().first != it->first || ans.back().second < height)
6         &&(height != ans.back().second)))
7         ans.push_back(pair<int, int>(it->first, height));
8 }

最后,不管怎么调整,总是还是有一堆奇怪的bug,而且我的代码调的越来越丑陋,于是我决定放弃,重新开始审视我的bug,我发现,只要我每次将同一个x所对应的所有Hi都处理完毕再求当前高度就可以完美的解决着一系列bug,于是有了如下的最终版本的代码:

 1 class Solution {
 2 public:
 3     vector<pair<int, int>> getSkyline(vector<vector<int>>& buildings) 
 4     {
 5         multimap<int, int> A;
 6         vector<pair<int, int>> ans;
 7         for(int i = 0; i < buildings.size(); i ++)
 8         {
 9             A.emplace(buildings[i][0], buildings[i][2]);
10             A.emplace(buildings[i][1], -buildings[i][2]);
11         };
12         multiset<int> h;
13         h.insert(0);
14         for (std::multimap<int,int>::iterator it = A.begin(); it!=A.end(); ++it)
15         {
16             int key = it->first;
17             while(it != A.end() && it->first == key)
18             {
19                 if(it->second > 0)
20                     h.insert(it->second);
21                 else
22                     h.erase(h.find(-it->second));
23                 it ++;
24             }
25             it --;
26             int height = *h.rbegin();
27             if(ans.size() == 0 || height != ans.back().second)
28                 ans.push_back(pair<int, int>(it->first, height));
29         }
30         return ans;
31     };
32 };

呼呼,终于结束了,今天一天(哦,昨天好像已经过了额!)都在写数据结构的作业效率也是够低的。不过通过这次实验,为了写出简洁高效的代码,自己真是查了好多好多文档呀,耐心和英语水平都受到了极大的考验。

最后,我有个一直没有想明白的问题,为何对于这道题,从OJ上的提交数据来看,用python,java等写出来的运行速度比用C,C++写出来的快得多?是stl写得不好的原因吗?C,C++不是一直以速度著称吗?

附1:使用线段树实现这道题 https://leetcode.com/discuss/65620/share-my-solution-segment-tree-c-888ms

附2:noip2012借教室解题报告 http://hzwer.com/2959.html

此博客中的内容均为原创或来自网络,不用做任何商业用途。欢迎与我交流学习,我的邮箱是b-liu14@mails.tsinghua.edu.cn
原文地址:https://www.cnblogs.com/bill-liu/p/5003831.html