经典线段树问题。
Prob.
\(n\) 个房间,两种操作:
1 x
:在 \([1,n]\)
中寻找一段长度为 \(x\)
的空房区间。若存在,入住这些房间,并输出最小的左端点。若不存在,输出
\(0\);
2 l r
:\([l,l+r-1]\)
退房。
Sol.
这道题也算是线段树常规操作了。我们要维护连续的空房区间,想想要维护哪些值?
与分治的思想类似,一段空房区间可以全部在左半部分、全部在右半部分、或者左半部分和右半部分都有一部分。那么我们可以维护三个值:从左端点开始的最长空房区间长度、从右端点开始的最长空房区间长度、以及整个区间的最长空房区间长度。我们把它们分别记为
\(lmax,rmax,maxx\)。
那我们想想线段树的各个模块该如何写。
\(\text{pushup}\)
由于我们要维护三个值,那么上传的时候也要上传三个值。
分情况讨论:
\(lmax\)
如何上传?
显然,当前节点的 \(lmax\)
直接选取左区间的 \(lmax\)
就可以了。但是这里有一种特殊情况:当左区间的 \(lmax\) 等于整段左区间的长度时,\(lmax\)
可以继续延伸至右区间的左端。此时 \(lmax\) 应该为左区间的 \(lmax\) 加上有区间的 \(lmax\)。
1 2
| if(t[ls].lmax == (t[ls].r - t[ls].l + 1)) t[k].lmax = t[ls].lmax + t[rs].lmax; else t[k].lmax = t[ls].lmax;
|
\(rmax\)
如何上传?
与 \(lmax\) 类似,当右区间的 \(rmax\) 等于整段右区间的长度时,\(rmax\)
可以继续延伸至左区间的右端。否则,选取右区间的 \(rmax\)。
1 2
| if(t[rs].rmax == (t[rs].r - t[rs].l + 1)) t[k].rmax = t[rs].rmax + t[ls].rmax; else t[k].rmax = t[rs].rmax;
|
\(maxx\)
如何上传?
显然,\(maxx\)
直接从左端点开始的长度、右端点开始的长度、以及左右都有的长度中选取最大值即可。左右都有的长度就是左区间的
\(rmax\) 加上右区间的 \(lmax\)。
1
| t[k].maxx = max(max(t[ls].maxx, t[rs].maxx), t[ls].rmax + t[rs].lmax);
|
那么,我们的 \(\text{pushup}\)
就可以这么写了:
1 2 3 4 5 6 7 8
| void pushup(int k) { if(t[ls].lmax == (t[ls].r - t[ls].l + 1)) t[k].lmax = t[ls].lmax + t[rs].lmax; else t[k].lmax = t[ls].lmax; if(t[rs].rmax == (t[rs].r - t[rs].l + 1)) t[k].rmax = t[rs].rmax + t[ls].rmax; else t[k].rmax = t[rs].rmax; t[k].maxx = max(max(t[ls].maxx, t[rs].maxx), t[ls].rmax + t[rs].lmax); }
|
\(\text{pushdown}\)
当然,区间修改就一定要打懒标记。虽然这道题目的修改不是常规的数值修改,我们仍然要打
\(tag\)。
注意到修改操作有两种:开房和退房。那么我们的 \(tag\) 可以分别用 \(0/1/2\) 表示 无标记/开房/退房。
那么当 \(tag=1\)
时,整段区间都不可用,\(lmax=rmax=maxx=0\)。
当 \(tag=2\)
时,整段区间都可用,\(lmax=rmax=maxx=len\),\(len\) 为区间长度。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| void pushdown(int k) { if(t[k].tag == 0) return; else if(t[k].tag == 1) { t[ls].tag = t[rs].tag = 1; t[ls].lmax = t[ls].rmax = t[ls].maxx = 0; t[rs].lmax = t[rs].rmax = t[rs].maxx = 0; } else if(t[k].tag == 2) { t[ls].tag = t[rs].tag = 2; t[ls].lmax = t[ls].rmax = t[ls].maxx = t[ls].r - t[ls].l + 1; t[rs].lmax = t[rs].rmax = t[rs].maxx = t[rs].r - t[rs].l + 1; } t[k].tag = 0; }
|
\(\text{build}\)
这个和常规操作没什么不同,只是由于初始全部都是空房,把所有 \(lmax,rmax,maxx\)
赋为当前区间长度就可以了。
\(\text{update}\)
修改操作有两个:开房和退房。但是大体上没什么不同,可以只用一个函数解决。
当是开房操作时,整段区间不可用,\(lmax=rmax=maxx=0\),打上 \(1\) 标记。
当是退房操作时,整段区间都可用,\(lmax=rmax=maxx=len\),打上 \(2\) 标记。
函数添加一个参数 \(type\),\(1/2\) 分别表示 开房/退房 操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| void update(int k, int l, int r, int type) { if(l <= t[k].l && t[k].r <= r) { if(type == 1) { t[k].lmax = t[k].rmax = t[k].maxx = 0; t[k].tag = 1; } else if(type == 2) { t[k].lmax = t[k].rmax = t[k].maxx = t[k].r - t[k].l + 1; t[k].tag = 2; } return; } pushdown(k); int mid = t[k].l + t[k].r >> 1; if(l <= mid) update(ls, l, r, type); if(r > mid) update(rs, l, r, type); pushup(k); }
|
\(\text{query}\)
查询时,要从三部分查询:全部在左区间找、在中间找、全部在右区间找。
这里有一个优先级的问题:由于我们要输出最小的编号,那么应该是左区间→中间→右区间的顺序。
所以,如果左儿子的 \(maxx>=len(len为要查询的长度)\),那么就从左儿子里找。否则,如果左儿子的
\(rmax\) 加上右儿子的 \(lmax\) 大于等于 \(len\)(也就是跨左右儿子的空房区间),那么直接输出这段的左端点。否则,去右儿子里找。
1 2 3 4 5 6 7 8
| int query(int k, int len) { if(t[k].l == t[k].r) return t[k].l; pushdown(k); if(t[ls].maxx >= len) return query(ls, len); else if(t[ls].rmax + t[rs].lmax >= len) return t[ls].r - t[ls].rmax + 1; else return query(rs, len); }
|
Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123
| #include<bits/stdc++.h> using namespace std;
const int MAXN = 5e4 + 7;
#define ls (k << 1) #define rs (k << 1 | 1)
inline int read() { int s = 0, f = 1; char ch = getchar(); while(ch < '0' || ch > '9'){if(ch == '-')f = -1; ch = getchar();} while(ch >= '0' && ch <= '9'){s = s * 10 + ch - '0'; ch = getchar();} return s * f; }
int n, q;
struct Tree { int l, r, tag; int lmax, rmax, maxx; }t[MAXN << 2];
void pushup(int k) { if(t[ls].lmax == (t[ls].r - t[ls].l + 1)) t[k].lmax = t[ls].lmax + t[rs].lmax; else t[k].lmax = t[ls].lmax; if(t[rs].rmax == (t[rs].r - t[rs].l + 1)) t[k].rmax = t[rs].rmax + t[ls].rmax; else t[k].rmax = t[rs].rmax; t[k].maxx = max(max(t[ls].maxx, t[rs].maxx), t[ls].rmax + t[rs].lmax); }
void pushdown(int k) { if(t[k].tag == 0) return; else if(t[k].tag == 1) { t[ls].tag = t[rs].tag = 1; t[ls].lmax = t[ls].rmax = t[ls].maxx = 0; t[rs].lmax = t[rs].rmax = t[rs].maxx = 0; } else if(t[k].tag == 2) { t[ls].tag = t[rs].tag = 2; t[ls].lmax = t[ls].rmax = t[ls].maxx = t[ls].r - t[ls].l + 1; t[rs].lmax = t[rs].rmax = t[rs].maxx = t[rs].r - t[rs].l + 1; } t[k].tag = 0; }
void build(int k, int l, int r) { t[k].tag = 0; t[k].l = l, t[k].r = r; t[k].maxx = t[k].lmax = t[k].rmax = (r - l + 1); if(l == r) return; int mid = l + r >> 1; build(ls, l, mid); build(rs, mid + 1, r); pushup(k); }
void update(int k, int l, int r, int type) { if(l <= t[k].l && t[k].r <= r) { if(type == 1) { t[k].lmax = t[k].rmax = t[k].maxx = 0; t[k].tag = 1; } else if(type == 2) { t[k].lmax = t[k].rmax = t[k].maxx = t[k].r - t[k].l + 1; t[k].tag = 2; } return; } pushdown(k); int mid = t[k].l + t[k].r >> 1; if(l <= mid) update(ls, l, r, type); if(r > mid) update(rs, l, r, type); pushup(k); }
int query(int k, int len) { if(t[k].l == t[k].r) return t[k].l; pushdown(k); if(t[ls].maxx >= len) return query(ls, len); else if(t[ls].rmax + t[rs].lmax >= len) return t[ls].r - t[ls].rmax + 1; else return query(rs, len); }
int main() { n = read(), q = read(); build(1, 1, n); while(q--) { int opt = read(); if(opt == 1) { int len = read(); if(t[1].maxx < len) { printf("0\n"); continue; } int ans = query(1, len); printf("%d\n", ans); update(1, ans, ans + len - 1, 1); } else if(opt == 2) { int l = read(), r = read(); update(1, l, l + r - 1, 2); } } return 0; }
|
Ext.
讲个笑话,这道题我调了三次的原因;
- 没建树
(lmt:我们要有所建树!);
Shift
没按上 (导致 93
行加号打成了等号);
- 从 \(l\) 开 \(r\) 个房,不是从 \(l\) 开到 \(r\)
(所以在 Prob. 里面特意说了是 [l,
r-l+1])。