吉司机线段树
引入:
训练赛遇到一个题打铁的匠,需要做到询问区间大于x的和。
赛后学习到,需要用到吉司机树的技巧。
引入的原因十分显然:
- 经典问题:给定一个序列,支持区间赋值 (min/max(a[i],x)) 以及区间求和。
- 每次修改的时间复杂度为(log)或(log^2)。
算法构造:
想要实现这种操作必然离不开线段树。
以 (min) 为例,对于线段树的每个节点(p)我们需要维护四个值:
- (mx) :区间最大值
- (cnt) :最大值出现的次数
- (md) :次大值(严格小于mx且最大)
- (sum) :区间和
对于节点(p),进行如下操作
- 若(x geq mx) 直接return。
- 若(md leq x leq mx, sum-=cnt imes (mx-x))(也就是只需要对mx修改)。
- (x leq md),暴力递归。
与区间加结合:
- 只需多加一个加法标记。
- 复杂度多一个(log) 。
复杂度的分析听说需要用到势能分析,吉老师已经证明过了,具体还需要看集训队论文。
代码实现:
#define ls p<<1
#define rs p<<1|1
struct Seg
{
int p,l,r,mx,md,c,mi;
ll sum;
#define l(i) t[i].l
#define r(i) t[i].r
#define c(i) t[i].c
#define mx(i) t[i].mx
#define md(i) t[i].md
#define sum(i) t[i].sum
#define mi(i) t[i].mi
}t[N<<2];
void pushup(int p)
{
sum(p)=sum(ls)+sum(rs);
mx(p)=max(mx(ls),mx(rs));
md(p)=max(md(ls),md(rs));
mi(p)=min(mi(ls),mi(rs));
c(p)=0;
if(mx(ls)!=mx(rs)) md(p)=max(md(p),min(mx(ls),mx(rs)));
if(mx(p)==mx(ls)) c(p)+=c(ls);
if(mx(p)==mx(rs)) c(p)+=c(rs);
}
void build(int p,int l,int r)
{
l(p)=l;r(p)=r;
if(l==r)
{
mi(p)=mx(p)=sum(p)=a[l];
md(p)=-1;
c(p)=1;
return;
}
int mid=l+r>>1;
build(ls,l,mid);
build(rs,mid+1,r);
pushup(p);
}
void update(int p,int z)
{
if(z>=mx(p)) return;
sum(p)-=(mx(p)-z)*c(p);
mx(p)=z;
}
void pushdown(int p)
{
update(ls,mx(p));
update(rs,mx(p));
}
void change(int p,int l,int r,int z)
{
if(z>=mx(p)) return;
if(l<=l(p)&&r>=r(p)&&md(p)<z)
{
update(p,z);
return;
}
int mid=l(p)+r(p)>>1;
pushdown(p);
if(l<=mid) change(ls,l,r,z);
if(r>mid) change(rs,l,r,z);
pushup(p);
}
虽然看起来长得跟一般的线段树差不多,但是它对于(min)的处理还是需要借鉴和学习。