算法学习——吉司机线段树

吉司机线段树

引入:

训练赛遇到一个题打铁的匠,需要做到询问区间大于x的和。

赛后学习到,需要用到吉司机树的技巧。
引入的原因十分显然:

  • 经典问题:给定一个序列,支持区间赋值 \(min/max(a[i],x)\) 以及区间求和。
  • 每次修改的时间复杂度为\(log\)\(log^2\)

算法构造:

想要实现这种操作必然离不开线段树。
\(min\) 为例,对于线段树的每个节点\(p\)我们需要维护四个值:

  • \(mx\) :区间最大值
  • \(cnt\) :最大值出现的次数
  • \(md\) :次大值(严格小于mx且最大)
  • \(sum\) :区间和

对于节点\(p\),进行如下操作

  1. \(x \geq mx\) 直接return。
  2. \(md \leq x \leq mx, sum-=cnt \times (mx-x)\)(也就是只需要对mx修改)。
  3. \(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\)的处理还是需要借鉴和学习。

PS:上面的那道题只需要用到询问操作只需要做一些相应的修改(可以参考修改操作)

代码链接

posted @ 2020-04-22 18:25  Suiyue_Li  阅读(535)  评论(0编辑  收藏  举报