线段树
线段树
定义
-
用 Node[l,r] 表示线段树表示区间[l,r] 的节点
-
其儿子就是
-
Node[l, l + r >> 1]
-
Node[(l + r >> 1) + 1, r]
-
当 l == r 时为叶子,停止
-
这样尽量等分下去的树形结构
功能
-
可以简单地抽象为一个数据结构功能
-
对一个范围进行某种可以快速维护的操作
-
查询一个范围内的某个可以合并的信息
-
通俗来说就是:
-
单点修改, 查询区间的某个信息
-
区间修改, 查询单点的某个信息
-
区间修改, 查询区间的某个信息
-
-
反正这三个都是线段树的功能
建树
-
建树直接按照上述规则递归即可
-
相当于是在 DFS 线段树
以下是 lxl 版
int a[N]; //线段树维护的原数组 struct Segment_Tree{ int sumv[N * 4]; // 存储线段树的区间和 // 一般线段树空间开到二倍即可,但是有些情况会超过,因此开到 4倍比较保险…… inline void pushup(int o) { sumv[o] = sumv[o * 2] + sumv[o * 2 + 1]; // pushup:节点信息的向上合并 } inline void build(int o, int l, int r) { if(l == r) { sumv[o] = a[l]; return; }// 叶子结点,也就是对应原数列的初始区间 int mid = (l + r) / 2; build(o * 2, l, mid); // 建左子树 build(o * 2 + 1, mid + 1, r) // 建右子树 pushup(o); // 把子树信息合并到当前点 } };
线段树区间查询
-
怎么查询信息呢?
-
既然这个要查询的区间被分为了 O(logn) 个节点
-
那么找到这 O(logn) 个节点即可
-
递归进行查找
lxl版代码(区间查询)
int a[N];
const int inf = 1e9 + 7;
struct Segment_Tree{
#define lson (o << 1)
#defint rson (o << 1 | 1)
int minv[N << 2];
inline int querymin(int o, int l, int ql, int qr)
{
// querysum 询问区间和,o, l, r 表示当前节点编号,以及该节点维护的[l,r]
// ql, qr 表示要查询的区间
if(ql <= l && r <= qr) return sumv[o]; // 如果之前区间已经被答案包含,直接计入答案
int mid = (l + r) >> 1, ans = inf;
if(ql <= mid) ans = min(ans, querysum(lson, l, mid, ql, qr));
if(qr > mid) ans = min(ans, querysum(rson, mid + 1, r, ql, qr));
// 在左右子树分开找
return ans;
}
}
线段树单点修改
-
单点修改就是指修改线段树的某个叶子节点的值
-
修改叶子节点会对其父节点的值产生影响
-
因此修改子节点后,要回溯更新其父节点的值
lxl版的单点修改
/* 在某一个位置加上一个值, */
const int N = 1000;
int a[N];
const int inf = 1e9 + 7;
struct Segment_Tree{
#define lson (o << 1)
#define rson (o << 1 | 1)
int minv[N << 2];
inline void pushup(int o)
{
minv[o] = min(minv[lson], minv[rson]);
}
inline void change(int o, int l, int r, int q, int v)
{
//要修改的位置为 q, 要加 v
if(l == r)
{
minv[o] += v;
return;
}// 找到了修改的单点,直接修改。
int mid = (l + r) >> 1;
if(q <= mid) change(lson, l, mid, q, v);
else change(rson, mid + 1, r, q, v);
// 根据要修改的位置在 左右子树找
pushup(o);
// 由于信息被修改了,所以最后要沿途更新线段树节点的信息
}
}
线段树区间修改
-
涉及到区间修改的问题没法快速的直接进行修改
-
有两种办法实现:
-
下放标记
-
标记永久
-
这里只讲下放标记
下放标记
-
每次线段树操作会涉及到 O(log n) 个线段树节点
-
而且一个点被访问当且仅当其父亲节点被访问过
-
所以我们可以用“懒惰”的思想来维护
-
这个最重要的思想就是,我们每次操作只会访问到少数线段树上的节点,那对于没有被访问的节点,比如这些节点子树内部的节点,我们不需要维护其经过区间修改后的值,只需要保证这些节点被访问到时候是正确的就行
-
以区间加区间 min 举例子:
-
区间 + x
-
区间最小值
-
我们在每个节点上维护一个标记,代表这个节点被加了多少值
-
查询的时候对于每个经过的节点,下放标记到其儿子
-
这样我们查询的是后还是查询log个节点,与之前的不同的是,每个节点有可能被整体加了个值
-
-
lxl版下放标记
inline void pushdown(int o)
{
// 把标记的信息下放给左右子树
if(!addv[o]) return;
addv[lson] += addv[o];
addv[rson] += addv[o];
minv[lson] += addv[o];
minv[rson] += addv[o];
addv[o] = 0;
}
更新区间
-
我们在 pushdown 的时候顺便更新儿子的最小值
-
即把两个儿子的最小值都加上 tag[x]
-
即可
线段树的区间修改区间查询
-
我们考虑一下这种情况下需要支持什么
-
两个区间维护的信息的合并:这个是最基本的,我们维护出了两个区间 [l,mid],[mid + 1, r] 的信息要能够快速合并出 [l,r] 的信息,否则这个问题线段树干不了……
-
一个线段树节点上多个标记的合并:标记之间要可以快速合并比如区间加标记,区间 + a 和 + b 合并后变成区间 +(a + b) ,区间修改标记:区间先修改为 a, 后修改为 b, 合并后变成区间修改为 b
-
标记对区间信息的影响: 这个一般是最麻烦的一步。我们维护出了线段树上一个的信息,要支持这个节点被修改后,这个信息能快速计算出来
-
例子:
-
区间和加区间和,当前线段树节点区间和为 a, 区间长度 len,我们加上 b, 则区间和变为 (a + len * b)
-
区间染色区间和, 当前线段树节点区间和为 a,区间长度是 len,我们加上 b, 区间和变为 (len * b)
-
-
-
吴凯路版线段树模板
#include<bits/stdc++.h>
#define LL long long
const int mod=1e9+7;
using namespace std;
//----------线段树-----------
namespace segtree{
struct msg{
int sum; // 工资和
int jia; // 每个人还没下传的加薪消息
int ren; // 人数
msg(){
}
msg operator +(const msg &b){ // 合并左右儿子的信息
msg ret;
ret.clear_tag();
ret.sum = sum + b.sum;
ret.ren = ren + b.ren;
return ret;
}
void pushdown(msg &son){//不修改父亲
// 消息下传
son.sum += son.ren * jia;
son.jia += jia;
}
void clear_tag(){
jia=0;
}
};
#define N 100005
const int L=0,R=1;
msg ini[N];
struct xds{
int son[2];
msg x;
}a[N*2];int cnt;
// int root;
// build(root, 1, 100000)
void build(int &k,int l,int r){
k=++cnt; //建一个新点,当前编号是k
if(l==r){ // 如果到最底层叶子了,那就很简单了
a[k].x=ini[l]; // 初始化
}else{
int mid=(l+r)>>1; // 没到最底层,就把区间劈一半
build(a[k].son[L],l,mid); // 分别建立左儿子
build(a[k].son[R],mid+1,r); // 和右儿子
a[k].x=a[a[k].son[L]].x+a[a[k].son[R]].x; // 合并来自左儿子右儿子的信息
}
}
void pushdown(int k){
a[k].x.pushdown(a[a[k].son[L]].x);
a[k].x.pushdown(a[a[k].son[R]].x);
a[k].x.clear_tag();
}
// 单点修改
void modify_point(int k, int l, int r, int q, msg val){
if(q==l && q==r){ // 到叶子了
a[k].x = val; // a[k].x = val + a[k].x;
}else{
pushdown(k);
int mid=(l+r)>>1; // >>1 是除以2的意思
if(q<=mid) modify_point(a[k].son[L], l, mid, q, val); //在左儿子里
else modify_point(a[k].son[R], mid+1, r, q, val); // 在右儿子
a[k].x=a[a[k].son[L]].x+a[a[k].son[R]].x; // 合并来自左儿子右儿子的信息
}
}
// 区间修改
void modify_qj(int k,int l,int r,int ql,int qr,msg val){
if(ql==l&&r==qr)
// a[k].x.jia += val.jia;
// a[k].x.sum += a[k].x.ren * val.jia;
val.pushdown(a[k].x); // 起到同样的作用
else{
pushdown(k);
int mid=(l+r)>>1;
msg ret;
if(qr<=mid) modify_qj(a[k].son[L],l,mid,ql,qr,val); // 修改全在左儿子
else if(ql>mid) modify_qj(a[k].son[R],mid+1,r,ql,qr,val); // 修改全在右儿子
// 修改区间劈开,左儿子右儿子全都改
else modify_qj(a[k].son[L],l,mid,ql,mid,val), modify_qj(a[k].son[R],mid+1,r,mid+1,qr,val);
// 改完记得更新信息
a[k].x=a[a[k].son[L]].x+a[a[k].son[R]].x;
}
}
// 区间查询
msg query(int k,int l,int r,int ql,int qr,msg val){
if(ql==l&&r==qr) return a[k].x; // 当前管理的[l,r]和查询的[ql,qr]完全一致
else{
pushdown(k);
int mid=(l+r)>>1; // 管理的区间不一致
msg ret;
if(qr<=mid) ret=query(a[k].son[L],l,mid,ql,qr,val); // 查询区间全在左儿子内
else if(ql>mid) ret=query(a[k].son[R],mid+1,r,ql,qr,val); // 查询区间全在右儿子内
// 跨过了中线,那就问问左右儿子,再把问到的结果合并起来
else ret=query(a[k].son[L],l,mid,ql,mid,val)+query(a[k].son[R],mid+1,r,mid+1,qr,val);
return ret;
}
}
////////////////////查询修改二合一版本//////////////////
msg modify(int k,int l,int r,int ql,int qr,msg val){
if(ql==l&&r==qr) return val.pushdown(a[k].x),a[k].x;
else{
pushdown(k);
int mid=(l+r)>>1;
msg ret;
if(qr<=mid) ret=modify(a[k].son[L],l,mid,ql,qr,val);
else if(ql>mid) ret=modify(a[k].son[R],mid+1,r,ql,qr,val);
else ret=modify(a[k].son[L],l,mid,ql,mid,val)+modify(a[k].son[R],mid+1,r,mid+1,qr,val);
a[k].x=a[a[k].son[L]].x+a[a[k].son[R]].x;
return ret;
}
}
#undef N
}
例题
维护序列
题目大意:
-
区间 + x
-
区间 * x
-
区间和
-
输出答案对 10 ^ 9 + 7 取模的值
Problem
-
如果只是区间加或区间乘,直接打个标记就可以了
-
但是同时有两个操作怎么办
Solution
-
维护两个标记,分别是加标记和乘标记
-
分别设为 add 和 mul
-
如果一个节点被加上了 x
-
则 add += x
-
如果一个节点被乘上了 x
-
则 add *= x, mul *= x
-
注意取模
-
即对于标记按顺序维护
-
假设区间原来值是 x ,则先乘 mul 后加 add 变成 mul * x + add
方差
Solution
-
可以通过维护区间和来维护区间平均数
-
其实就是区间和 / 区间长度
-
但是方差呢?
-
这就需要推推式子
-
(a _ 1 - \overline a) + (a_2 - \overline a) + (a_3 - \overline a) + ... + (a_n - \overline a)
= \dfrac{a_1 ^ 2 - 2a_1\overline a ^ 2 + \overline a^2 + a_2 ^ 2 - 2a_2\overline a + \overline a ^ 2 + a_3 ^ 2 - 2a_3\overline a + \overline a ^ 2 + ... + a_n^2 - 2a_n\overline a ^ 2}{n}
-
= \dfrac{n\overline a ^ 2 - 2\overline a(a_1 + a_2 + a_3 +...+a_n) + a_1 ^ 2 + a_2 ^2 + a_3^2 + ...+a_n ^ 2}{n}
\because \dfrac{a_1 + a_2 + a_3 + ... + a_n}{n} = \overline a
\therefore -\overline a^2 + \dfrac{a _ 1 ^ 2 + a _ 2 ^ 2 + a_3 ^2 + ... + a_n^2}{n}
-
于是我们要维护一个序列,支持区间加一个数 ,查询区间和,以及区间每个位置平方的和
-
区间修改,使用打标记的方法实现
-
区间和比较简单,平方和怎么办?
-
我们维护了区间 a[i] ^2 的和,则 经过区间 + x 后,我们需要维护 (a[i] + x) ^ 2 的和,展开式子就是 a[i]^2 + 2x * a[i] + x ^ 2 的和
-
于是我们维护区间 a[i] 的和,区间长度,就可以计算出式子的值
脑洞治疗仪
悄咪咪:SDOI 让山东大山里的孩子走进深山(来自赵小姐话的改编)(被敲打)
题目大意
-
区间赋0
-
区间最长连续 0
-
把一个区间的 1依次填到另一个区间
Solution
-
使用线段树维护序列,对每个节点维护其是否为全1,以及内部最长 0 段,以及左右最长0段
-
对于 3 操作,我们查区间和,这样就可以知道区间有多少1
-
然后我们在填到的区间中二分一下填到哪个位置用完了我们挖出来的1
-
为了实现这两步需要维护每个节点代表区间和
-
然后就把 3 操作转换为了:
- 区间修改为 0 或 1
-
这样的操作
-
每次直接二分位置然后线段树的话时间复杂度是 O(n + mlog ^2 n)
-
可以做到 O(n + mlogn),在线段树上二分即可

浙公网安备 33010602011771号