一、引例
有M个数排成一列,做N次操作,每次操作包括:
(1)询问指定区间的最大值、最小值
(2)将指定区间的每个数加上一个值
如果按照最朴素的做法,一个个的遍历,时间复杂度:O(MN)。
那么如何解决一个区间求和(最大值,最小值)的问题呢?那么就要用到线段树啦。
二、定义
线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。
主要用来解决区间查询、区间修改,使用线段树可以快速的查找某一个节点在若干条线段中出现的次数,基本保证每次操作的时间复杂度为O(logN)。
三、实际应用
a.单点更新
1.定义每个结点的信息
线段树是建立在线段的基础上,每个结点都代表了一条线段[a,b]。长度为1的线段称为元线段。非元线段都有两个子结点,左结点代表的线段为[a,(a + b) / 2],右结点代表的线段为[((a + b) / 2)+1,b]。
struct node
{
int left,right,sum;//左端点,右端点,和
} tree[maxn<<2];
2.更新
void maintain(int root)//更新根节点为左右子结点的和
{
int lnode=root<<1;
int rnode=root<<1+1;
tree[root].sum=tree[lnode].sum+tree[rnode].sum;
}
3.递归建树
遇到叶子节点直接赋值,否则递归遍历左右建树,最后回溯即可。
void build(int root,int begin,int end)//树的结点编号 左端点下标 右端点下标
{
tree[root].left=begin;
tree[root].right=end;
if(begin==end)//叶子节点
{
scanf("%d",&adj[begin]);
tree[root].sum=adj[begin];//单元素 直接赋值
return ;
}
int mid=(begin+end)>>1;
build(root<<1,begin,mid);//更新左子树
build(root<<1+1,mid+1,end);//更新右子树
maintain(root);//存储左右子树的和
}
4.单点更新
将一条线段[a,b] 插入到代表线段[l,r]的结点p中,如果p不是元线段,那么令mid=(l+r)/2。如果b<mid,那么将线段[a,b] 也插入到p的左儿子结点中,如果a>mid,那么将线段[a,b] 也插入到p的右儿子结点中。
void update(int root,int pos,int num)
//根结点编号 欲修改值的下标 期待的值
{
if(tree[root].left==tree[root].right&&tree[root].left==pos)
{//若修改的值在这个节点的左右区间之间那么就直接更改此区间的sum值就可
tree[root].sum+=num;
return ;
}
int mid=(tree[root].left+tree[root].right)>>1;
if(pos<=mid)
update(root<<1,pos,num);
else
update(root<<1+1,pos,num);
maintain(root);//每次都要更新根节点
}
5.求和操作
int query(int root,int begin,int end)//求和
{
int ans=0;
if(begin==tree[root].left&&end==tree[root].right)
{
return tree[root].sum;
}
int mid=(tree[root].left+tree[root].right)>>1;
if(end<=mid)
ans+=query(root<<1,begin,end);
else if(begin>=mid+1)
ans+=query(root<<1+1,begin,end);
else
{
ans+=query(root<<1,begin,mid);
ans+=query(root<<1+1,mid+1,end);
}
return ans;
}
b.区间更新(成段更新)
比如 从[1,10]每个结点的值都+1,普通单点更新就会超时。
*区间更新:
指更新某个区间内的叶子节点的值,因为涉及到的叶子节点不止一个,而叶子节点会影响其相应的非叶父节点,那么回溯需要更新的非叶子节点也会有很多,如果一次性更新完,操作的时间复杂度肯定不是O(lgn),例如当我们要更新区间[0,3]内的叶子节点时,需要更新出了叶子节点3,9外的所有其他节点。为此引入了线段树中的延迟标记概念,这也是线段树的精华所在。
*延迟标记:
因为更新的数很多,所以我每一步的更新不接着算出来,等到最后需要的时候再去取消标记算出来。
比如现在需要对[a,b]区间值进行加c操作,那么就从根节点[1,n]开始调用update函数进行操作,如果刚好执行到一个子节点,它的节点标记为rt,这时tree[rt].l == a && tree[rt].r == b 这时我们可以一步更新此时rt节点的sum[rt]的值,sum[rt] += c * (tree[rt].r - tree[rt].l + 1),注意关键的时刻来了,如果此时按照常规的线段树的update操作,这时候还应该更新rt子节点的sum[]值,而Lazy思想恰恰是暂时不更新rt子节点的sum[]值,到此就return,直到下次需要用到rt子节点的值的时候才去更新,这样避免许多可能无用的操作,从而节省时间 。
用lazy标记,等到当前区间比我需要的目标区间大的时候,我必须用到下面的值了,必须往下修改了,这时候,我们就把之前堆积起来的懒惰标记pushdown了,于是就有了一个神奇的pushdown操作。
其他的建树什么的和单点更新一样,只是多了lazy标记和pushdown。
void pushdown(LL root) //向下传递lazy标记
{
if (tree[root].lazy)
{
tree[root<<1].lazy+=tree[root].lazy;
tree[root<<1+1].lazy+=tree[root].lazy;
tree[root<<1].val+=tree[root<<1].len*tree[root].lazy;
tree[root<<1+1].val+=tree[root<<1+1].len*tree[root].lazy;
tree[root].lazy=0;
}
}
c.区间合并
不过还没做过这方面的题QAQ
ps:之前看过,今天再看像重新学了一遍(>_<)
今天距离省赛过去已经一个星期了,该调整回来了。
还是不够强,继续修炼吧QAQ。