一. 题目 Leetcode平台上天际线问题
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).
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], ...]
二.解题思路
萌新在刚刚看到这道题的时候表示是无从下手的,后来在看时辰巨巨的博客时了解到了线段树这个数据结构,感觉这个数据结构真是很牛逼啊,也很适合解决这道题~
首先贴上一个讲解线段树的博客,也是从时辰的博客里看到的,感觉对线段树的讲解的确很生动形象,样例给的也很有启发性。
线段树博客
就像上文博客中作者提到的:
线段树的每个节点上往往都增加了一些其他的域。在这些域中保存了某种动态维护的信息,视不同情况而定。这些域使得线段树具有极大的灵活性,可以适应不同的需求。
这段话体现了线段树灵活性的好处,为了适应这道题的要求,我设计了这样的线段树数据结构:
typedef struct SegmentNode { int start; //线段树的区间左端点 int end; //线段树的区间有端点 int isFlat; //当前区间内的天际线是否是水平的 int height; //如果isFlat等于1,代表当前区间天际线的高度,否则无意义 SegmentNode* left; //左儿子 SegmentNode* right; //右儿子 }SegmentNode;
解题思路是这样的,初始时天际线是一条高度为0的水平线,每添加一个矩形(对应线段树插入节点的操作)就更新天际线。
假设已经有了一条天际线,那么在此基础上添加一个矩形时,对于线段树的某个端点,若其所代表的区间包含新添加矩形所在区间,天际线的变化将会有以下几种可能的情况:
1. 原有的天际线是水平的
这时如果矩形高度小于等于原有高度,那么天际线不会发生变化
如果矩形高度大于原有高度,如果矩形所在区间恰好等于该线段树节点所代表的区间,那么直接更新该节点的height值为矩形高度。如果矩形所在区间小于等于该线段树节点所在区间,那么将该节点的isFlat值重置为0,对该端点左右子树进行递归,直至找到恰好等于矩形所在区间的线段树端点为止,这里需要注意的一点是如果矩形和左右节点所代表的区间都有重合的部分,那么需要将矩形分裂为两个矩形进行操作。
2. 原有的天际线不是水平的,那么对该端点左右子树递归,直至找到isFlat=1的端点为止。
以上过程都是在插入线段树节点过程中进行的,插入节点函数的代码如下:
void insertNode(int l, int r, int h, SegmentNode* tree) //因为这个函数是递归调用的,所以可以保证矩形范围在对应节点区间内 { if(!tree) return; if(tree->isFlat) //当前该区间天际线水平 { int mid = tree->start / 2 + tree->end / 2; if((tree->start % 2) && (tree->end % 2)) mid++; //求该区间的中间节点 if(h <= tree->height) //当前该区间天际线高度大于矩形高度 return; if(tree->start == l && tree->end == r) //当前区间即是矩形区间 { tree->height = h; return; } else if(mid >= r) //矩形全部都在左子树代表的区间中 { tree->isFlat = 0; if(!tree->left) { tree->left = createTree(tree->start, mid); tree->right = createTree(mid, tree->end); tree->right->height = tree->height; tree->left->height = tree->height; } insertNode(l, r, h, tree->left); //insertNode(mid, tree->end, tree->height, tree->right); } else if(mid <= l) //矩形全部都在右子树对应区间中 { tree->isFlat = 0; if(!tree->right) { tree->left = createTree(tree->start, mid); tree->right = createTree(mid, tree->end); tree->left->height = tree->height; tree->right->height = tree->height; } insertNode(l, r, h, tree->right); } else //矩形横跨左右子树区间 { tree->isFlat = 0; if(!tree->left) { tree->left = createTree(tree->start, mid); tree->right = createTree(mid, tree->end); tree->left->height = tree->height; tree->right->height = tree->height; } insertNode(l, mid, h, tree->left); insertNode(mid, r, h, tree->right); } } else //当前线段树不平 { int mid = tree->start / 2 + tree->end / 2; if((tree->start % 2) && (tree->end % 2)) mid++; if(tree->start == l && tree->end == r) { insertNode(l, mid, h, tree->left); insertNode(mid, r, h, tree->right); } else if(mid >= r) { insertNode(l, r, h, tree->left); } else if(mid <= l) { insertNode(l, r, h, tree->right); } else { insertNode(l, mid, h, tree->left); insertNode(mid, r, h, tree->right); } } }
于是在添加完了所有的矩形之后,对线段树进行中序遍历,线段树的所有叶子节点储存的就是天际线的信息了。最后在Leetcode平台上提交的测试代码是这样的(因为之前线段树的代码部分有注释了,这里相应部分就不再写注释了):
typedef struct SegmentNode { int start; int end; int isFlat; int height; SegmentNode* left; SegmentNode* right; }SegmentNode; SegmentNode* createTree(int leftRange, int rightRange) //线段树的初始化 { SegmentNode* tree = new SegmentNode; tree->start = leftRange; tree->end = rightRange; tree->left = NULL; tree->right = NULL; tree->isFlat = 1; tree->height = 0; return tree; } void insertNode(int l, int r, int h, SegmentNode* tree) { if(!tree) return; if(tree->isFlat) { int mid = tree->start / 2 + tree->end / 2; if((tree->start % 2) && (tree->end % 2)) mid++; if(h <= tree->height) return; if(tree->start == l && tree->end == r) { tree->height = h; return; } else if(mid >= r) { tree->isFlat = 0; if(!tree->left) { tree->left = createTree(tree->start, mid); tree->right = createTree(mid, tree->end); tree->right->height = tree->height; tree->left->height = tree->height; } insertNode(l, r, h, tree->left); //insertNode(mid, tree->end, tree->height, tree->right); } else if(mid <= l) { tree->isFlat = 0; if(!tree->right) { tree->left = createTree(tree->start, mid); tree->right = createTree(mid, tree->end); tree->left->height = tree->height; tree->right->height = tree->height; } insertNode(l, r, h, tree->right); } else { tree->isFlat = 0; if(!tree->left) { tree->left = createTree(tree->start, mid); tree->right = createTree(mid, tree->end); tree->left->height = tree->height; tree->right->height = tree->height; } insertNode(l, mid, h, tree->left); insertNode(mid, r, h, tree->right); } } else { int mid = tree->start / 2 + tree->end / 2; if((tree->start % 2) && (tree->end % 2)) mid++; if(tree->start == l && tree->end == r) { insertNode(l, mid, h, tree->left); insertNode(mid, r, h, tree->right); } else if(mid >= r) { insertNode(l, r, h, tree->left); } else if(mid <= l) { insertNode(l, r, h, tree->right); } else { insertNode(l, mid, h, tree->left); insertNode(mid, r, h, tree->right); } } } void getResult(SegmentNode* tree, vector<pair<int, int>>& result, int& lastHeight) { if(!tree) return; pair<int, int> temp; if(tree->isFlat) //如果该节点对应区间是平坦的,得到结果,不然对左右子树进行递归 { temp.first = tree->start; temp.second = tree->height; if(temp.second != lastHeight) //如果该节点高度和上一个节点高度相等,说明天际线不发生弯折,就不输出这个节点信息了 { result.push_back(temp); } lastHeight = temp.second; return; } else { getResult(tree->left, result, lastHeight); getResult(tree->right, result, lastHeight); } } class Solution { public: vector<pair<int, int>> getSkyline(vector<vector<int>>& buildings) { int rightRange = 0; for(int i = 0; i < buildings.size(); i++) { if(buildings[i][1] > rightRange) rightRange = buildings[i][1]; } //确定线段树根节点的区间范围 SegmentNode* tree = createTree(0, rightRange); for(int i = 0; i < buildings.size(); i++) { insertNode(buildings[i][0], buildings[i][1], buildings[i][2], tree); //逐个插入矩形,更新天际线 } vector<pair<int, int>> result; int lastHeight = 0; int lastPos = 0; getResult(tree, result, lastHeight); //得到天际线 pair<int, int> temp; temp.first = rightRange; temp.second = 0; if(rightRange) result.push_back(temp); //添加天际线的最后一个节点 return result; } };
在提交过程中,出现了一些问题:一开始我在建立线段树时直接建立了完全二叉树,结果在测试样例[0, 2147483647, 2147483647]时出现了memory limit exceeded错误,后来改成在新建线段树中只新建根节点,在插入节点过程中再根据需要申请空间,问题得到了解决;还有一些各种各样的错误,基本都是因为自己脑残造成的。。。
最后的提交结果如图所示,可以看到运行速度还是挺快的,可能是因为没有使用STL的结果。
三. 总结
解这道题的过程还是很有收获的,主要体现在对线段树这个数据结构有了初步的了解,调试代码的过程也让我对自己可能会犯什么五花八门的错误有了新的认识,感觉姿势水平得到了提高。最后,如果你看到了我的这篇博客,希望我们共同学习,共同进步~