宾馆房间(线段树 + 最长连续空房)【挺好】
宾馆房间(线段树 + 最长连续空房)
一、题目描述
有 n 个房间(编号 1 ~ n),初始全为空。
维护两种操作:
操作 1(查询入住):1 x
- 在
1 ~ n中找长度为x的连续空房 - 找到后,在这
x个空房中住人 - 输出这段连续空房的最左端房间号
- 如果找不到,输出
0
操作 2(退房):2 x y
- 房间
x ~ x+y-1退房,即让这些房间变空
二、思路分析
线段树维护什么
这道题的核心是维护最长连续空房。
线段树每个节点维护 4 个信息:
pre:从区间左端开始的最长连续空房长度end:从区间右端开始的最长连续空房长度mlen:区间内最长连续空房的长度lazy:区间覆盖标记,-1表示无标记,0表示全空,1表示全满
为什么要维护这三个长度
因为一个区间的最长连续空房,可能:
- 完全在左半 →
seg[lc].mlen - 完全在右半 →
seg[rc].mlen - 跨越左右 →
seg[lc].end + seg[rc].pre
取三者最大值,就是当前区间的 mlen。
push_up 的合并逻辑
seg[p].pre = seg[lc].pre;
if (seg[lc].pre == seg[lc].R - seg[lc].L + 1) seg[p].pre += seg[rc].pre;
如果左半全空,那么整个左半可以接上右半的前缀,pre 要加上 seg[rc].pre。
end 同理:
seg[p].end = seg[rc].end;
if (seg[rc].end == seg[rc].R - seg[rc].L + 1) seg[p].end += seg[lc].end;
mlen 取三者最大值:
seg[p].mlen = max(seg[lc].mlen, seg[rc].mlen);
if (seg[lc].end + seg[rc].pre > seg[p].mlen)
seg[p].mlen = seg[lc].end + seg[rc].pre;
懒标记:区间赋值
lazy = 0:整个区间清空(全空)lazy = 1:整个区间住满(全满)lazy = -1:无标记
下放时,直接根据 k 更新左右儿子的 pre / end / mlen。
查询:找最左的连续 x 个空房
从根开始递归:
- 如果
seg[p].mlen < x,说明当前区间不存在长度为 x 的连续空房,返回 0 - 如果
l == r,返回l - 先看左半:如果
seg[lc].mlen >= x,递归左半 - 再看跨越左右:如果
seg[lc].end + seg[rc].pre >= x,返回seg[lc].R - seg[lc].end + 1 - 否则递归右半
为什么优先看左半:题目要求「尽量让房间号最小」,所以先从左半找,再考虑跨越,最后才去右半。
三、算法流程
- 建树:所有房间初始为空,
pre = end = mlen = 区间长度,lazy = -1 - 对每个操作:
- op = 1:
- 调用
query(1, x)找最左的连续 x 个空房 - 输出位置
pos - 如果
pos != 0,把[pos, pos + x - 1]区间赋值为1(住人)
- 调用
- op = 2:
- 把
[x, x + y - 1]区间赋值为0(退房)
- 把
- op = 1:
四、AC 代码
#include<bits/stdc++.h>
using namespace std;
const int N = 5e4 + 5;
struct node
{
int L, R, lazy;
int pre, end; // 从左端/右端开始的最长连续空房
int mlen; // 区间内最长连续空房
} seg[N * 4];
// 向上合并
void push_up(int p)
{
int lc = p << 1, rc = p << 1 | 1;
// 前缀:左半全空时,可以接上右半的前缀
seg[p].pre = seg[lc].pre;
if (seg[lc].pre == seg[lc].R - seg[lc].L + 1)
seg[p].pre += seg[rc].pre;
// 后缀:右半全空时,可以接上左半的后缀
seg[p].end = seg[rc].end;
if (seg[rc].end == seg[rc].R - seg[rc].L + 1)
seg[p].end += seg[lc].end;
// 最长连续空房:左半、右半、跨越左右,取最大
seg[p].mlen = max(seg[lc].mlen, seg[rc].mlen);
if (seg[lc].end + seg[rc].pre > seg[p].mlen)
seg[p].mlen = seg[lc].end + seg[rc].pre;
}
// 建树:初始全空
void build(int p, int L, int R)
{
seg[p].L = L, seg[p].R = R;
if (L == R)
{
seg[p].mlen = 1;
seg[p].lazy = -1;
seg[p].pre = seg[p].end = 1;
return;
}
int mid = (L + R) >> 1;
build(p << 1, L, mid);
build(p << 1 | 1, mid + 1, R);
push_up(p);
}
// 下放懒标记
void pushdown(int p)
{
if (seg[p].lazy == -1) return;
int lc = p << 1, rc = p << 1 | 1;
int k = seg[p].lazy;
if (k == 1)
{
// 全满:pre = end = mlen = 0
seg[lc].pre = seg[lc].end = seg[lc].mlen = 0;
seg[lc].lazy = k;
seg[rc].pre = seg[rc].end = seg[rc].mlen = 0;
seg[rc].lazy = k;
}
else
{
// 全空:pre = end = mlen = 区间长度
seg[lc].pre = seg[lc].end = seg[lc].mlen = seg[lc].R - seg[lc].L + 1;
seg[lc].lazy = k;
seg[rc].pre = seg[rc].end = seg[rc].mlen = seg[rc].R - seg[rc].L + 1;
seg[rc].lazy = k;
}
seg[p].lazy = -1;
}
// 查询:找最左的连续 x 个空房
int query(int p, int x)
{
if (seg[p].mlen < x) return 0; // 当前区间不存在长度为 x 的连续空房
int l = seg[p].L, r = seg[p].R;
if (l == r) return l;
pushdown(p);
int lc = p << 1, rc = p << 1 | 1;
// 优先看左半
if (seg[lc].mlen >= x) return query(lc, x);
// 跨越左右
if (seg[lc].end + seg[rc].pre >= x)
return seg[lc].R - seg[lc].end + 1;
// 右半
return query(rc, x);
}
// 区间赋值:k = 1 住人,k = 0 退房
void update(int p, int L, int R, int k)
{
int l = seg[p].L, r = seg[p].R;
if (L <= l && r <= R)
{
seg[p].pre = seg[p].end = (k == 1 ? 0 : 1) * (r - l + 1);
seg[p].mlen = (k == 0 ? r - l + 1 : 0);
seg[p].lazy = k;
return;
}
pushdown(p);
int mid = (l + r) >> 1;
if (L <= mid) update(p << 1, L, R, k);
if (R > mid) update(p << 1 | 1, L, R, k);
push_up(p);
}
int main()
{
int n, m;
cin >> n >> m;
int op, x, y;
build(1, 1, n);
while (m--)
{
cin >> op;
if (op == 1)
{
cin >> x;
int pos = query(1, x);
cout << pos << endl;
if (pos != 0)
update(1, pos, pos + x - 1, 1); // 住入
}
else
{
cin >> x >> y;
update(1, x, x + y - 1, 0); // 退房
}
}
return 0;
}
五、复杂度
| 操作 | 时间复杂度 |
|---|---|
| 建树 | O(n) |
| 查询 | O(log n) |
| 更新 | O(log n) |
| 总复杂度 | O(m log n) |
六、相关题目
- 洛谷 P2572 [SCOI2010] 序列操作 — 线段树维护 0/1 信息
- HDU 4614 Vases and Flowers — 线段树 + 二分
七、总结
| 要点 | 内容 |
|---|---|
| 数据结构 | 线段树(维护最长连续空房) |
| 节点信息 | pre / end / mlen / lazy |
| 合并关键 | 左半全空才能延伸前缀,右半全空才能延伸后缀 |
| 查询顺序 | 左半 → 跨越 → 右半(保证最左) |

浙公网安备 33010602011771号