算法分析与设计第四次作业(leetcode 中 The Skyline Problem 问题求解)
心得体会
这个题目是“分治法”分类种的题目,但是自己并没有感受到这个题和分治法有什么关系,甚至因为强行使用这个方法导致把问题搞得很麻烦(debug过程中遇到了很多没有考虑到的边界条件,比如两个房子某条边重合等,然后花了很多时间debug),而且也没能够训练所学到的分治法。
但是也不是什么收获都没有,通过这道题的第二道解法学习,自己了解了C++优先队列的实现方法:
通过堆实现,默认情况是大顶堆,传入的第三个参数默认是<
比较函数。一开始自己无法理解为什么<
对应大顶堆,按照正常思路,如果小于号比较结果为true,就将元素前推,这样最小的元素应该在堆顶,像C++的sort泛型算法就是按照这样的逻辑实现的,如果使用<
作为排序规则则小元素排在队首,而这里恰好相反,所以难以理解。
之后找了很多资料,在《STL源码剖析》一书中看到优先队列具体实现之后发现插入元素push_heap方法实现如下:首先将元素插到最末尾的叶节点,然后将元素向上推,比较的时候将新插入元素作为第二个参数,其父节点作为第一个参数
,如果<
比较返回true,即新插入元素更大,就将新插入元素向上推,这样最大的元素就会排到堆顶,形成大顶堆。
即优先队列利用比较函数的方法和其他算法利用比较函数的方法不同,它是将新加入元素放在比较操作的右端,所以造成比较结果相反。
其它声明:本篇博客的第二种解法参考这篇文章,英其它申明:文讲解简单易懂,自己只是用自己的话将这篇文章的核心部分写出来。
题解正文
题目描述
问题分析
这个题就是要先画出楼房的轮廓然后选取特定顶点得到答案。特定顶点是轮廓线上每个高度对应的起始折点(如上图红点处)。解题难点在于如何在考虑到房屋重叠情况下构建轮廓,另一方面,轮廓的表示方法也很重要,怎样表示更容易解体/更容易理解,这些都是要考虑的问题。
解题思路&复杂度分析
首先说明一下,第一种方法不是很好,写下了只是为了理一下自己当时的解题思路并有一定的借鉴作用,如果想要好好解题还是看改进方法更好。
-
自己做的方法:
分治法的题目应该用分治法做才对,自己首先想到的就是将所有的楼房分成不同的楼盘,然后得到不同的楼盘的点集表示。一个楼盘指的是有重叠的一组连续的楼房组成的集合,比如题目给出的例图中一共有两个楼盘。
对于单个楼盘,我们可以考虑对一个原本为空的点集逐渐添加楼房,然后根据新轮廓形成新的点集(形成过程需要新增、删减一些点),所有楼房添加完毕就能够形成一个楼盘的点集表示。
通过上面两层的分治我们可以得到最终答案,下面是一些实现细节:- 第一层问题是并集求解,每个楼房有leftEdge、rightEdge、height三个参数,发生重叠的楼房的(leftEdge、rightEdge)区间一定是相交的,所以求出楼盘就是求出所有(leftEdge、rightEdge)区间的并集,并集中互不相交的区间就是楼盘。
具体求并集的方法可以看我的Merge Interval题解,先将区间按照leftEdge排序,然后判断后续区间的leftEdge是否落在前一个区间上,如果是就说明相交,需要更新前一区间的rightEdge为max{,}。 - 第二层问题稍微复杂一些,我们需要分析每个房子加入楼盘的过程,对于任意形状的轮廓,加入一个新的楼房可以分为以下两种情况:(暂时不考虑边界情况)
对于第一种情况我们需要添加新房子的左上角作为新的顶点,第二种情况则需要添加height_line和轮廓的交点;然后两种做法都需要添加rightEdge和轮廓线交点;最后去掉被新房子覆盖的点。
反复如上操作,不断加入房子,最后就能根据楼盘的房子集合得到轮廓线的点集表示。
将上面的两层问题的解决方案合并就能完成整个问题求解。因为上面两层每一层的问题都不难解决,所以这种方法是可行的。但是复杂度比较高:首先要求出交集需要遍历一次所有的房子(已经排好序所以无需再排序),复杂度为O(n);然后需要遍历所有房子,并且每次循环进行一次查找操作、两次插入操作、若干次删除操作,这些操作每一个复杂度都是O(n),所以这一步复杂度为O();最终该方法的复杂度为O()。另外,后续考虑边界条件使得编码异常复杂,所以自己的这个方法并不好。
- 第一层问题是并集求解,每个楼房有leftEdge、rightEdge、height三个参数,发生重叠的楼房的(leftEdge、rightEdge)区间一定是相交的,所以求出楼盘就是求出所有(leftEdge、rightEdge)区间的并集,并集中互不相交的区间就是楼盘。
-
更好的做法
- 这个题目很自然地能够想到的解决方法是:求出每一个横坐标单元上的最高高度(覆盖该单元所有房子中最高楼的高度),然后将相邻的、高度相同的单元合并(用区间起点和终点表示),最后去掉每个区间的终点只保留起点就能够得到轮廓线的点集表示。
- 在上述想法基础上还可以进行优化:我们没必要对每个单元都求出最高高度,而只需要在 { , } 所划分出来的一系列区间上求出最高高度就行了,因为在每个区间中都没有发生高度变化,区间内的所有横坐标单元对应的最高高度相同,最终还是会合并成一个区间的,所以可以直接考虑整块区间最高高度情况。
具体的实现方法就是对于每一个区间,遍历所有房子,将该区间的高度设置为覆盖该区间的所有房子中最高楼的高度,双层遍历所以复杂度为O()。 - 还能继续改进,但是这个改进的方法很难想得到:使用一个大顶堆来辅助解题,堆的顺序按照building的高度来决定。然后仍然是顺序遍历所有的区间,每次遇到一个区间端点的时候判断该端点是某个building的leftEdge还是rightEdge,如果是前者就将这个building入堆,否则就从堆中去掉这个building,当入堆、出堆操作完成之后就可以将当前所处在的区间的高度设置为堆顶building(这个building就是最高楼)的高度。这样一次循环之后就能够得出轮廓线的点集表示了。
可能我语言描述的不是很清晰,建议看这篇博客的最后一张动图帮助理解。
这种做法一共n次循环,每次循环只需要常数次的入堆、出堆操作,而入堆出堆操作复杂度为O(log(h)),h是元素总个数,一定有h<n,所以最终总的复杂度一定比O(n*log(n))小。相比前面的做法,有很大的改进。
算法步骤
- 第一种方法:
- 利用求并集的方法,求出不同的楼盘区间;
- 对于每一个楼盘,依次插入楼房,为表示轮廓线的点集添加两个新的顶点、删除所有被新楼房覆盖的点;
- 每一个楼盘轮廓线点集构成之后将点集加入到答案的点集之中,当所有楼盘遍历完毕就能得到答案;
- 第二种方法:(这里采用priority_queue作为解题思路中提到的堆的具体实现)
- 将 { , }中的点进行排序,排序后的队列记作candidates;
- 遍历candidates中所有点
- 如果这个点是未遇见的楼房的leftEdge点,那么将该楼房加入优先队列queue,如果这个点是已经过楼房的rightEdge点,就将该楼房从优先队列queue中删除(具体做法是,用一个count记录已经遇见过的楼房数目,判断当前candidate点是否和第count+1个楼房,
也就是第一个未遇见的楼房
的leftEdge重合,如果重合就将该楼房加入queue,并且将count递增;如果优先队列中的building的rightEdge在当前candidate点左侧那么这个楼房就已经经过,应该去掉,直到没有这样的building时删除停止); - 插入删除操作完成之后更新当前candidate点高度为queue队首楼房高度,并且如果这个高度和答案点集最后一个点高度不同就可以加入到答案点集中;
- 如果这个点是未遇见的楼房的leftEdge点,那么将该楼房加入优先队列queue,如果这个点是已经过楼房的rightEdge点,就将该楼房从优先队列queue中删除(具体做法是,用一个count记录已经遇见过的楼房数目,判断当前candidate点是否和第count+1个楼房,
- 上述所有candidates点遍历完成之后得到答案点集;
代码实现及结果分析
第一种方法(O()复杂度):
class Solution {
public:
void build(vector<vector<int>>& building, vector<pair<int,int>>& res) {
vector<pair<int,int>> bricks;
bricks.push_back({building[0][0], building[0][2]});
bricks.push_back({building[0][1], 0});
building.erase(building.begin());
while ( building.size() != 0 ) {
int beginIndex = -1;
int endIndex = -1;
// 首先找出begin砖块落在的位置
for (int i = 0; i < bricks.size(); ++i) {
if ( building[0][0] >= bricks[i].first ) {
beginIndex = i;
}
if ( building[0][1] >= bricks[i].first ) {
endIndex = i;
}
}
int heightIndex = -1;
for (int i = beginIndex; i < bricks.size(); ++i)
{
if ( bricks[i].second <= building[0][2] ) {
heightIndex = i;
break;
}
}
if ( building[0][1] <= bricks[heightIndex].first ) {
building.erase(building.begin());
continue;
}
// 然后判断砖块属于两种情况的哪一种,第一种是高楼加入,第二种是矮楼加入,判断过后将砖块插入
pair<int, int> endPoint = bricks[endIndex];
if ( building[0][2] > bricks[beginIndex].second ) {
if ( bricks.begin()+beginIndex+1 == bricks.end() ) bricks.push_back({building[0][0],building[0][2]});
else bricks.insert(bricks.begin()+beginIndex+1, {building[0][0],building[0][2]});
} else {
if ( bricks.begin()+heightIndex+1 == bricks.end() ) bricks.push_back({bricks[heightIndex].first,building[0][2]});
else bricks.insert(bricks.begin()+heightIndex+1, {bricks[heightIndex].first,building[0][2]});
}
if (bricks.begin()+endIndex+2 == bricks.end()) bricks.push_back({building[0][1],endPoint.second});
else bricks.insert(bricks.begin()+endIndex+2, {building[0][1],endPoint.second});
// 去掉多余的砖块
if ( bricks[beginIndex].first == building[0][0] && bricks[beginIndex].second < building[0][2] ) {
bricks.erase( bricks.begin() + beginIndex );
} else if ( bricks[beginIndex].second == building[0][2]) {
bricks.erase( bricks.begin() + beginIndex + 1 );
}
for ( auto i = bricks.begin()+beginIndex; i != bricks.end(); ) {
// cout << (bricks.end()-1)->first << endl;
// cout << (bricks.end()-2)->first << endl;
if ( i->first == building[0][1] ) {
// if ((i+1) != bricks.end())
// cout << "ok1" << endl;
// if ((i+1)->first == building[0][1]) cout << i->first << ' ' << i->second << ' '
// << (i+1)->first << ' ' << (i+1)->second << " ok2" << endl;
// if ((i+2)->first == building[0][1]) cout << "test" << endl;
if ((i+1) != bricks.end() && (i+1)->first == building[0][1]) {
bricks.erase(i);
}
break;
}
if ( i->first > building[0][0] && i->first < building[0][1] && i->second < building[0][2] ) {
bricks.erase(i);
} else {
i++;
}
}
building.erase(building.begin());
}
for ( int i = 0; i < bricks.size(); ++i ) {
//cout << bricks[i].first << ' ' << bricks[i].second << endl;
res.push_back(bricks[i]);
}
}
vector<pair<int, int>> getSkyline(vector<vector<int>>& buildings) {
vector<pair<int, int>> res;
vector<vector<int>> building;
if (buildings.size() == 0) {
return res;
}
int begin = *(buildings.begin()->begin());
int end = *(buildings.begin()->begin()+1);
for ( auto i = buildings.begin(); i != buildings.end(); i++ ) {
if ( *(i->begin()) <= end ) {
end = end > *(i->begin()+1) ? end : *(i->begin()+1);
building.push_back(*i);
} else {
build(building, res);
begin = *(i->begin());
end = *(i->begin()+1);
building.clear();
i--;
}
}
build(building, res);
for ( auto i = res.begin()+1; i != res.end(); ) {
//cout << "info: " << i->second << ' ' << (i-1)->second << endl;
if ( i->second == (i-1)->second ) {
res.erase(i);
} else {
i++;
}
}
return res;
}
};
相应的结果:(beat 4%,可以说很差了,并且代码量很大)
第二种方法(复杂度O(n*log(n))):
class Solution {
public:
struct cmp
{
bool operator()(const vector<int>& a, const vector<int>& b) {
return a[2] < b[2];
}
};
vector<pair<int, int>> getSkyline(vector<vector<int>>& buildings) {
vector<pair<int, int>> res;
vector<pair<int, int>> allPoints;
for ( auto i = buildings.begin(); i != buildings.end(); i++ ) {
allPoints.push_back({(*i)[0], (*i)[2]});
allPoints.push_back({(*i)[1], 0});
}
sort(allPoints.begin(), allPoints.end());
priority_queue<vector<int>, vector<vector<int>>, cmp> q;
int addIndex = 0;
for ( auto i = allPoints.begin(); i != allPoints.end(); i++ ) {
while ( addIndex < buildings.size() && buildings[addIndex][0] <= i->first && i->first < buildings[addIndex][1] ) {
q.push(buildings[addIndex]);
addIndex++;
}
while ( !q.empty() && (q.top())[1] <= i->first ) {
q.pop();
}
i->second = q.empty() ? 0 : (q.top())[2];
if ( res.empty() || res.back().second != i->second ) {
res.push_back(*i);
}
}
return res;
}
};
对应的结果:(beat 90+%,相比上一个方法复杂度上改进很大,并且代码量大大减少、易读性更强,也不用额外考虑各种边界条件,鲁棒性更强)