(Part 1:) 线段树简介
线段树这个东西,学之前感觉很难很冷门,学之后感觉很难很热门。
首先我们先理解一下“线段树”这个词语。顾名思义,根据我们语文老师提供的拆词法,拆成“线段”+“树”两个部分。易得,这是一种树形数据结构,其节点基于线段操作。
我们使用一个插图来了解一下具体的一颗线段树:
显然可以看出,线段树的每个节点是一个线段。而在这个线段上,我们通常维护一些跟线段有关的值比如区间和或者最大值什么的。
(1.Build Tree) 千里之行,始于建树
这可不是什么好笑的事情,每年有那么多的(OIer)因为忘记建树而.......(缅怀同胞)
如何建树呢?在建树的时候,我们通常把一些很基本的信息添加到树上去,一般是区间左右端点和区间和。
如果当前节点是叶子节点,即(t[u].l==t[u].r),那么我们就把属于这一个长度为(1)的区间的值给到(t[u].sum),一般来说这个值题目会给出。
如果不是叶子节点,那么我们先求出一个(mid),此处有二分的思想,向左右分别递归,形如:
BuildTree(u*2,l,mid);
BuildTree(u*2+1,mid+1,r);
在递归完之后别忘了把左右儿子的值加给父节点:
t[u].sum=t[u*2].sum+t[u*2+1].sum;
(2.) 普天之下,莫非王土;率土之滨,区间修改
区间修改是个重点。
我们想一下,如果把区间修改改成(r-l+1)次单点修改,这个时间复杂度是我们不能接受的。
区间修改的主要思想是#lazy-tag#,即,如果想要修改([1,6])这个区间如上图,那么我们并不是立即把修改操作向下传递到叶子节点,而是现在(1)号节点这里记录一个(tag)。具体的,如果我想([1,6]+=5),那么我先(t[1].tag+=5),其他地方先不改动。
在修改完(tag)之后,需要立刻下传递一次,注意,是一次,所以时间复杂度是(Theta (1))。具体的操作如下:
if(t[p].add)
{
t[p*2].add+=t[p].add;
t[p*2+1].add+=t[p].add;
t[p*2].sum+=(t[p*2].r-t[p*2].l+1)*t[p].add;
t[p*2+1].sum+=(t[p*2+1].r-t[p*2+1].l+1)*t[p].add;
t[p].add=0;
}
这样做是为了之后从儿子节点更新(t[u].sum),具体操作见“千里之行”章节。
之后也是一样,算出(mid)之后向左右递归即可。
void pd(int p)
{
if(t[p].add)
{
t[p*2].add+=t[p].add;
t[p*2+1].add+=t[p].add;
t[p*2].sum+=(t[p*2].r-t[p*2].l+1)*t[p].add;
t[p*2+1].sum+=(t[p*2+1].r-t[p*2+1].l+1)*t[p].add;
t[p].add=0;
}
}
void modify(int p,int l, int r,int d)
{
if(l<=t[p].l&&t[p].r<=r){t[p].add+=d;t[p].sum+=(t[p].r-t[p].l+1)*d;return;}
pd(p);
int mid=(t[p].l+t[p].r)/2;
if(l<=mid)modify(p*2,l,r,d);
if(r>mid)modify(p*2+1,l,r,d);
t[p].sum=t[p*2].sum+t[p*2+1].sum;
}
(3.) 明察秋毫,区间查询
区间查询其实跟区间修改特别像,详见“普天之下,莫非王土”章节。
直接查找,向两侧递归即可。
ll query(int p,int l,int r)
{
if(l<=t[p].l&&t[p].r<=r)return t[p].sum;
pd(p);
int mid=(t[p].l+t[p].r)/2;
ll ans=0;
if(l<=mid)ans+=query(p*2,l,r);
if(r>mid)ans+=query(p*2+1,l,r);
return ans;
}