洛谷:P1904:天际线


洛谷:P1904:天际线

题目

P1904:天际线

分析

这是一道离散化的经典例题,但也可以用扫描线解决。离散化已经讲过,而扫描线的介绍参见此处

离散化、排序、去重

每个建筑可以用这样一个结构表示:

struct Building 
{
    int l, h, r; //左边界,高度,右边界
};

... ...
    while (cin >> l >> h >> r)
    {
        buildings.push_back({l, h, r});
    }

    // 离散化:收集所有关键坐标
    vector<int> coords;
    for (const auto& building : buildings)
    {
        coords.push_back(building.left);
        coords.push_back(building.right);
    }

    sort(coords.begin(), coords.end());
    coords.erase(unique(coords.begin(), coords.end()), coords.end());

假定有三个建筑: (1,5,4), (2,5,6), (3,8,7)

显然,坐标排序去重后形成的数组为:

[1, 2, 3, 4, 6, 7]

一共有六个关键点,也就是说在这6个点上高度有变化,但我们没有记录是出现建筑还是建筑结束,也没有记录建筑的高度——这个记录将在下面处理。

找到各区间的最高高度

这三个建筑构成如下的形状:

我们已经整理出6个关键点,它们形成了5个区间。建筑高度只可能在这5个区间中出现。对于每个建筑,它自己的左右边界在这个关键点中肯定会出现,但因为经过了排序,所以位置不定,需要用lower_bound去找出来。同时,在遍历这个区间时,要去看别的建筑(遍历)高度:这个区间的高度只能是最高的那个。

    int n = coords.size(); //有n个独立坐标
    vector<int> maxHeight(n-1, 0); //只能形成n-1个区间

    for (const auto& building : buildings)
    {
        int leftIdx = lower_bound(coords.begin(), coords.end(), building.left) -
                      coords.begin();
        int rightIdx =
            lower_bound(coords.begin(), coords.end(), building.right) -
            coords.begin();

        for (int i = leftIdx; i < rightIdx; i++)
        {
            maxHeight[i] = max(maxHeight[i], building.height);
        }
    }

在上面的代码中,我们遍历所有的building。对于每个building,用lower_boundcoords找到该building影响的范围([leftIdx, rightIdx))——一定能精确找到——并将这个区间的高度通过打擂台的方式更新为最高。如果没有区间永远没有哪个building覆盖,那就是缺省的0

形成轮廓线

得到所有建筑在各自区间对高度的贡献,并最终只保留最高的高度后,我们得到了最终的maxHeight数组。注意:这里离散化的好处就体现出来了,因为我们没有记录一个区间各点的高度,只记录了关键点(也就是高度转折,也就是天际线折点)的高度。

我们遍历maxHeight数组,跟踪当前高度currentHeight(初始化为0),只要高度变化就按照题目要求加入转折点coords[i]和高度maxHeight[i],并更新当前高度。最后输出即可。

但还要注意一个陷阱:

注意到,我们在构造区间最高高度的时候,是设定[leftIdx, rightIdx),也就是说,右边界没有被设置,因为根据我们的定义,右边界的高度一定是下一个折点的高度。而最后一个建筑没有后续建筑,且它的结束点一定也是一个折点,于是我们要再补充设置最后一个坐标coords[n-1]对应了高度0

vector<int> result;
    int currentHeight = 0;

    for (int i = 0; i < n - 1; i++) //注意,我们只有0-(n-2),一共n-1个区间!
    {
        if (maxHeight[i] != currentHeight)
        {
            result.push_back(coords[i]);
            result.push_back(maxHeight[i]);
            currentHeight = maxHeight[i];
        }
    }

    // 最后回到地面高度0
    if (currentHeight != 0)
    {
        result.push_back(coords[n - 1]);
        result.push_back(0);
    }

答案

Solution

思考

这个算法因为要遍历所有的建筑,并遍历每个建筑的左右区间,所以时间复杂度是\(O(N^2)\)。用扫描线算法可以降低时间复杂度,核心代码如下:

#include <bits/stdc++.h>
using namespace std;

struct Building {
    int left, height, right;
};

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    vector<Building> buildings;
    int l, h, r;

    // 读取所有建筑
    while (cin >> l >> h >> r) {
        buildings.push_back({l, h, r});
    }

    // 用事件扫描线直接生成轮廓
    vector<pair<int, int>> events;  // (坐标, 高度变化)
    for (const auto& building : buildings) {
        events.push_back({building.left, building.height});    // 建筑开始
        events.push_back({building.right, -building.height});  // 建筑结束
    }

    // 排序:先按x坐标,再按事件类型(开始事件优先)
    sort(events.begin(), events.end(),
         [](const pair<int, int>& a, const pair<int, int>& b) {
             if (a.first != b.first) return a.first < b.first;
             return a.second > b.second;  // 正数(开始)优先于负数(结束)
         });

    multiset<int> heights;
    heights.insert(0);  // 地面高度

    vector<int> result;
    int currentHeight = 0;

    for (const auto& event : events) {
        int x = event.first;
        int h = event.second;

        if (h > 0) {
            heights.insert(h);
        } else {
            heights.erase(heights.find(-h));
        }

        int newHeight = *heights.rbegin();
        if (newHeight != currentHeight) {
            result.push_back(x);
            result.push_back(newHeight);
            currentHeight = newHeight;
        }
    }

    // 输出
    for (int i = 0; i < result.size(); i++) {
        if (i > 0) cout << " ";
        cout << result[i];
    }
    cout << endl;

    return 0;
}

这里用到了STL中的multiset,因为对于各个关键坐标点,有可能存在相同的高度,而取最高高度的时候不用打擂台,因为multiset会排序高度,将最高的放在最后,用*heights.rbegin()就可以得到。时间复杂度也降低为\(O(n log n)\)

Previous Next