P6242 【模板】线段树 3
Utilokasteinn · · 题解
P6242 【模板】线段树 3
题目简介
给你一个长度为
现在进行
-
1 l r k:将a_l\sim a_r 加上k 。 -
2 l r v:将a_l\sim a_r 对v 取最小值。 -
3 l r:求a_l\sim a_r 的和。 -
4 l r:求a_l\sim a_r 的最大值。 -
5 l r:求b_l\sim b_r 的最大值。
每次操作后令
思路
前置知识:只要会写线段树
吉司机线段树模版题,考虑用线段树维护。(大雾)
先讲一些基础的东西,会的可以直接跳到后面。
我们从线段树
先考虑解决操作
每个节点维护一个
区间加时就直接把这个
现在我们解决了操作
接下来我们尝试解决操作
先引入
-
-
区间最(小
/ 大)值操作:将a_l\sim a_r 中的数a_{i} 变为\min(a_i,v) 或\max(a_i,v) 。
显然在这题中,
看完上面的定义后,再来尝试解决一下。
操作
因为一个区间总能被若干个点表示,我们先将一个区间先简化一个节点。
我们在区间加时,会对一个区间加上
若
在
若
想象一下我们正在对一个节点(不是任意区间)进行疯狂的区间加,此时也就是该节点的
经过一次加法,这个
但是我们注意到,那么在标记下传前,当前节点的儿子的值是不会变的。
那么显然,当
假设这个这个
那么,我们只需要再设一个
注意下传时的顺序,要先更新
可以过测试点
这里一定要理解透,不然后面肯定都看不懂。
接下来进入正题,如何解决操作
容易发现这道题的瓶颈在于进行操作
这样写单次修改的时间复杂度就会达到惊人的
那怎么办呢?
线段树显然不能直接区间取
我们可以换一个思路,可以把区间内大于
因为不同的数要减去不同的数才能等于
那退一步来讲,若区间内大于
只需要维护一个
那怎样才能使得区间内大于
只需要多递归几次就行了。
我们在线段树每个节点设以下
因为只有在区间只有一种数大于
综上所述,解决方法就是在线段树中递归,直到该区间大于
这样做总的时间复杂度为
因为有人说只需要设 吧。所以再来说明一下为什么需要设
在没有操作
现在加入了区间取
那么我们就需要把最大值和非最大值分开来维护,故要设
换句话说,如果不设
代码及其理解
首先说明,本题解主要以代码为主。个人认为代码写得很清晰,如果懂得上述思路就一定可以看懂。
先来看一下线段树每个节点需要存些什么。
以下是需要维护的
具体地,
这
其他就简单了,具体详见代码。
这里需要注意,一个区间可能只有一种数,这样就没有区间严格次大值。这时我们需要给
建树代码如下:
struct segment_tree{
ll sum;
int l,r,maxa,cnt,se,maxb;
int add1,add2,add3,add4;
}s[2000005];
inline void push_up(int p)
{
s[p].sum=s[p*2].sum+s[p*2+1].sum;
s[p].maxa=max(s[p*2].maxa,s[p*2+1].maxa);
s[p].maxb=max(s[p*2].maxb,s[p*2+1].maxb);
if(s[p*2].maxa==s[p*2+1].maxa)
{
s[p].se=max(s[p*2].se,s[p*2+1].se);
s[p].cnt=s[p*2].cnt+s[p*2+1].cnt;
}
else if(s[p*2].maxa>s[p*2+1].maxa)
{
s[p].se=max(s[p*2].se,s[p*2+1].maxa);
s[p].cnt=s[p*2].cnt;
}
else
{
s[p].se=max(s[p*2].maxa,s[p*2+1].se);
s[p].cnt=s[p*2+1].cnt;
}
}
void build(int l,int r,int p)
{
s[p].l=l,s[p].r=r;
if(l==r)
{
s[p].sum=s[p].maxa=s[p].maxb=read();
s[p].cnt=1,s[p].se=-2e9;
return;
}
int mid=(l+r)/2;
build(l,mid,p*2);
build(mid+1,r,p*2+1);
push_up(p);
}
查询与普通的线段树无异,注意求和要开 long long。
代码如下:
ll query_sum(int p)
{
if(l>s[p].r||r<s[p].l)return 0;
if(l<=s[p].l&&s[p].r<=r)return s[p].sum;
push_down(p);
return query_sum(p*2)+query_sum(p*2+1);
}
int query_maxa(int p)
{
if(l>s[p].r||r<s[p].l)return -2e9;
if(l<=s[p].l&&s[p].r<=r)return s[p].maxa;
push_down(p);
return max(query_maxa(p*2),query_maxa(p*2+1));
}
int query_maxb(int p)
{
if(l>s[p].r||r<s[p].l)return -2e9;
if(l<=s[p].l&&s[p].r<=r)return s[p].maxb;
push_down(p);
return max(query_maxb(p*2),query_maxb(p*2+1));
}
我们先来看一下修改时要如何维护
先是区间加修改的代码:
void update_add(int p)
{
if(l>s[p].r||r<s[p].l)return;
if(l<=s[p].l&&s[p].r<=r)
{
s[p].sum+=1ll*k*s[p].cnt+1ll*k*(s[p].r-s[p].l+1-s[p].cnt);
s[p].maxa+=k;
s[p].maxb=max(s[p].maxb,s[p].maxa);
if(s[p].se!=-2e9)s[p].se+=k;
s[p].add1+=k,s[p].add2+=k;
s[p].add3=max(s[p].add3,s[p].add1);
s[p].add4=max(s[p].add4,s[p].add2);
return;
}
push_down(p);
update_add(p*2),update_add(p*2+1);
push_up(p);
}
根据
就是不管最大值还是非最大值,都加上
然后
最后记录一下未下传前最大的
接着就是区间取
void update_min(int p)
{
if(l>s[p].r||r<s[p].l||v>=s[p].maxa)return;
if(l<=s[p].l&&s[p].r<=r&&s[p].se<v)
{
int k=s[p].maxa-v;
s[p].sum-=1ll*s[p].cnt*k;
s[p].maxa=v,s[p].add1-=k;
return;
}
push_down(p);
update_min(p*2),update_min(p*2+1);
push_up(p);
}
代码做的事大致就是,先递归到一个满足
因为只修改最大值,所以只用更新
最后来看一下本题的难点,push_down 是如何下传懒标记。
先放上代码:
inline void change(int k1,int k2,int k3,int k4,int p)
{
s[p].sum+=1ll*k1*s[p].cnt+1ll*k2*(s[p].r-s[p].l+1-s[p].cnt);
s[p].maxb=max(s[p].maxb,s[p].maxa+k3);
s[p].maxa+=k1;
if(s[p].se!=-2e9)s[p].se+=k2;
s[p].add3=max(s[p].add3,s[p].add1+k3);
s[p].add4=max(s[p].add4,s[p].add2+k4);
s[p].add1+=k1,s[p].add2+=k2;
}
inline void push_down(int p)
{
int maxn=max(s[p*2].maxa,s[p*2+1].maxa);
if(s[p*2].maxa==maxn)
change(s[p].add1,s[p].add2,s[p].add3,s[p].add4,p*2);
else change(s[p].add2,s[p].add2,s[p].add4,s[p].add4,p*2);
if(s[p*2+1].maxa==maxn)
change(s[p].add1,s[p].add2,s[p].add3,s[p].add4,p*2+1);
else change(s[p].add2,s[p].add2,s[p].add4,s[p].add4,p*2+1);
s[p].add1=s[p].add2=s[p].add3=s[p].add4=0;
}
看着就很恶心,但其实也不难。
push_down 中是判断该区间的最大值是否位于左区间或右区间,是就下传最大值的
change 就是下传
注意下传的顺序。
最后记得清空
如果看不懂,就把测试点
注意事项&常数优化
-
使用快读和快写。
-
求和开
long long。 -
只有求和才开
long long,其他用int。这样可以比全用long long快半秒左右。 -
递归的时候参数尽量少。
-
用一个结构体来储存树的各种信息,这样空间访问较连续,速度较快。
-
在建树的时候读入,可以一定的节省空间和时间。
-
加一些函数前加
inline。 -
修改时的更新可以用
change代替,可以减少大量码量。 -
注意更新顺序。
其他
最后附上 AC Code,在第一篇代码的下面。
参考资料:
-
灵梦の题解。
-
灵梦の洛谷日报。
-
OI-Wiki。