##[$P2894$ [$USACO08FEB$]$Hotel$ $G$](https://www.luogu.com.cn/problem/P2894) ### 一、题目描述 参考样例,第一行输入$n,m$ ,$n$代表有$n$个房间,编号为$1-\sim n$,开始都为 **空房**,$m$表示以下有$m$行操作,以下每行先输入一个数 $op$ ,表示一种操作: 若$op=1$,表示查询房间$query$,再输入一个数$x$,表示在$1 \sim n$ 房间中找到 **长度为$x$的连续空房**,输出连续$x$个房间中 **左端的房间号**,尽量让这个房间号最小: * 若找不到长度为$x$的连续空房,输出$0$ * **若找得到,在这$x$个空房间中住上人** 若$op=2$,表示退房,再输入两个数 $x,y$ 代表 房间号 $x \sim x+y-1$ 退房,即让房间为空。 ### 二、解题思路 ![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/{year}/{month}/{md5}.{extName}/202308230852428.png) ### 三、实现代码 ```cpp {.line-numbers} #include using namespace std; const int N = 200010; // n个房间,m个操作 int n, m; // 线段树,最大空房数,左起最大空房数,右起最大空房数 struct Node { int l, r; // 范围[l,r] int mx; // 区间内最大连续空房数 int lx, rx; // 从左开始或从右开始的最大连续空房数 int tag; // tag:懒标记,1 开房, :2 退房,0: 默认值 } tr[N]; // 根据左右儿子的统计信息,更新父亲的统计信息,也就是更新父亲的统计属性 void pushup(int u) { // 左后+右前,左半边最大长度,右半边最大长度,三者的最大值 // ① mx:区间内最长连续空房 tr[u].mx = max({tr[u << 1].rx + tr[u << 1 | 1].lx, tr[u << 1].mx, tr[u << 1 | 1].mx}); // ② tr[u].lx:父亲节点的最长左前缀,通过tr[u<<1],tr[u<<1|1]的信息来组装 int mid = (tr[u].l + tr[u].r) >> 1; // 这个mid一般都有用,套路性的写上 // mid 是划给左儿子的,所以 mid - tr[u].l + 1 就是左儿子的控制范围,而tr[u].r - mid 就是右儿子的控制范围 // 结合实际场景,利用左右儿子的信息,更新父亲的lx,rx信息 // 如果左儿子整体区间都是空的,那么 父亲的左半边最大长度=左儿子区间长度+右儿子左半边最大长度 if (tr[u << 1].mx == mid - tr[u].l + 1) tr[u].lx = tr[u << 1].mx + tr[u << 1 | 1].lx; else // 否则,左儿子的左半边最大长度,就是父亲的左半边最大长度 tr[u].lx = tr[u << 1].lx; // ③ rx:最长右后缀 // 如果右儿子的整体区间都是空的,那么 父亲的右半边最长度=右儿子区间长度+左儿子右半边最大长度 if (tr[u << 1 | 1].mx == tr[u].r - mid) tr[u].rx = tr[u << 1].rx + tr[u << 1 | 1].mx; else // 否则,右儿子的右半边最大长度,就是父亲的右半边最大长度 tr[u].rx = tr[u << 1 | 1].rx; } // 构建线段树 void build(int u, int l, int r) { tr[u].l = l, tr[u].r = r; // 标记范围 if (l == r) { // 叶子 // 初始时都是空房间,连续空房长度就是区间长度:1, // 从本区间内,左边向右数有1个空白房间, // 从右边向左数,也有1个空白房间 tr[u].mx = tr[u].lx = tr[u].rx = 1; return; } int mid = (l + r) >> 1; build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r); pushup(u); } // 向下传递更新信息 void pushdown(int u) { if (tr[u].tag) { // 如果存在懒标记 tr[u << 1].tag = tr[u << 1 | 1].tag = tr[u].tag; // 向左右儿子传递这个懒标记 // 根据实际的变化情况,修改左右儿子的统计信息 if (tr[u].tag == 1) { // 开房,则全部为1,左、右儿子的三个统计信息都需要修改为0 tr[u << 1].mx = tr[u << 1].lx = tr[u << 1].rx = 0; tr[u << 1 | 1].mx = tr[u << 1 | 1].lx = tr[u << 1 | 1].rx = 0; } else { // 退房 int mid = (tr[u].l + tr[u].r) >> 1; // 退房后,整个区间全是0,左半区间中 左最长=右最长=整个区间长=mid-l+1 // 退房后,整个区间全是0,右半区间中 左最长=右最长=整个区间长=r-mid tr[u << 1].mx = tr[u << 1].lx = tr[u << 1].rx = mid - tr[u].l + 1; tr[u << 1 | 1].mx = tr[u << 1 | 1].lx = tr[u << 1 | 1].rx = tr[u].r - mid; }; // 将本节点的懒标记修改为0,表示已经完成了懒标记的向下传递 tr[u].tag = 0; } } void modify(int u, int l, int r, int v) { if (tr[u].l >= l && tr[u].r <= r) { // 完全包含了当前区间[l,r],那么所有内部节点都需要进行修改 if (v == 1) // 1:开房 tr[u].mx = tr[u].lx = tr[u].rx = 0; // 统计信息全面修改为0,因为全住上人了,没有空的了 else // 2:退房 tr[u].mx = tr[u].lx = tr[u].rx = tr[u].r - tr[u].l + 1; // 退房嘛,整个区间都是空白的,左边最长=右边最长=整个区间最长 // 打上懒标记(有的懒标记是可以累加的,这里是不可以的) tr[u].tag = v; return; } // 下传懒标记 pushdown(u); // 分裂 int mid = (tr[u].l + tr[u].r) >> 1; if (l <= mid) modify(u << 1, l, r, v); if (r > mid) modify(u << 1 | 1, l, r, v); // 向上更新统计信息 pushup(u); } // 以u为根节点的树中,控制范围是[l,r],找出连续空白房间个数>=x的区间,返回此区间的左边界 int query(int u, int x) { // 分裂前先进行懒标记下传 pushdown(u); int mid = (tr[u].l + tr[u].r) >> 1; // 因为要获取最小的房间号,所以,必须是左,中,右这个顺序来进行检查,否则可能返回的不是最小房间号 if (tr[u << 1].mx >= x) return query(u << 1, x); if (tr[u << 1].rx + tr[u << 1 | 1].lx >= x) return mid - tr[u << 1].rx + 1; // 直接找到开始点 if (tr[u << 1 | 1].mx >= x) return query(u << 1 | 1, x); // 如果不存在x这么多个连续空白,则返回0 return 0; } int main() { // 文件输入输出 #ifndef ONLINE_JUDGE freopen("P2894.in", "r", stdin); #endif // 加快读入 ios::sync_with_stdio(false), cin.tie(0); cin >> n >> m; // 构建线段树 build(1, 1, n); while (m--) { int op; cin >> op; if (op == 1) { // 查询连续x个空房 且 入住 int x; cin >> x; if (tr[1].mx >= x) { // 如果目前整体的最大值满足条件,可以找出x个连续空房 int l = query(1, x); printf("%d\n", l); // 如果找到的合适的起点,从l开始,连续x个房间住上人 if (l) modify(1, l, l + x - 1, 1); } else puts("0"); // 找不到x个连续空房 } else { // 退房 int x, y; cin >> x >> y; // 2:退房 modify(1, x, x + y - 1, 2); } } return 0; } ```