线段树
线段树
板子
线段树可以在优秀(一般是 \(O(m\log n)\) )的时间复杂度内处理区间问题
其思想就是分治,如果一个区间可以直接操作,那么直接操作就好了
否则就把一个区间 \([l,r]\) 均等地分为2个等大的子区间 \([l,mid],[mid+1,r]\) 递归处理
至多就有 \(O(\log n)\) 种大小的区间,容易证明每种区间数量不超过2个,于是复杂度是严格 \(O(\log n)\) 的
从上面的推导不难看出线段树使用的前提是操作的区间之间相互不影响(比如关于数对的询问一般就不行,因为两个区间数对相互产生贡献),对一个区间操作优于逐个点的操作,操作的区间是整数(浮点数的话可能精度会卡层数,不过并不是绝对不能用)
首先看一下线段树的板子
template<typename node,typename act,int N=10005>
class segment_tree
{
private:
void (*pushdown)(node&,node&,node&);//下传标记,记得清空父节点的信息
node (*pushup)(node,node);//合并子节点,记得判断有没有儿子
bool (*change)(node&,act&);//修改节点信息
//返回是否成功,不成功则递归到左右儿子,比如区间取max
class seg_node
{
public:
int lc,rc;
node d;
seg_node()=default;
};
seg_node s[N];
//如果没有不造成影响的节点,pushup时应判断是否为虚节点
int rt,cnt;//根节点,节点数
int lw,up;//区间的下界、上界
#undef assert
#define assert(_expr) for (; !(_expr); \
__builtin_exit(114514) )
#define mid ((l+r)>>1)
//区间中点
#define Lc s[id].lc
#define Rc s[id].rc
//左右儿子编号
#define lson Lc,l,mid
#define rson Rc,mid+1,r
//左右儿子及其区间
#define no_cross ( (!id) || (l>qr) || (r<ql) )
//是否和目标区间无交
#define in_range ( (l>=ql) && (r<=qr) )
//是否在目标区间中
#define data(x) s[x].d
#define now data(id)
//节点的信息
inline void update(int id)
{
if(Lc) now=pushup(data(Lc),data(Rc));
}
inline void pushDown(int id)
{
if(!id) return;
pushdown(now,data(Lc),data(Rc));//下传标记
}
template<typename from_type>
inline void build(from_type h,int &id,int l,int r)
{
if(!id) id=++cnt;//动态开点
if(l==r)
now=node(*(h+l),l,r);//存在某种转换关系,必须写好
//可能需要用到区间长度,比如区间和
else
build(h,lson),
build(h,rson),
update(id);//合并到父节点上
}
inline void modify(int id,int l,int r,int ql,int qr,act &a)//考虑到a可能叫为复杂,用引用减少空间
{
if(no_cross) return;
if(in_range)
if(change(now,a))
return;//操作成功
pushDown(id);//下传标记
if(l!=r)
{
if(!Lc) Lc=++cnt,data(Lc)=node(l,mid);
if(!Rc) Rc=++cnt,data(Rc)=node(mid+1,r);
}//动态开点
modify(lson,ql,qr,a),
modify(rson,ql,qr,a);//递归到左右子树
update(id);//更新父节点
}
inline node query(int id,int l,int r,int ql,int qr)
{
if(no_cross) return doomy;
if(in_range) return now;
pushDown(id);//下传标记
return pushup( query(lson,ql,qr) , query(rson,ql,qr) );//递归到左右子树
}
public:
node doomy;//作为虚节点
segment_tree(node (*my_pushup)(node,node),void (*my_pushdown)(node&,node&,node&),bool (*my_change)(node&,act&))
{
pushup=(my_pushup),
pushdown=(my_pushdown),
change=(my_change),
lw=1,up=0;
}
template<typename from_type>
inline void build(from_type h,int l,int r)
{
lw=l,up=r;
build(h,rt,l,r);
}//基于一个序列建树
inline void modify(int l,int r,act a)
{
assert(lw<=up){cerr<<"Not Build!\n";}//可以不用
modify(rt,lw,up,l,r,a);
} //修改区间
inline node query(int l,int r)
{
assert(lw<=up){cerr<<"Not Build!\n";}
return query(rt,lw,up,l,r);
} //区间询问
inline void vir_build(int l,int r)
{
lw=l,up=r;
rt=cnt=1;
}//只设定区间不建树,之后动态开点
#undef mid
#undef Lc
#undef Rc
#undef lson
#undef rson
#undef no_cross
#undef in_range
#undef data
#undef now
};//合并方式,下传方式,修改规则
虽然操作已经在注释中写得比较清楚了,但不结合例子依然比较难懂,接下来就是一些例子
运用
a simple example
区间加,区间求和
考虑维护一个加法标记,表示整个区间被加了多少,然后一个点的值就是所有在线段树的划分下包含它的区间1的加法标记的和
对于区间求和,只需要在维护加法标记的同时乘上区间长度,就可以维护区间和了
于是可以写出如下代码
inline bool add(node& a,act& x)//act是操作类,只有一个int,node是树的节点类,有pls和sum
{
a.sum+=x.k*a.len;//维护区间和
a.pls+=x.k;//维护加法标记
return true;
}
返回值在此处没有意义,但对于部分线段树有意义
接着考虑询问,一个小细节是如果大的区间包含询问区间,那么需要统计其标记,一种方法是记录沿途的标记的等效,也就是标记永久化的写法,这里我采用了另一种方法,就是直接下放标记,两种方式没有明显差异(当然如果你需要可持久化那就必须标记永久化了)
代码如下
inline void pd(node a,node &b)
{
b.sum+=a.pls*b.len;//维护区间和
b.pls+=a.pls;//显然加法标记直接叠加
}
inline void pdd(node& a,node& b,node& c)
{
pd(a,b),pd(a,c);//下传标记
a.pls=0;//记得要删除父节点的标记
}
线段树的划分方法提供了一种把任意区间划分为 \(O(\log n)\) 个不交的子区间的方法,显而易见,我们还需要把这些区间的信息合并起来
inline node merge(node a,node b)
{
node tmp;
tmp.sum=a.sum+b.sum;
tmp.len=a.len+b.len;
return tmp;
}
看看 \(node\) 类
class node
{
public:
ll sum;
ll pls;
int len;
node()=default;
node(int l,int r)
{len=r-l+1;}
node(ll x,int l,int r)
{sum=x,len=r-l+1;}
};
为什么要有3个构造函数有一点难以理解
实际上是因为动态开点是只知道所在区间,所以需要第二个构造函数,而静态建树即知道所在区间,又知道区间信息,就需要第三个构造函数
上文的是模板,就算维护的信息和所在区间无关,也必须有这三类构造函数
然后这样声明一颗线段树
segment_tree<node,act,maxn*2> t(merge,pdd,add);
模板参数的意义依次是:节点信息,操作信息,树的大小
构造函数的参数依次是:节点合并规则,标记下放规则,操作规则
\(modify\) 没有返回值,进行操作,如果有多种操作,就只能写在操作规则内了
\(query\) 得到一个区间的合并的节点信息,返回一个节点类
\(vir\_build\) 设定区间的大小并只建立根节点
\(build\) 需要一个随机访问容器的首指针和一个区间,建立一颗线段树
a harder example
区间加,区间与 \(v\) 取min,区间询问max,区间求和
上文说线段树是严格复杂度的做法,这样的说法是欠妥的,为了解决这一问题,我们的线段树做法将是均摊 \(O(\log n)\) 的(也就是说如果你愿意打splay也是可以的)
(区间和 \(v\) 取min,即 \(\forall i\in [l,r],a_i\leftarrow min(a_i,v)\) 这样的操作又叫区间checkmin)
不考虑区间checkmin的话容易解决,只需要维护区间和、加标记、max值即可,转移也很简单,在此略过
难点在于区间checkmin,一个naive的优化是,如果v大于区间最大值,那么不用操作
类比这个思路,再维护区间严格次小值 \(sec\),因为要维护区间和,不难想到还要维护最大值个数 \(maxt\)
分类讨论:
如果 \(v\ge max\),什么也不用做
如果 \(sec<v<max\),只有最大值会被修改
如果 \(v<sec\),那只有递归到子树了
这样做就是均摊 \(O(\log n)\) 了,考虑势能分析,只有一个区间存在严格次小值时,才可能需要递归,否则是 \(O(1)\)
如果只有一种值,显然不会有严格次小值,区间总长的和为 \(O(n\log n)\),一次加法操作至多增加 \(O(\log n)\) 种数(因为只会修改 \(O(\log n)\) 个节点),且不会超过刚刚的上界
一次暴力的递归将减少 \(1\) 种数,故暴力递归的次数不会超过 \(O(n\log n+m\log n)\)
给一下checkmin操作的代码
if(opt==CHECK_MIN)
{
if(a.mx<=x.v) return true;//什么也不用干
if(a.sec>=x.v) return false;//操作失败,继续递归到子树
a.sum-=a.cnt*(a.mx-v),
a.mx=v;
}