宾馆房间(线段树 + 最长连续空房)【挺好】

宾馆房间(线段树 + 最长连续空房)

一、题目描述

有 n 个房间(编号 1 ~ n),初始全为空。

维护两种操作:

操作 1(查询入住):1 x

  • 在 1 ~ n 中找长度为 x 的连续空房
  • 找到后,在这 x 个空房中住人
  • 输出这段连续空房的最左端房间号
  • 如果找不到,输出 0

操作 2(退房):2 x y

  • 房间 x ~ x+y-1 退房,即让这些房间变空

对应题目:洛谷 P2894 [USACO08FEB] Hotel G

二、思路分析

线段树维护什么

这道题的核心是维护最长连续空房。

线段树每个节点维护 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 个空房

从根开始递归:

  1. 如果 seg[p].mlen < x,说明当前区间不存在长度为 x 的连续空房,返回 0
  2. 如果 l == r,返回 l
  3. 先看左半:如果 seg[lc].mlen >= x,递归左半
  4. 再看跨越左右:如果 seg[lc].end + seg[rc].pre >= x,返回 seg[lc].R - seg[lc].end + 1
  5. 否则递归右半

为什么优先看左半:题目要求「尽量让房间号最小」,所以先从左半找,再考虑跨越,最后才去右半。

三、算法流程

  1. 建树:所有房间初始为空,pre = end = mlen = 区间长度,lazy = -1
  2. 对每个操作:
    • op = 1:
      • 调用 query(1, x) 找最左的连续 x 个空房
      • 输出位置 pos
      • 如果 pos != 0,把 [pos, pos + x - 1] 区间赋值为 1(住人)
    • op = 2:
      • 把 [x, x + y - 1] 区间赋值为 0(退房)

四、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)

六、相关题目

七、总结

要点 内容
数据结构 线段树(维护最长连续空房)
节点信息 pre / end / mlen / lazy
合并关键 左半全空才能延伸前缀,右半全空才能延伸后缀
查询顺序 左半 → 跨越 → 右半(保证最左)
posted @ 2026-10-14 19:43  leaderling  阅读(10)  评论(0)    收藏  举报