线段树相关
Hotel
大意:给定\(n\)个旅馆,初始全为空。有两种询问方式。
1.输入\(x\),查找满足连续空旅馆数\(\ge\)x的最小编号,并在此x间开房。
2.输入\(l,r\), 给指定区间退房。
思路
对线段树的节点维护以下信息:
- 该区间的长度\(len\)
- 该区间从左端点开始的最长连续空旅馆长度\(lnum\)
- 该区间从右端点开始的最长连续空旅馆长度\(rnum\)
- 该区间最长的连续空旅馆长度\(num\)
- 懒标记\(lz\) \(\rightarrow\left\{\begin{array}{l}1&覆盖\\2&清空\end{array}\right.\)
修改一段区间时,由于线段树的性质,会将区间分解为若干结点所代表的区间,在覆盖或清空时,可直接将\(lz\)设为\(1/2\),同时将\(lnum,rnum.num\)设为\(0/len\)。具体合并下传等操作见代码。
实现时有一些细节注意一下,要想清楚线段树各项操作的过程,注意当\(n=1\)和对叶子节点的处理。
#include "bits/stdc++.h" using namespace std; class segtree { public: struct node { int lz = 0; int num = 0; int lnum = 0; int rnum = 0; int len; void apply(int l, int r, int v) { if(v == 1) { lz = 1; num = lnum = rnum = 0; } else if(v == 2) { lz = 2; num = lnum = rnum = r - l + 1; } else { num = lnum = rnum = len = r - l + 1; } } }; node unite(const node& a, const node& b) const { node res; res.len = a.len + b.len; if(b.rnum == b.len) { res.rnum = b.rnum + a.rnum; } else { res.rnum = b.rnum; } if(a.lnum == a.len) { res.lnum = a.lnum + b.lnum; } else { res.lnum = a.lnum; } res.num = max(max(res.lnum, res.rnum), a.rnum + b.lnum); res.num = max(res.num, max(a.num, b.num)); return res; } inline void push(int x, int l, int r) { int y = (l + r) >> 1; int z = x + ((y - l + 1) << 1); if (tree[x].lz != 0) { tree[x + 1].apply(l, y, tree[x].lz); tree[z].apply(y + 1, r, tree[x].lz); tree[x].lz = 0; } } inline void pulL(int x, int z) { tree[x] = unite(tree[x + 1], tree[z]);} int n; vector<node> tree; void build(int x, int l, int r) { if (l == r) { tree[x].apply(l, r, 0); return; } int y = (l + r) >> 1; int z = x + ((y - l + 1) << 1); build(x + 1, l, y); build(z, y + 1, r); pulL(x, z); } int get(int x, int l, int r, int p) { int res = -1; if (l == r) { if(tree[x].num >= p) return l; else return -1; } int y = (l + r) >> 1; int z = x + ((y - l + 1) << 1); push(x, l, r); if(tree[x + 1].num >= p) { res = get(x + 1, l, y, p); } else if(tree[x + 1].rnum + tree[z].lnum >= p) { res = y - tree[x + 1].rnum + 1; } else if(tree[z].num >= p) { res = get(z, y + 1, r, p); } pulL(x, z); return res; } template <typename... M> void modify(int x, int l, int r, int lL, int rr, const M&... v) { if (lL <= l && r <= rr) { tree[x].apply(l, r, v...); return; } int y = (l + r) >> 1; int z = x + ((y - l + 1) << 1); push(x, l, r); if (lL <= y) { modify(x + 1, l, y, lL, rr, v...); } if (rr > y) { modify(z, y + 1, r, lL, rr, v...); } pulL(x, z); } segtree(int _n) : n(_n) { assert(n > 0); tree.resize(2 * n - 1); build(0, 0, n - 1); } int get(int p) { return get(0, 0, n - 1, p); } template <typename... M> void modify(int lL, int rr, const M&... v) { assert(0 <= lL && lL <= rr && rr <= n - 1); modify(0, 0, n - 1, lL, rr, v...); } }; void solve() { int n, m; cin >> n >> m; segtree st(n); while(m--) { int op; cin >> op; if(op == 1) { int x; cin >> x; int res = st.get(x); if(res == -1) { cout << 0 << '\n'; } else { cout << res + 1 << '\n'; st.modify(res, res + x - 1, 1); } } else { int x, y; cin >> x >> y; --x; st.modify(x, x + y - 1, 2); } } }
似乎维护最大子段和一类的都是这个套路,某最大的某连续区间满足...要么在左区间,要么在右区间,要么就在两区间合并处。
跳树
大意: 一个兔子在一颗完全二叉树上移动,每次移动可以移动到两个儿子节点或父亲节点,给定一串移动序列,每次询问给定起点,问从起点经过\(l-r\)这段移动,终点的位置。需要支持单点修改
思路
一条路径可以用一个二进制数来表示,从左到右,若是1就向右儿子移动,0则向左儿子移动。在路径的二进制表达中,不体现退格。对于一段\(l-r\)的操作,我们可以将其看作一个整体,当执行这段操作序列时,需要先退格多少,再怎样进行移动。可以发现若当前在s点,执行过后的终点为 \(((s >> backnum) << lenofpath) + path\)。注意到这个几个信息线段树可以维护。
合并代码
node unite(const node& a, const node& b) const {
node res;
//xy意思是需要先向祖先跳几步
if(b.xy < a.len) {
res.path = ((a.path >> b.xy) << b.len) + b.path;
res.xy = a.xy;
res.len = a.len - b.xy + b.len;
}
else {
res.xy = a.xy + b.xy - a.len;
res.path = b.path;
res.len = b.len;
}
return res;
}
修改是单点修改,所以不需要维护懒标记。
[SDOI2009] HH的项链
大意: 给出n个数,q次询问,每次询问要求输出\([l,r]\)有多少个不同的数
思路
先将询问离线,按右端点排序。(套路)
开一个线段树或树状数组维护一段区间中真正有效的点的个数。同样的数取最后出现的为有效的数,枚举右端点时,每次将上一个位置的贡献取消,这个位置的贡献加一。然后对同一个\(r,\)输出所有\(sum(l,r)\)。
关键代码
for(int i = 1; i <= n; i++) {
if(last[a[i]]) {
f.add(last[a[i]], -1);
f.add(i, 1);
last[a[i]] = i;
}
else {
f.add(i, 1);
last[a[i]] = i;
}
while(d[now].r == i) {
ans[d[now].id] = f.sum(d[now].l, d[now].r);
now++;
}
}
Picture
大意:求\(n\)个矩形并起来的外围周长
思路
利用扫描线算法,从下往上扫,可以求出当前图上在这一高度上有多少独立的线段,这些线段的两侧在扫到下一条线时画出的长度就是有效的竖直长度。同理在从左往右扫一遍,把两个结果加起来就是答案。如何求有多少独立的线段呢?可以用线段树维护。
- 该段区间被完整覆盖了\(lz\)次
- 该段区间的独立不相交线段数\(num\)
- 左,右端点是否被覆盖\(lv,rv\)
细节有很多:
\(1.\)区间\([l,r]\)是一段实数区间,在树上使用\([l,r - 1]\)表示。
\(2.\) 对于\(lz\)这个值可以理解为独立的,父节点被覆盖与子节点无关。
\(3.\) 为什么可以不用下传,一是因为查询只查根,二是因为,在修改时,如果是新加一条线段,其子区间的信息不需更新,因为用不到,当别的区间需要访问时,两区间必然相交,最终根节点上的\(num\)结果一样;如果是减掉一条线段,这时这个线段上的区间的全覆盖状态可能取消,这时其\(num\)值需要通过子区间来更新,如果当初下传了的话还得把所有子区间的状态全更新一遍才能确定。
\(4.\) 任何区间修改完都要马上\(pushup\)
\(5.\) 叶子节点不能合并子节点,单独处理
pushup代码
void pushup(int o,int l,int r)
{
if(tree[o].sum)//此区间之前被一整个线段覆盖过
{
tree[o].num=1;
tree[o].len=r-l+1;
tree[o].lflag=tree[o].rflag=1;
}
else if(l==r)//这是一个叶节点
{
tree[o].num=0;
tree[o].len=0;
tree[o].lflag=tree[o].rflag=0;
}
else//一般情况
{
tree[o].num=tree[lson].num+tree[rson].num;
if(tree[lson].rflag&&tree[rson].lflag)tree[o].num--;//flag的用处
tree[o].len=tree[lson].len+tree[rson].len;
tree[o].lflag=tree[lson].lflag;
tree[o].rflag=tree[rson].rflag;
//注意:sum不会被修改,只有当它被一整个线段覆盖时才会修改
}
}

浙公网安备 33010602011771号