算法学习——吉司机线段树
吉司机线段树
引入:
训练赛遇到一个题打铁的匠,需要做到询问区间大于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 \times (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\)的处理还是需要借鉴和学习。