掉落的方块(离散化 + 线段树)

掉落的方块(离散化 + 线段树)

一、题目描述

在二维平面的 x 轴上放置一些方块,每个方块用一个 [left, sideLength] 表示:

  • 左侧边与 x 轴上坐标 left 对齐
  • 边长为 sideLength

每个方块从比目前所有方块都高的位置沿 y 轴下落,直到:

  • 落到另一个方块的顶边,或
  • 落到 x 轴

擦过侧面不算着陆。方块落地后固定不动。

每掉一个方块后,记录当前所有方块堆叠的最高高度。返回一个数组 ans,ans[i] 表示第 i 个方块掉落后堆叠的最高高度。

对应题目:LeetCode 699. 掉落的方块

二、思路分析

问题转化

每个方块落下时,它的高度 = 「它覆盖的 x 区间内,当前所有方块的最大高度」+ 它自己的边长。

因为方块沿 y 轴下落,会落在区间内最高的那个方块上。

关键操作:

  1. 查询:在 [left, left + sideLength - 1] 区间内查询当前最大高度 h
  2. 更新:把该区间的高度统一覆盖为 h + sideLength
  3. 记录当前全局最大值

为什么需要离散化

坐标范围可能很大(left 可达 10^8),但实际出现的端点最多 2n 个。

所以把每个方块的左右端点收集起来,排序去重,得到离散化后的坐标轴。

注意:区间的右端点是 left + sideLength - 1,因为方块是左闭右闭区间。

区间处理:左闭右开

“擦过侧面不算着陆”
假设有两个方块 A 和 B:

方块 A 占据区间 [0, 2](左闭右闭,宽为 2)

方块 B 占据区间 [2, 4](左闭右闭,宽为 2)

如果使用左闭右闭,坐标点 2 既是 A 的右边界,又是 B 的左边界。此时如果方块 B 下落到 A 旁边,由于它们的边界在坐标 2 处重合,程序在处理时很容易误判为“它们在点 2 处发生了重叠着陆”,从而计算出错误的高度。“擦过侧面不算着陆”的物理语义要求边界不能重叠。

线段树维护什么

线段树每个节点维护:

  • v:区间内最大高度
  • lazy:区间覆盖的懒标记(本题是「区间赋值」而非「区间加」)

三、算法流程

  1. 读入所有方块,把每个方块的左右端点加入 poi
  2. 对 poi 排序去重,得到离散化坐标
  3. 建立线段树,初始值全为 0
  4. 对每个方块:
    • 二分找到左右端点在 poi 中的下标 L, R
    • h = query(1, L, R) 查询当前区间最大高度
    • op(1, L, R, h + sideLength) 把该区间覆盖为新高度
    • res.push_back(query(1, 0, len - 1)) 记录全局最大值
  5. 输出 res

四、AC 代码

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

typedef long long ll;

const int N = 1005;
int pos[N][2];
vector<int> poi;

const int maxn = 2 * N;

struct node
{
    int L, R;
    ll v, lazy;
} seg[maxn * 4];

// 向上合并:父节点最大值 = 左右儿子最大值
void push_up(int p)
{
    seg[p].v = max(seg[p << 1].v, seg[p << 1 | 1].v);
}

// 建树:初始值全为 0
void build(int p, int L, int R)
{
    seg[p].L = L, seg[p].R = R, seg[p].lazy = 0;
    seg[p].v = 0;
    if (L == R) return;
    int mid = (L + R) >> 1;
    build(p << 1, L, mid);
    build(p << 1 | 1, mid + 1, R);
}

// 下放懒标记:把父节点的赋值标记传给左右儿子
void pushdown(int p)
{
    if (seg[p].lazy == 0) return;
    int Lc = p << 1, Rc = p << 1 | 1;

    seg[Lc].v = seg[p].lazy;
    seg[Lc].lazy = seg[p].lazy;

    seg[Rc].v = seg[p].lazy;
    seg[Rc].lazy = seg[p].lazy;

    seg[p].lazy = 0;
}

// 区间查询最大值
ll query(int p, int L, int R)
{
    int l = seg[p].L, r = seg[p].R;
    if (L <= l && r <= R) return seg[p].v;

    pushdown(p);
    int mid = (l + r) >> 1;
    ll res = 0;

    if (L <= mid) res = max(query(p << 1, L, R), res);
    if (R > mid) res = max(query(p << 1 | 1, L, R), res);

    return res;
}

// 区间赋值:把 [L, R] 全部赋为 val
void op(int p, int L, int R, ll val)
{
    int l = seg[p].L, r = seg[p].R;
    if (L <= l && r <= R)
    {
        seg[p].v = val;
        seg[p].lazy = val;
        return;
    }

    pushdown(p);
    int mid = (l + r) >> 1;
    if (L <= mid) op(p << 1, L, R, val);
    if (R > mid) op(p << 1 | 1, L, R, val);

    push_up(p);
}

int main()
{
    int n;
    cin >> n;

    vector<ll> res;

    // 收集所有端点
    for (int i = 1; i <= n; i++)
    {
        cin >> pos[i][0] >> pos[i][1];
        poi.push_back(pos[i][0]);
        poi.push_back(pos[i][0] + pos[i][1] - 1);
    }

    // 离散化
    sort(poi.begin(), poi.end());
    poi.erase(unique(poi.begin(), poi.end()), poi.end());

    int len = poi.size();
    build(1, 0, len - 1);

    for (int i = 1; i <= n; i++)
    {
        int L = lower_bound(poi.begin(), poi.end(), pos[i][0]) - poi.begin();
        int R = lower_bound(poi.begin(), poi.end(), pos[i][0] + pos[i][1] - 1) - poi.begin();

        int val = query(1, L, R);           // 查询当前区间最大高度
        op(1, L, R, val + pos[i][1]);       // 区间覆盖为新高度
        res.push_back(query(1, 0, len - 1)); // 记录全局最大值
    }

    for (int i = 0; i < res.size(); i++)
    {
        if (i != 0) cout << ' ';
        cout << res[i];
    }
    cout << endl;

    return 0;
}

五、复杂度

操作 时间复杂度
离散化 O(n log n)
每次查询/更新 O(log n)
总复杂度 O(n log n)

n ≤ 1000,非常轻松。

六、相关题目

七、总结

要点 内容
核心转化 方块高度 = 区间最大高度 + 边长
数据结构 离散化 + 线段树(区间查询 + 区间赋值)
关键区别 懒标记是「赋值」不是「加」
复杂度 O(n log n)
posted @ 2026-10-13 10:37  leaderling  阅读(23)  评论(0)    收藏  举报