掉落的方块(离散化 + 线段树)
掉落的方块(离散化 + 线段树)
一、题目描述
在二维平面的 x 轴上放置一些方块,每个方块用一个 [left, sideLength] 表示:
- 左侧边与 x 轴上坐标
left对齐 - 边长为
sideLength
每个方块从比目前所有方块都高的位置沿 y 轴下落,直到:
- 落到另一个方块的顶边,或
- 落到 x 轴
擦过侧面不算着陆。方块落地后固定不动。
每掉一个方块后,记录当前所有方块堆叠的最高高度。返回一个数组 ans,ans[i] 表示第 i 个方块掉落后堆叠的最高高度。
对应题目:LeetCode 699. 掉落的方块
二、思路分析
问题转化
每个方块落下时,它的高度 = 「它覆盖的 x 区间内,当前所有方块的最大高度」+ 它自己的边长。
因为方块沿 y 轴下落,会落在区间内最高的那个方块上。
关键操作:
- 查询:在
[left, left + sideLength - 1]区间内查询当前最大高度h - 更新:把该区间的高度统一覆盖为
h + sideLength - 记录当前全局最大值
为什么需要离散化
坐标范围可能很大(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:区间覆盖的懒标记(本题是「区间赋值」而非「区间加」)
三、算法流程
- 读入所有方块,把每个方块的左右端点加入
poi - 对
poi排序去重,得到离散化坐标 - 建立线段树,初始值全为 0
- 对每个方块:
- 二分找到左右端点在
poi中的下标L, R h = query(1, L, R)查询当前区间最大高度op(1, L, R, h + sideLength)把该区间覆盖为新高度res.push_back(query(1, 0, len - 1))记录全局最大值
- 二分找到左右端点在
- 输出
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,非常轻松。
六、相关题目
- LeetCode 699. 掉落的方块 — 本题
- 洛谷 P2894 [USACO08FEB] Hotel G — 区间赋值线段树
- 洛谷 P2572 [SCOI2010] 序列操作 — 复杂懒标记
七、总结
| 要点 | 内容 |
|---|---|
| 核心转化 | 方块高度 = 区间最大高度 + 边长 |
| 数据结构 | 离散化 + 线段树(区间查询 + 区间赋值) |
| 关键区别 | 懒标记是「赋值」不是「加」 |
| 复杂度 | O(n log n) |

浙公网安备 33010602011771号