先看一个例子:给出一个数组a[ n ],提供两种操作:1、修改其中任意一个数字。2、给出一个下标区间[ i , j ],得出a[ ]中该下标区间数字之和。
按照最朴素的想法做,修改数字可以在O(1)的复杂度下实现,操作2用循环从i到j求和,若有m次操作2,则复杂度为O(mn),那么这样的一个算法最终复杂度实为O(n^2),这样的复杂度下能处理的数据规模就比较有限。而采用线段树这样一种数据结构就能将这类区间问题的复杂度降低到O(nlogn),实现一个极大的优化。
建树的方式不唯一,下面为根据上述问题建的一颗线段树(大小可变)。
这个问题的建树原则:根节点是要处理的数据区间段,左儿子是父亲节点的左半部分,右儿子则是右半部分,直至节点为”一个点”。
查询或修改某个节点的复杂度都为O(logn),题目中每个节点保存该节点所示区间之和,在更新了某个点的数据后向上回溯更新父亲节点的数据。
节点定义成这样:
struct node
{
int l,r; //该节点表示区间为[l,r]
int sum; //数据域
}t[MAXN*4];
其中数据域根据需求不同而不同
先构造并初始化一颗线段树
void construct(int l,int r,int p) //所需建树的区间[l,r]
{
t[p].l=l,t[p].r=r,t[p].sum=0;
if(l==r)
{
scanf("%d",&t[p].sum);
return ;
}//递归至“一个点”时输入该点的值
int m=(l+r)>>1;
construct(l,m,p*2); //建左子树
construct(m+1,r,p*2+1); //建右子树
t[p].sum=t[p*2].sum+t[p*2+1].sum;//向上回溯更新父节点
}
修改操作
void modify(int x,int p,int val)//将a[ ]中下标为x的数改为val
{
if(t[p].l==t[p].r)
{
t[p].sum=val;
return ;
}
int m=(t[p].l+t[p].r)>>1;
if(x<=m) modify(x,p*2,val); //小于中间值则在左子树找
else modify(x,p*2+1,val); //否则在右子树找
t[p].s=t[p*2].sum+t[p*2+1].sum; //向上回溯更新父节点
}
查询操作
int query(int l,int r,int p) //查询区间[l,r]之和
{
if(t[p].l==l&&t[p].r==r)
return t[p].sum;
int m=(t[p].l+t[p].r)>>1;
if(r<=m) return query(l,r,p*2);//[l,r]全在左子树
if(l>m) return query(l,r,p*2+1); //[l,r]全在右子树
else return query(l,m,p*2)+query(m+1,r,p*2+1); //左右子树各有一部分
}
至此,这道题便基本解决了,这种类型的题属于点更新段/点查询,属于最简单的线段树运用了,查询一个区间内的最大/最小值等也类似此做法,在回溯更新等处稍作变化就行。
那成段更新呢?这涉及到Lazy思想也就是延迟更新,我会在以后有空的时候再写写,还有区间合并、扫描线等。
线段树是种容易上手又非常优雅的数据结构,有很多优点,即便有时只是最简单的线段树操作,但是修改顺序查询方式不一样又会让人耳目一新!
本blog中有几篇线段树的结题报告,欢迎查看。