线段树用于处理什么问题?
答:基于二叉树数据结构基础,进行如下几种操作
单点修改/区间修改
单点询问/区间询问
能在nlogn的时间复杂度下解决问题
1. 建树与维护
首先就是线段树的结点怎么存,方法有很多,我喜欢结构体首先就是线段树的结点怎么存,方法有很多,我喜欢结构体
struct node
{
ll sum;
ll tag;
}t[maxn<<2];
别忘了空间开到4*n,tag是懒惰标记,特别有用,接下来会讲
接下来就是递归建树,build(k,l,r)表示当前要构建区间[l,r]的线段树,k表示区间[l,r]所对应的标号。建树过程就是不断二分,直到到达叶子结点,就输入当前结点的值。
void build(ll k,ll l,ll r)
{
if(l==r)
{
scanf("%lld",&t[k].sum);
return;
}
ll mid=(l+r)>>1;
build(k<<1,l,mid);
build(k<<1|1,mid + 1,r);
update(k);
}
维护
维护就是update函数,操作的目的是为了维护父子节点之间的逻辑关系。当我们递归建树时,对于每一个节点我们都需要遍历一遍,并且电脑中的递归实际意义是先向底层递归,然后从底层向上回溯,所以开始递归之后必然是先去整合子节点的信息,再向它们的祖先回溯整合之后的信息。
void update(ll k)
{
t[k].sum = t[k<<1].sum + t[k<<1|1].sum;
}
k<<1和k<<1|1 是位运算,当然不是用来装逼 ,肯定能提速
2. 区间修改
为什么不讨论单点修改?因为其实很显然,单点修改就是区间修改的一个子问题而已,即区间长度为1时进行的区间修改操作。
那么对于区间操作,我们考虑引入一个名叫“lazy tag”(懒惰标记)的东西——称其lazy的原因是因为原本区间修改需要通过先改变叶子节点的值,然后不断地向上递归修改祖先节点直至到达根节点,时间复杂度最高可以到达O(nlogn)的级别。但当我们引入了懒惰标记之后,区间更新的期望复杂度就降到了O(logn)的级别且甚至会更低。
比如上面的例子,你要对2-5号点加1,朴素做法时间复杂度O(n),而用线段树区间修改是怎样的呢?
从线段树的顶端开始,如果当前枚举到的区间被要加v的区间完全包含,就在这个区间进行加法操作,把这个区间加上要加的数v乘上这段区间的元素(点)个数,再记录一下这段区间被加过v,就不再往下枚举了。
如果不被完全包含,就接着二分,枚举当前这一段的前半段和后半段
还是拿1~8那个图举栗子。
我们从最上面开始,发现当前枚举到的区间是1-8,而要修改的区间是2-5,并没有完全包含,于是我们开始枚举它的前半段和后半段(1-4和5-8)。
再枚举1-4和5-8,发现仍没有被2-5完全包含,所以继续二分,枚举1-2,3-4,5-6,7-8.
注意!这时我们发现3-4被2-5完全包含了!!
将3~4这段区间加上元素个数(右端点-左端点+1)× 要加的数v,不再二分它。
注意!我们发现7-8已经完全不被2-5包含了!
对于完全离经叛道的区间,我们要及时return,不再二分它,不然整个算法的时间复杂度将退化成nlogn。
然后发现其它区间仍然不满足,接着二分其它的区间(1-2,5-6)
现在我们的区间经过层层二分已经变成单点了,我们把目前被包含的单点加上v;
void modify(ll k,ll l,ll r,ll x,ll y,ll v)
{
if(x<=l&&r<=y)
{
t[k].sum+=(r-l+1)*v;
t[k].tag+=v;
return;
}
if(r<x||y<l) return;
pushdown(k,l,r);
ll mid=(l+r)>>1;
modify(k<<1,l,mid,x,y,v);
modify(k<<1|1,mid+1,r,x,y,v);
update(k);
}
懒惰标记的作用是记录每次、每个节点要更新的值,而线段树的优点在于传递式记录:
如果整个区间都被操作,记录在公共祖先节点上;只修改了一部分,那么就记录在这部分的公共祖先上;如果只修改了自己的话,那就只改变自己。
如果我们采用上述的优化方式的话,我们就需要在每次区间的查询修改时pushdown一次,以免重复或者冲突或者爆炸
那么对于pushdown而言,其实就是纯粹的update的逆向思维(但不是逆向操作): 因为修改信息存在父节点上,所以要由父节点向下传导lazy tag
怎么传导pushdown呢?这里很有意思,开始回溯时执行update,因为是向上传导信息;那我们如果要让它向下更新,调整顺序,即在向下递归的时候pushdown就可以了
void pushdown(ll k,ll l,ll r)
{
if(t[k].tag==0) return;
ll mid=(l+r)>>1;
t[k<<1].sum += t[k].tag * (mid - l + 1);
t[k<<1|1].sum += t[k].tag * (r - (mid + 1) + 1);
t[k<<1].tag += t[k].tag;
t[k<<1|1].tag += t[k].tag;
t[k].tag = 0;
}
3. 区间查询
区间查询的思路跟区间修改大致差不多
ll query(ll k,ll l,ll r,ll x,ll y)
{
ll ans=0;
if(x<=l && r<=y) return t[k].sum;
pushdown(k,l,r);
ll mid = (l + r) >> 1;
if(x<=mid) ans+=query(k<<1,l,mid,x,y);
if(mid<y) ans+=query(k<<1|1,mid + 1,r,x,y);
return ans;
}
蒟蒻还学的云里雾里,哈哈哈,继续努力!