目录:
线段树我花了整整两天的时间去啃,进度很慢,但终究还是坚持下来了,在涉及到Lazy标记的部分卡了很久,刚开始看了一大堆理论,发现很晦涩,也看不懂,后来结合代码一点一点的慢慢就看懂了。
#define il inline
#define ull unsigned long long
#define ll long long
#define ls (u<<1)
//等价于u*2
#define rs (u<<1|1)
//等价于u*2+1
Lazy标记也称延迟标记,通常和pushdown()
操作配合使用,我喜欢讲这两者合称为Lazy操作。
Lazy操作一般用于线段树的区间更新。因为区间更新涉及到的叶子节点不止一个,而叶子节点会影响到其它的非叶父子节点,那么回溯需要更新的非叶子节点就会非常多,时间复杂度肯定远大于O(lgn)
,为此,线段树引入了延迟标记(Lazy)的概念,也是线段树之所以这么快的精华所在。
延迟标记:每有一个节点新增加一个标记,说明这个节点所对应的区间被更新过,但是这个节点的左右子节点却没有被更新过。如果我们在操作过程中要涉及到这个节点的子节点,而这个节点又存在Lazy标记,那么我们就必须调用pushdown()
函数来清除这个节点的Lazy标记,否则会导致结果出错。
通俗来讲,比如说操作:把区间[3,6]中的每一个数都加上3
,而在实现过程中我们发现某个节点u
它所覆盖的区间刚好就是[3,6]
,常规线段树的操作是:给这个节点添加一个标记Lazy标记(laz[u] += 3
),然后再更新这个节点的值(node[u] += 3 * (6 - 3 + 1)
),然后直接return。
如果没有Lazy标记会发生什么事呢,在更新完这个节点的值(node[u] += 3 * (6 - 3 + 1)
)之后,我们开始递归调用update()
函数分别进入它的左子树,右子树,左子树的左子树,右子树,右子树的左子树,右子树,如此往复循环,直到更新到叶子节点,然后再一层一层的回溯。
可想而知,在数据量很大的时候这将会是怎样的一种操作量,个人来说感觉效率还不如线性数组要高。
所以,Lazy标记被称为线段树的精髓所在。
在大部分的区间更新操作中,都会涉及到区间的长度,比如说把区间[2,9]中的每一个数都加上3
,结合Lazy标记,我们只需要把区间的权值权值加上3乘以这个区间的长度就可以,因为区间的长度代表着这个区间包含有几个元素。
单点更新其实就是区间更新的特殊情况,只要在调用区间更新函数的时候让左节点等于右节点就可以。
显然,单点查询和区间查询的关系也是如此。
const int maxn = 200000+7;
ll n,m,ans;
struct node {//线段树成员
ll l,r,sum,add;//左端点,右端点,权,lazy标记
}tree[maxn<<2];
il void pushup(ll u) {//向上回溯更新
tree[u].sum = tree[ls].sum + tree[rs].sum;
}
il void pushdown(ll u) {//去除lazy标记并向下更新
tree[ls].add += tree[u].add;
tree[rs].add += tree[u].add;
tree[ls].sum += tree[u].add*(tree[ls].r-tree[ls].l+1);
tree[rs].sum += tree[u].add*(tree[rs].r-tree[rs].l+1);
tree[u].add = 0;
}
il void build(ll u, ll l, ll r) {
tree[u].l = l;
tree[u].r = r;
tree[u].add = 0;
if(l==r) {
scanf("%lld",&tree[u].sum);
return ;
}
ll mid = (l+r) >> 1;
//递归建树
build(ls,l,mid);//左子树
build(rs,mid+1,r);//右子树
pushup(u);
//已经更新完这个节点的左右儿子了,最后再更新这个节点本身
}
il void update(ll u, ll l, ll r, ll num) {
if(rtree[u].r) return ;
//如果这个节点的区间里根本没有需要更新的区间就直接return
if(l <= tree[u].l && r >= tree[u].r) {
//如果当前区间完全包含在需要更新的区间里
tree[u].add += num;
//添加lazy标记
tree[u].sum += num * (tree[u].r-tree[u].l+1);
//更新当前节点的权值,因为是给区间里每一个数都增加num
//所以这个节点权值的总增量是num乘以区间长度
//也就是num*(r-l+1)
return ;
}
if(tree[u].add) pushdown(u);//如果当前区间含有lazy标记
update(ls,l,r,num);//更新左子树
update(rs,l,r,num);//更新右子树
pushup(u);
//更新完左右子树,向上回溯
}
il void oneupdate(ll u, ll x, ll num) {
update(u,x,x,num);
//让区间更新的左右端点相等即可,没有验证过这样写的效率
}
il void query(ll u, ll l, ll r) {
if(r < tree[u].l || l > tree[u].r) return ;
//如果这个节点的区间里根本没有需要更新的区间就直接return
if(l <= tree[u].l && r >= tree[u].r) {
//如果当前区间完全包含在需要更新的区间里
ans += tree[u].sum;
return ;
}
if(tree[u].add) pushdown(u);
//如果当前区间存在lazy标记,更新
ll mid = (tree[u].l+tree[u].r)>>1;
if(r<=mid) query(ls,l,r);
//如果查询区间完全在左子树
else if(l>mid) query(rs,l,r);
//如果查询区间完全在右子树
else {//如果查询区间跨越了mid
query(ls,l,mid);
query(rs,mid+1,r);
}
}
u
,左右节点l
,r
作为参数放入每一个线段树操作函数中。显然,可以通过和数组版本的线段树操作对比得到这个结论。il void pushup(ll u)
函数,直接将其写成了node[u] = node[ls] + node[rs];
const int maxn = 200000+7;
ll n,m,ans;
ll node[maxn<<2],laz[maxn<<2];//node是树节点,laz是lazy标记
il void pushdown(ll u, ll l, ll r) {
ll mid = (l+r)>>1;
laz[ls] += laz[u];
laz[rs] += laz[u];
node[ls] += laz[u]*(mid-l+1);
node[rs] += laz[u]*(r-mid);
laz[u] = 0;
}
il void build(ll u, ll l, ll r) {
if(l==r) {
scanf("%lld",node+u);
return ;
}
ll mid = (l+r)>>1;
build(ls,l,mid);
build(rs,mid+1,r);
node[u] = node[ls] + node[rs];
}
il void update(ll u, ll l, ll r, ll b, ll e, ll num) {
if(er) return ;
if(b<=l && r<=e) {
laz[u] += num;
node[u] += num * (r-l+1);
return ;
}
if(laz[u]) pushdown(u,l,r);
ll mid = (l+r)>>1;
update(ls,l,mid,b,e,num);
update(rs,mid+1,r,b,e,num);
node[u] = node[ls] + node[rs];
}
/*略,参考数组版本*/
il void query(ll u, ll l, ll r, ll b, ll e) {
if(er) return ;
if(b<=l && r<=e) {
ans += node[u];
return ;
}
if(laz[u]) pushdown(u,l,r);
ll mid = (l+r)>>1;
if(e<=mid) query(ls,l,mid,b,e);
else if(b>mid) query(rs,mid+1,r,b,e);
else {
query(ls,l,mid,b,e);
query(rs,mid+1,r,b,e);
}
}
一个裸线段树区间查询/更新的题,题意很简单,代码也比较简单。需要注意的点就是不用long long的话会爆。
传送门:A Simple Problem with Integers POJ - 3468
题解:A Simple Problem with Integers - POJ 3468 - 线段树 区间更新