浅谈如何优美地实现线段树?
前言
线段树作为一种基础的数据结构,在信息学竞赛中有着广泛的应用。
与之相伴随的是大量的基于线段树维护的题目。
在这其中,有不少题目所需要实现的线段树包含多种操作,维护多种信息,拥有较大的代码难度。
相信不少人也有过写线段树越写越乱,写大量本质相同代码的经历。
笔者在此结合平日里实现线段树的经验,斗胆浅谈一下如何准确,快速的实现线段树。
正文
首先,在笔者的平时习惯中。线段树的写法通常根据节点信息是否用结构体封装可以分为两类。
这里强烈建议读者通过练习做到熟悉两种不同的线段树写法。
从而在题目较为简单时,使用第一类线段树快速实现。
而在操作较为繁琐,询问多种信息时,则采取第二种线段树,提高代码准确度。
这里第一类线段树的实现较为简单,不在赘述,重点讲解第二类线段树的实现。
这里首先给出第二类线段树的框架
struct tag
{
//the tag of each node
void clear(){}
};
tag operator+=(tag &x,tag k)
{
tag nx=x;
/*
...
*/
return x=nx;
}
struct data
{
//the data of each node
};
data operator+(data a,data b)
{
data ans;
/*
...
*/
return ans;
}
data operator+=(data &x,tag k)
{
data nx=x;
/*
...
*/
return x=nx;
}
struct Segment_Tree
{
#define lson o<<1
#define rson o<<1|1
#define mid ((t[o].l+t[o].r)>>1)
tag f[N*4];
data t[N*4];
inline void pushup(int o){t[o]=t[lson]+t[rson];}
inline void update(int o,tag k){f[o]+=k,t[o]+=k;}
inline void pushdown(int o){update(lson,f[o]);update(rson,f[o]);f[o].clear();}
void build(int o,int l,int r)
{
f[o].clear();t[o].l=l;t[o].r=r;t[o].len=r-l+1;
if(l==r)return (void)(t[o]={l,r,r-l+1});
build(lson,l,mid);build(rson,mid+1,r);pushup(o);
}
void opt(int o,int ql,int qr,tag k)
{
if(ql<=t[o].l&&t[o].r<=qr)return update(o,k);
pushdown(o);
if(ql<=mid)opt(lson,ql,qr,k);
if(qr>mid)opt(rson,ql,qr,k);
pushup(o);
}
data query(int o,int ql,int qr)
{
if(ql<=t[o].l&&t[o].r<=qr)return t[o];
pushdown(o);
bool fg1=ql<=mid,fg2=qr>mid;
if(fg1&&fg2)return query(lson,ql,qr)+query(rson,ql,qr);
else
{
if(fg1)return query(lson,ql,qr);
if(fg2)return query(rson,ql,qr);
}
}
}T;
简单介绍一下实现的思想。
首先将每个节点所存储的信息分为\(tag\)和\(data\),分别对应"懒标记"和"维护信息"。
然后我们需要做的便是重载以下三种运算符
\(1.tag+=tag\)
\(2.data+data\)
\(3.data+=tag\)
这三种运算符分别对应了
1.父节点的标记作用于子节点的标记
2.左儿子信息与右儿子信息的合并
3.父节点的标记作用于子节点的存储信息
有了这三种运算符以后,我们便将几乎一切线段树的实现问题转化为了定义这三种运算+复制模板这一问题。