// 此博文为迁移而来,写于2015年3月30日,不代表本人现在的观点与看法。原始地址:http://blog.sina.com.cn/s/blog_6022c4720102vw6j.html
1、前言
一道题目:给出一个一维数组共n个结点,每次对他进行一些操作:在[l,r]范围内增加x,或是询问第i个结点当前的值为多少。这不水题嘛!直接模拟就可以了。但是如果n<=100000呢?询问次数超过100000呢?O(n^2)并不能扛住。今天要引入的内容就是线段树。这东西很早之前有提到过,我也编过,但是当时没有很好的理解。最近无意间去看了一眼,发现一下就懂了。
2、概念
线段树也是一种树结构,不一样的是,它的每个节点保存的是一条条线段。先看看他的构造:
我们发现,线段树虽然以树结构展现出来,但是他每个节点索要维护的东西和一般的树结构问题差别还是比较大的。
2、建造线段树
线段树的根节点所表示的线段是[1,n],而后它的两个子节点left和right则是将其父节点的线段从中间劈开,如图所示,当n=8时,mid=(1+8)/2=4 (向下取整),所以子节点所代表的线段分别是[1,4]和[5,8]。从根节点一次往下建造,直到该节点的线段为一个点,即叶子节点。
-------------------------------------------------------------------------------------------------------
void buildTree(int now,int l,int r) // 建树
{
if (l==r)
{
scanf("%d",&tree[now].sum);
tree[now].max=tree[now].min=tree[now].sum;
return;
}
int mid=(l+r)/2;
buildTree(now*2,l,mid);
buildTree(now*2+1,mid+1,r);
tree[now].sum=tree[now*2].sum+tree[now*2+1].sum;
tree[now].max=max(tree[now*2].max,tree[now*2+1].max);
tree[now].min=min(tree[now*2].min,tree[now*2+1].min);
}
-------------------------------------------------------------------------------------------------------
代码中可以看出,每个节点需要包含sum,max,min。但是,不是任何时候都需要的,在此列出所有需要的,如何取舍后文将提到。建树很简单,从根到叶子递归就行了。
3、问题类型
线段树的功能很强大,所以有很多数组上的问题都可以很方便的求出来。这里可以作一个总结:
修改类型:1、单点加减(每次只在数组中的一位数进行加减);
2、单点修改(同理);
3、区间加减(每次对[l,r]内所有数进行加减);
4、区间修改(每次对[l,r]内所有数修改成相同的一个数)。
询问类型:1、单点查询(查询一个数的值);
2、查询区间和(查找[l,r]内所有数的和);
3、查询区间最大值(查找[l,r]内最大值);
4、查询区间最小值(同理)。
这些内容可以任意搭配,任意修改查询,但是一般只会同时出现2、3种。而线段树每个点需要维护的变量,就是根据它所要求的来加入。为什么有几项加了红色?因为这些类型中,这些最难,也最重要,需要重点讲。
<1>单点加减/单点修改
-------------------------------------------------------------------------------------------------------
void update1(int now,int l,int r,int loc,int val) // 单点修改,loc表示所要更改的点,val表示所增加的值
{
int mid=(l+r)/2;
tree[now].sum+=val;
if (l==r) return;
if (loc>=l && loc<=mid) update1(now*2,l,mid,loc,val);
else update1(now*2+1,mid+1,r,loc,val);
}
-------------------------------------------------------------------------------------------------------
很好理解,从根节点到叶子节点一直走下去,将所有包含了loc这个点的线段进行修改。单点修改其实可以一起讲,只是多一个步骤,在更新之前找到原来的值和要修改的值的差,在进行单点加减操作。
<2>区间加减
-------------------------------------------------------------------------------------------------------
void update2(int now,int l,int r,int ql,int qr,int val) // 区间修改,ql和qr为查询部分,val为增加的值
{
if (l==ql && r==qr) { tree[now].sum+=val*(r-l+1),tree[now].lazy+=val; return; } // 如果当前的线段就是所求
int mid=(l+r)/2;
if (tree[now].lazy!=0) pushDown(now,r-l+1); // 下放
if (l<=ql && mid>=qr) update2(now*2,l,mid,ql,qr,val); // 如果所求线段包含于当前线段的左儿子
else if (mid+1<=ql && r>=qr) update2(now*2+1,mid+1,r,ql,qr,val); // 右儿子
else // 所求线段在左儿子右儿子均有一部分,所以可以分开更新
{
update2(now*2,l,mid,ql,mid,val);
update2(now*2+1,mid+1,r,mid+1,qr,val);
}
tree[now].sum=tree[now*2].sum+tree[now*2+1].sum; // 上放
}
-------------------------------------------------------------------------------------------------------
区间加减相对而言就有点麻烦了。先不管pushDown函数。麻烦之处是在于线段树本身的一个优化,如果没有这个优化,线段树的效率就会变低。我们考虑到一个问题:假设一开始在[2,6]内增加2,又在[5,7]内增加5,那么在[5,6]内不要相当于增加7?在暂时没有询问操作的情况下,我们可以偷一点懒——对于每一条线段,加一个lazy标识,当没有询问的时候,可以不需要去对树进行更新。
在更新的时候,我们不进行线段树本身值的加减,只修改变量lazy。也许我的思路相对于复杂一些,但是一样可以正常理解啦。。
没看懂?其实可以说得很形象:秋天到了,你是庙里的一个和尚,方丈给你一个任务:扫院子里的落叶。众所周知,落叶在秋天是会不断落下的,所以扫了一遍之后,过不一阵子就又有叶子了。在方丈还没有来检查你的工作的情况下,你可以偷一点懒——让叶子堆积在那里!反正早扫晚扫都是扫,还不如晚扫呢是不?(还没懂你还是弃疗算了。。)
现在,pushDown函数的作用可否知道了?那就是将当前线段保存的lazy标识释放出来,对线段树进行真正值的更新。什么时候要用到?如果你明白了其原理应该已经知道了——两种情况,当你需要对标了lazy标识的线段树的子节点进行操作,你不得不进行更新了;当需要进行查询操作时(就是当方丈过来的时候= =),进行更新。
-----------------------------------------------------------------------------------------------------------------
void pushDown(long long now,long long m) // 区间修改——lazy标记下放
{
tree[now*2].lazy+=tree[now].lazy; tree[now*2+1].lazy+=tree[now].lazy;
tree[now*2].sum+=tree[now].lazy*(m-(m/2));
tree[now*2+1].sum+=tree[now].lazy*(m/2);
tree[now].lazy=0;
}
-----------------------------------------------------------------------------------------------------------------
<3>单点查询
直接上代码。
-----------------------------------------------------------------------------------------------------------------
int getVal(int now,int l,int r,int loc) // 单点查询
{
int mid=(l+r)/2;
if (l==r && r==loc) return tree[now].val;
else if (loc>=l && loc<=mid) return getVal(now*2,l,mid,loc);
else return getVal(now*2+1,mid+1,r,loc);
}
-----------------------------------------------------------------------------------------------------------------
<4>查询区间和/最大值/最小值
也没什么难的,就是需要注意到前面提到的pushDown函数需要在这里用到。
-----------------------------------------------------------------------------------------------------------------
int getSum(int now,int l,int r,int ql,int qr) // 区间和
{
if (l==ql && r==qr) return tree[now].sum;
int mid=(l+r)/2,ans;
if (tree[now].lazy!=0) pushDown(now,r-l+1);
if (ql>=l && qr<=mid) ans=getSum(now*2,l,mid,ql,qr);
else if (ql>=mid+1 && qr<=r) ans=getSum(now*2+1,mid+1,r,ql,qr);
else ans=getSum(now*2,l,mid,ql,mid)+getSum(now*2+1,mid+1,r,mid+1,qr);
return ans;
}
-----------------------------------------------------------------------------------------------------------------
这里只给出了区间和的代码,最大值最小值同理,大家自己去写了。
线段树就这么讲完了,完整的代码就是通过上述函数组合起来就可以了。