线段树 学习笔记
简介
略。
-1 一个模板
改编自 tourist。其中只需要写 apply、pushdown、merge
class segtree {
public:
struct node {
// don't forget to set default value (used for leaves)
// not necessarily neutral element!
int sum=0,add=0;
void apply(int l,int r,... v) {
//sum+=(r-l+1)*v,add+=v;
}
};
node merge(const node &a,const node &b) const {
node res;
res.sum=a.sum+b.sum;
return res;
}
inline void pushdown(int x,int l,int r) {
int y=(l+r)>>1;
int z=x+((y-l+1)<<1);
// pushdown from x into (x + 1) and z
if(tree[x].add!=0) {
tree[x+1].apply(l,y,tree[x].add);
tree[z].apply(y+1,r,tree[x].add);
tree[x].add=0;
}
}
inline void pushup(int x,int z) {
tree[x]=merge(tree[x+1],tree[z]);
}
int n;
vector<node> tree;
void build(int x,int l,int r) {
if(l==r) {
return;
}
int y=(l+r)>>1,z=x+((y-l+1)<<1);
build(x+1,l,y),build(z,y+1,r);
pushup(x,z);
}
template <typename M>
void build(int x,int l,int r,const vector<M> &v) {
if(l==r) {
tree[x].apply(l,r,v[l]);
return;
}
int y=(l+r)>>1,z=x+((y-l+1)<<1);
build(x+1,l,y,v),build(z,y+1,r,v);
pushup(x,z);
}
node get(int x,int l,int r,int ll,int rr) {
if(ll<=l&&r<=rr) {
return tree[x];
}
int y=(l+r)>>1,z=x+((y-l+1)<<1);
pushdown(x,l,r);
node res{};
if(rr<=y) res=get(x+1,l,y,ll,rr);
else if(ll>y) res=get(z,y+1,r,ll,rr);
else res=merge(get(x+1,l,y,ll,rr),get(z,y+1,r,ll,rr));
pushup(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,z=x+((y-l+1)<<1);
pushdown(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...);
pushup(x,z);
}
int find_first_knowingly(int x,int l,int r,const function<bool(const node&)> &f) {
if(l==r) return l;
pushdown(x,l,r);
int y=(l+r)>>1,z=x+((y-l+1)<<1);
int res;
if(f(tree[x+1])) res=find_first_knowingly(x+1,l,y,f);
else res=find_first_knowingly(z,y+1,r,f);
pushup(x,z);
return res;
}
int find_first(int x,int l,int r,int ll,int rr,const function<bool(const node&)> &f) {
if(ll<=l&&r<=rr) {
if(!f(tree[x])) return -1;
return find_first_knowingly(x,l,r,f);
}
pushdown(x,l,r);
int y=(l+r)>>1;
int z=x+((y-l+1)<<1);
int res=-1;
if(ll<=y) res=find_first(x+1,l,y,ll,rr,f);
if(rr>y&&res==-1) res=find_first(z,y+1,r,ll,rr,f);
pushup(x,z);
return res;
}
int find_last_knowingly(int x,int l,int r,const function<bool(const node&)> &f) {
if(l==r) {
return l;
}
pushdown(x,l,r);
int y=(l+r)>>1,z=x+((y-l+1)<<1);
int res;
if(f(tree[z])) res=find_last_knowingly(z,y+1,r,f);
else res=find_last_knowingly(x+1,l,y,f);
pushup(x,z);
return res;
}
int find_last(int x,int l,int r,int ll,int rr,const function<bool(const node&)> &f) {
if(ll<=l&&r<=rr) {
if(!f(tree[x])) return -1;
return find_last_knowingly(x,l,r,f);
}
pushdown(x,l,r);
int y=(l+r)>>1,z=x+((y-l+1)<<1);
int res=-1;
if(rr>y) res=find_last(z,y+1,r,ll,rr,f);
if(ll<=y&&res==-1) res=find_last(x+1,l,y,ll,rr,f);
pushup(x,z);
return res;
}
segtree(int _n): n(_n) {
assert(n>0);
tree.resize(2*n-1);
build(0,0,n-1);
}
template <typename M>
segtree(const vector<M> &v) {
n=v.size();
assert(n>0);
tree.resize(2*n-1);
build(0,0,n-1,v);
}
node get(int ll,int rr) {
assert(0<=ll&&ll<=rr&&rr<=n-1);
return get(0,0,n-1,ll,rr);
}
node get(int p) {
assert(0<=p&&p<=n-1);
return get(0,0,n-1,p,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...);
}
// find_first and find_last call all FALSE elements
// to the left (right) of the sought position exactly once
int find_first(int ll,int rr,const function<bool(const node&)> &f) {
assert(0<=ll&&ll<=rr&&rr<=n-1);
return find_first(0,0,n-1,ll,rr,f);
}
int find_last(int ll,int rr,const function<bool(const node&)> &f) {
assert(0<=ll&&ll<=rr&&rr<=n-1);
return find_last(0,0,n-1,ll,rr,f);
}
};
0. 一些简单的约定
令 ls 表示左儿子,rs 表示右儿子,mid 表示区间中点(即 \((l+r)/2\))
1. 一般线段树的常见优化技巧
1.1 动态开点线段树
对于普通的线段树节点 \(x\),定义其左儿子的编号为 \(2x\),右儿子的编号为 \(2x+1\)。
但是如果在长度为 \(1e9\) 的序列中做区间加区间求和,普通线段树无法高效地解决问题,因此产生动态开点线段树。
对于每个节点,维护其 ls 和 rs 的编号,修改下传时若对应儿子为空就新建一个节点,可以通过传递引用值方便地实现。查询时如果走到空节点就直接返回。
1.2 两倍空间线段树
令区间 \([l,r]\) 的编号为 \((l+r)\operatorname{or}[l\ne r]\)。
一个节点 \([x,l,r]\) 的左儿子编号为 \(mid\times 2\),右儿子编号为 \(mid \times 2+1\)。
容易证明这是两倍空间。
1.3 标记永久化
考虑每次不递归,直接打标记,这样省去了 pushdown 的常数,但是需要在 upd 和 query 时计算当前区间和查询区间的贡献。
2. 权值线段树
类似线段树维护一个桶,可以支持单点插入,区间查询 x 的排名,第 k 小(线段树二分)。
3. 线段树合并
考虑如果能快速合并叶子节点的信息和 pushup,就能合并快速合并两棵线段树。
动态开点,对空区间返回,否则合并叶子节点之后 pushup。
int merge(int l,int r,int x,int y) {
if(!x||!y) return x|y;//or x^y.
int z=++cnt;
if(l==r) return /* 合并叶子 x 和 y */,z;
ls[z]=merge(l,mid,ls[x],ls[y]);
rs[z]=merge(mid+1,r,rs[x],rs[y]);
return /*pushup*/,z;
}
参考资料:
1