1. 题号和题目名称
- 天际线问题
2. 题目叙述
城市的天际线是从远处观看该城市中所有建筑物形成的轮廓的外部轮廓。给你所有建筑物的位置和高度,请返回由这些建筑物形成的天际线。
每个建筑物的几何信息由数组 buildings
表示,其中三元组 buildings[i] = [lefti, righti, heighti]
表示:
lefti
是第i
座建筑物左边缘的x
坐标。righti
是第i
座建筑物右边缘的x
坐标。heighti
是第i
座建筑物的高度。
天际线应该表示为由 “关键点” 组成的列表,格式 [[x1,y1],[x2,y2],...]
,并按 x
坐标进行排序。关键点是水平线段的左端点。列表中最后一个点是最右侧建筑物的终点,y
坐标始终为 0 ,仅用于标记天际线的终点。此外,任何两个相邻建筑物之间的地面都应被视为天际线轮廓的一部分。
注意:输出天际线中不得有连续的相同高度的水平线。例如 [...[2 3], [4 3], [5 0]]
是不正确的,应该是 [...[2 3], [5 0]]
。
3. 模式识别
本题可看作是区间合并与高度变化处理的问题。关键在于对所有建筑物的左右边界进行排序,然后动态地维护当前最大高度,当最大高度发生变化时记录关键点。可以使用扫描线算法来解决此类问题。
4. 考点分析
- 扫描线算法:扫描线算法是解决此类区间和高度变化问题的核心方法,需要理解如何通过扫描线从左到右遍历建筑物的边界,动态更新最大高度。
- 优先队列(堆):为了高效地获取当前最大高度,需要使用优先队列(最大堆)来维护当前扫描线经过的建筑物的高度。
- 排序:需要对所有建筑物的左右边界进行排序,以便按照
x
坐标的顺序进行处理。
5. 所有解法
解法一:扫描线算法 + 优先队列
将所有建筑物的左右边界提取出来并排序,使用优先队列(最大堆)来维护当前扫描线经过的建筑物的高度。当扫描线遇到建筑物的左边界时,将该建筑物的高度加入优先队列;当遇到右边界时,将该建筑物的高度从优先队列中移除。每次高度发生变化时,记录关键点。
6. 最优解法(扫描线算法 + 优先队列)的 C 语言代码
/**
* Return an array of arrays of size *returnSize.
* The sizes of the arrays are returned as *returnColumnSizes array.
* Note: Both returned array and *columnSizes array must be malloced, assume caller calls free().
*/
// 定义堆结构体
struct Heap {
int capacity; // 堆的容量,即能容纳的最大元素个数
int size; // 堆中当前元素的个数
int *arr; // 用于存储堆元素的数组
};
// 初始化堆的函数,返回一个初始化好的堆结构体指针
struct Heap* HeapInit(int maxSize)
{
struct Heap* heap;
heap = malloc(sizeof(struct Heap)); // 为堆结构体分配内存
heap->capacity = maxSize; // 设置堆的容量
heap->size = 0; // 初始化堆的元素个数为0
heap->arr = malloc(sizeof(int)*(maxSize+1)); // 为堆数组分配内存,+1是因为堆从下标1开始存储元素
return heap; // 返回初始化好的堆结构体指针
}
// 向堆中插入元素的函数
void HeapInsert(int x, struct Heap *obj)
{
int i;
obj->size += 1; // 堆的元素个数加1
for (i=obj->size; i!=1 && x>(obj->arr)[i/2]; i=i/2) {
// 将父节点的值下移
(obj->arr)[i] = (obj->arr)[i/2];
}
(obj->arr)[i] = x; // 将新元素插入到合适的位置
return;
}
// 从堆中删除最大值的函数,返回被删除的最大值
int HeapDeleteMax(struct Heap *obj)
{
int ret = (obj->arr)[1]; // 保存堆顶元素(最大值)
int x = (obj->arr)[obj->size]; // 取出堆的最后一个元素
int i, child;
obj->size -= 1; // 堆的元素个数减1
for (i=1; i<=obj->size/2;i=child) {
child = i*2; // 左子节点的下标
if (child < obj->size && (obj->arr)[child]<(obj->arr)[child+1]) {
// 如果右子节点存在且右子节点更大,则选择右子节点
child++;
}
if (x < (obj->arr)[child])
// 将较大子节点的值上移
(obj->arr)[i] = (obj->arr)[child];
else
break;
}
(obj->arr)[i] = x; // 将最后一个元素插入到合适的位置
return ret; // 返回被删除的最大值
}
// 从堆中删除指定元素的函数
void HeapDelete(int x, struct Heap *obj)
{
int i;
for(i=1; i<=obj->size; i++) {
if (x == (obj->arr)[i]) {
break;
}
}
if (i > obj->size)
return;
// 节点x上滤
for (; i!=1; i=i/2) {
(obj->arr)[i] = (obj->arr)[i/2];
}
(obj->arr)[i] = INT_MAX;
HeapDeleteMax(obj); // 删除堆顶的INT_MAX(即刚才上移的元素)
return;
}
// 获取堆中最大值的函数
int HeapGetMax(struct Heap *obj)
{
if (obj->size > 0) {
return (obj->arr)[1]; // 如果堆不为空,返回堆顶元素(最大值)
} else {
return 0; // 如果堆为空,返回0
}
}
// 打印堆中元素的函数,用于调试
void HeapPrint(struct Heap *obj)
{
int i;
for(i=1; i<=obj->size; i++) {
printf("%d ", (obj->arr)[i]); // 依次打印堆中的元素
}
printf("\n");
}
// 计算绝对值的函数
int Abs(int a)
{
return a<0?-a:a; // 如果a小于0,返回-a,否则返回a
}
// 用于qsort的比较函数,比较两个数组(代表点)的大小
int arraycmp(const void *elem1,const void *elem2)
{
int ret;
int a, b;
ret = (*((int**)elem1))[0]-(*((int**)elem2))[0]; // 先比较横坐标
if (ret == 0) {
a = -(*((int**)elem2))[1];
b = -(*((int**)elem1))[1];
ret = a - b; // 如果横坐标相同,比较纵坐标(取负数是为了排序顺序符合需求)
}
return ret;
}
// 主函数,计算天际线的函数
int** getSkyline(int** buildings, int buildingsSize, int* buildingsColSize, int* returnSize, int** returnColumnSizes){
int **vetex; // 用于存储建筑物的顶点(左边界和右边界点)
int vetexCount = buildingsSize*2; // 顶点的数量,每个建筑物有两个顶点
int **ret; // 用于存储最终结果(天际线的转折点)
int retCount; // 结果数组中元素的个数
struct Heap *heap; // 堆结构体指针
int i;
int lastHeight; // 记录上一次的最大高度
/*
step 1 找出建筑物所有左上、右上的点,存入vetex中。左上的点,高取负数
*/
vetex = malloc(sizeof(int*)*vetexCount); // 为顶点数组分配内存
for (i=0; i<vetexCount; i++) {
vetex[i] = malloc(sizeof(int)*2); // 为每个顶点分配内存
}
for (i=0; i<buildingsSize; i++) {
vetex[i*2][0] = buildings[i][0]; // 左边界点的横坐标
vetex[i*2][1] = -buildings[i][2]; // 左边界点的纵坐标(取负数)
vetex[i*2+1][0] = buildings[i][1]; // 右边界点的横坐标
vetex[i*2+1][1] = buildings[i][2]; // 右边界点的纵坐标
}
qsort(vetex, vetexCount, sizeof(int*), arraycmp); // 对点数组进行排序
// for(i=0; i<vetexCount; i++) {
// printf("(%d %d)", vetex[i][0], vetex[i][1]);
// }
// printf("\n");
/*
调试堆
*/
// heap = HeapInit(10);
// HeapInsert(8, heap);
// HeapInsert(20, heap);
// HeapInsert(2, heap);
// HeapInsert(15, heap);
// HeapInsert(19, heap);
// HeapPrint(heap);
// HeapDelete(19, heap);
// printf("\n");
// HeapPrint(heap);
/*
step 2 扫描线从左向右扫描,获取转折点
1)扫描到建筑物的左边界时,将高存入堆
2)扫描到建筑物的右边界时,将高从堆中取出
3)当存入、取出时,最大高度发生变化时,遇到转折点。算出新的height,转折点为 当前节点,height
*/
heap = HeapInit(buildingsSize+1); // 初始化堆,容量为建筑物数量+1
retCount = 0; // 结果数组元素个数初始化为0
ret = malloc(sizeof(int*)*vetexCount); // 为结果数组分配内存
lastHeight = 0; // 初始最大高度为0
for (i=0; i<vetexCount; i++) {
// printf("i=%d size=%d\n",i, heap->size);
if (vetex[i][1] < 0) {
HeapInsert(-vetex[i][1], heap); // 如果是左边界点,将高度插入堆中
} else {
HeapDelete(vetex[i][1], heap); // 如果是右边界点,从堆中删除高度
}
// HeapPrint(heap);
// printf("1111111\n");
if (lastHeight != HeapGetMax(heap)) {
lastHeight = HeapGetMax(heap); // 更新最大高度
//get i, lastHeight;
ret[retCount] = malloc(sizeof(int)*2); // 为结果数组的新元素分配内存
ret[retCount][0] = vetex[i][0]; // 记录转折点的横坐标
ret[retCount][1] = HeapGetMax(heap); // 记录转折点的纵坐标
retCount++; // 结果数组元素个数加1
}
}
*returnColumnSizes = malloc(sizeof(int)*retCount); // 为返回的列大小数组分配内存
for (i=0; i<retCount; i++) {
(*returnColumnSizes)[i] = 2; // 结果数组中每个元素的列数为2
}
*returnSize = retCount; // 设置返回的结果数组大小
return ret; // 返回结果数组
}
7. 复杂度分析
- 时间复杂度: O ( n l o g n ) O(n log n) O(nlogn),其中 n n n 是建筑物的数量。主要时间开销在于对事件数组进行排序,时间复杂度为 O ( n l o g n ) O(n log n) O(nlogn),以及在优先队列中进行插入和删除操作,每次操作的时间复杂度为 O ( l o g n ) O(log n) O(logn),总共需要进行 O ( n ) O(n) O(n) 次操作。
- 空间复杂度: O ( n ) O(n) O(n),主要空间开销在于存储事件数组和优先队列,事件数组的大小为 2 n 2n 2n,优先队列的最大容量为 n n n。