线段树是算法竞赛中常用的用来维护 区间信息 的数据结构。
线段树可以在 O ( l o g N ) O(logN) O(logN) 的时间复杂度内实现单点修改、区间修改、区间查询(区间求和,求区间最大值,求区间最小值)等操作。
线段树将每个长度不为 1 1 1 的区间划分成左右两个区间递归求解,把整个线段划分为一个树形结构,通过合并左右两区间信息来求得该区间的信息。这种数据结构可以方便的进行大部分的区间操作。
线段树的结构如下:
简单的来讲,对于线段树内任意一颗非叶子结点而言,他表示的区间是 [ l , r ] [l,r] [l,r] ,其左儿子表示的区间是 [ l , m i d ] [l, mid] [l,mid] ,其右儿子表示的区间是 [ m i d + 1 , r ] [mid+1, r] [mid+1,r] ,其中 m i d = ⌊ l + r 2 ⌋ mid=\lfloor\frac{l+r}2\rfloor mid=⌊2l+r⌋ 。
如果 n n n 为叶子结点数量,那么数组要开 2 l o g ( n ) + 1 2^{log(n)+1} 2log(n)+1 这么大,如果懒得算就开直接 4n。
考虑递归建树,dfs遍历一遍即可,要在回溯时pushup(即维护当前区间的值,因为下面更改了)。
单点修改相当于 l = r l = r l=r 的情况 , 我就不啰嗦了。
可以考虑直接修改表示区间被 [ l , r ] [l,r] [l,r] 包含的所有节点。但如果这样做时间肯定爆,所以我们采用 l a z y t a g lazytag lazytag 技术。
就是懒标记拉。我们令 t a g [ k ] tag[k] tag[k] 表示节点 k k k 欠下其子树的更新值,区间修改时点到即止(即不去更新被完全包含的结点的子树),然后呢什么时候访问到这个结点再下传标记给它的子节点(注意修改时也要下传,因为如果不下传标记,子节点会pushup一个错误额值)。
我们可以记下传标记为pushdown,这个操作实在接着向下递归之前。
直接将所有被 [ l , r ] [l,r] [l,r] 包含的区间合并起来即可,记得下传标记。
struct SegamentTree {
static const int MAX = 1e5;
struct TreeNode {
ll val, tag;
}tr[MAX<<2];
inline void pushup(int &k) {
tr[k].val = tr[k<<1].val + tr[k<<1|1].val;
}
inline void pushdown(int &k, int &l, int &r, int &mid) {
if(tr[k].tag) {
tr[k<<1].val += tr[k].tag * (mid-l+1), tr[k<<1|1].val += tr[k].tag * (r-mid);
if(l < mid) tr[k<<1].tag += tr[k].tag, tr[k<<1|1].tag += tr[k].tag; // 叶子结点不需要标记
tr[k].tag = 0;
}
}
void build(int k, int l, int r) {
// k:结点编号 [l, r] 结点k的表示的区间。
if(l == r) {
tr[k].val = a[l];
return;
}
int mid = l + (r - l >>1);
build(k<<1, l, mid);
build(k<<1|1, mid+1, r);
pushup(k);
}
ll query(int k, int l, int r, int x, int y) {
// k, l, r 同上 [x, y] 查询区间
if(x <= l && r <= y) // 如果被包含
return tr[k].val;
int mid = l + (r - l >> 1); ll res = 0;
pushdown(k, l, r, mid);
if(x <= mid) res = query(k<<1, l, mid, x, y); // 左区间与查询区间有交集
if(y > mid) res += query(k<<1|1, mid+1, r, x, y); // 右区间与查询区间有交集
return res;
}
void update(int k, int l, int r, int x, int y, int t) {
// k, l, r, x, y 同上 t 表示增加值
if(x <= l && r <= y) {
tr[k].val += (r - l + 1) * t;
if(l != r) tr[k].tag += t;
return;
}
int mid = l + (r - l >> 1);
pushdown(k, l, r, mid);
if(x <= mid) update(k<<1, l, mid, x, y, t);
if(y > mid) update(k<<1|1, mid+1, r, x, y, t);
pushup(k);
}
};
标记永久化就是不上下传递标记,在不爆的前提下可以优化常数,也更加方便使用。
修改时,把所有不被修改区间完全包含和被修改区间一级包含(即第一个被枚举到的完全包含区间)结点累计上他总共能获得的贡献,然后在一级包含结点上打标记。
查询时,累计枚举到一级包含结点前的所有tag,返回 t a g s u m ∗ l e n tagsum*len tagsum∗len ,其中 t a g s u m tagsum tagsum 指标记和, l e n len len 指的是这一节点表示的区间的长度。
ll query(int k, int l, int r, int x, int y, ll sum) {
if(x <= l && r <= y)
return tr[k].val + sum * (r - l + 1);
sum += tr[k].tag;
int mid = l + (r - l >> 1); ll res = 0;
if(x <= mid) res = query(k<<1, l, mid, x, y, sum);
if(y > mid) res += query(k<<1|1, mid+1, r, x, y, sum);
return res;
}
void update(int k, int l, int r, const int& x, const int& y, const int& t) {
tr[k].val += (std::min(y, r) - std::max(x, l) + 1) * t; // 他应得的贡献
if(x <= l && r <= y) {
tr[k].tag += t;
return;
}
int mid = l + (r - l >> 1);
if(x <= mid) update(k<<1, l, mid, x, y, t);
if(y > mid) update(k<<1|1, mid+1, r, x, y, t);
}
动态开点就是不一下开满整一棵树,用到哪开到哪。可以在结构体内建立ls和rs表示左右儿子分别存在哪,这里就不贴代码了。