吉司机线段树

upd on 2023.3.25:终于补完了。马上省选了还不会这个。

看到今天他们A层邀请赛整了一个于是决定多少看看。

来源:吉如一老师2016年国家集训队论文《区间最值操作与历史最值问题》。oiwiki上的论文没有粘全,最后的四类讨论只有一类(而且没有精髓\(Segment\ Tree\ Beats!\))。所以在这里结合自己的正(xia)常(bian)理(luan)解(zao)来发挥一下,补充这个知识点。

首先线段树显然不用再说,直接进入正题。

区间最值问题

区间最值问题,也就是对序列\(a\),给出\(l,r,x\),使在区间\([l,r]\)内的所有\(a_i\)都变成\(\max(a_i,x)\)\(\min(a_i,x)\)。下面以区间取\(\min\),询问区间和为例。

我们对于每个节点维护区间和\(sum\),区间最大值\(max\),次大值\(se\)和最大值的个数\(cnt\)。然后我们扫描到对\([l,r]\)\(x\)去最小值时,开始大力分讨:

  1. 如果\(x\ge max\),说明\(x\)无法更新答案,直接退出。
  2. 如果\(max\ge x\ge se\),说明x可以更新所有最大值而不改变次大值,直接修改\(sum\),减去\(cnt \cdot (max-x)\),更新\(max\)\(x\),打上标记退出。
  3. 其他情况,递归向下。

代码很好写,大力分讨即可。复杂度玄学,\(O(\)能过\()\),但是不超过\(O(m\log^2n)\)

#define lson rt<<1
#define rson rt<<1|1
struct node{
    int l,r,max,se,lazy,sum,cnt;
}tree[800010];
int n,ans,mex[200010],a[200010],nxt[200010],pos[200010];
bool v[200010];
void pushup(int rt){
    tree[rt].sum=tree[lson].sum+tree[rson].sum;
    if(tree[lson].max==tree[rson].max){
        tree[rt].max=tree[lson].max;
        tree[rt].se=max(tree[lson].se,tree[rson].se);
        tree[rt].cnt=tree[lson].cnt+tree[rson].cnt;
    }
    else if(tree[lson].max>tree[rson].max){
        tree[rt].max=tree[lson].max;
        tree[rt].se=max(tree[lson].se,tree[rson].max);
        tree[rt].cnt=tree[lson].cnt;
    }
    else{
        tree[rt].max=tree[rson].max;
        tree[rt].se=max(tree[rson].se,tree[lson].max);
        tree[rt].cnt=tree[rson].cnt;
    }
}
void pushmax(int rt,int val){
    if(tree[rt].max<=val)return;
    tree[rt].sum-=(tree[rt].max-val)*tree[rt].cnt;
    tree[rt].max=tree[rt].lazy=val;
}
void pushdown(int rt){
    if(tree[rt].lazy==-1)return;
    pushmax(lson,tree[rt].lazy);pushmax(rson,tree[rt].lazy);
    tree[rt].lazy=-1;
}
void build(int rt,int l,int r){
    tree[rt].l=l;tree[rt].r=r;tree[rt].lazy=-1;
    if(l==r){
        tree[rt].sum=tree[rt].max=a[l];
        tree[rt].se=-1;tree[rt].cnt=1;
        return;
    }
    int mid=(l+r)>>1;
    build(lson,l,mid);build(rson,mid+1,r);
    pushup(rt);
}
void update(int rt,int l,int r,int val){
    if(tree[rt].max<=val)return;
    if(l<=tree[rt].l&&tree[rt].r<=r&&tree[rt].se<val){
        pushmax(rt,val);return;
    }
    int mid=(tree[rt].l+tree[rt].r)>>1;
    pushdown(rt);
    if(l<=mid)update(lson,l,r,val);
    if(mid<r)update(rson,l,r,val);
    pushup(rt);
}
int query(int rt,int l,int r){
    if(l<=tree[rt].l&&tree[rt].r<=r)return tree[rt].sum;
    int mid=(tree[rt].l+tree[rt].r)>>1,val=0;
    pushdown(rt);
    if(l<=mid)val+=query(lson,l,r);
    if(mid<r)val+=query(rson,l,r);
    return val;
}

如果我们再加一个操作:区间加减,会怎样呢?

其实可以像之前一样搞。具体地,我们在下传标记的时候,首先下传加减标记再下传大小标记。同时,加减的时候可以更新大小的标记。啥?你问我大小对加减的影响?

讨论操作顺序。

  1. 先加减后取最值,最值操作会把加减的影响覆盖掉。
  2. 先最值后加减,加减会改变最值操作的标记。

所以是正确的。代码在此不表。(其实是我发懒)

吉老师证明了这个的复杂度是\(O(m\log^2n)\)的(大概思路差不多是证明不同类的标记有\(m\log n\)个然后每个标记至少需要找到子树标记的lca,这个过程是\(\log n\)的,因为不只有一类标记)。

类似的,同时维护区间\(\min\),区间\(\max\)也是两个\(\log\)的复杂度。当然,注意特判一下区间只有一个或两个不同元素的情况(详见oi-wiki)。

下面是几个论文中的例题:

  1. 开头说的他们A层的考试题:就是这两个操作再多加一个询问某个元素修改了多少次。其实也好搞,另外建一棵线段树,每次区间加的时候同样的区间修改,每次下传区间最值标记的时候修改一次(不带标记),也就是什么时候下放什么时候改。查询的时候继续下传区间最值标记就行了。
  2. 给你两个数组\(A,B\),同时支持区间最小和区间加,询问区间\(A_i+B_i\)的最大值。将位置分四类,\(A.B\)中都是最大值,\(A\)中最大\(B\)中非最大,\(B\)中最大\(A\)中非最大,\(A,B\)中都非最大。四类大力分讨,代码又臭又长就免了。
  3. 区间最小,区间加,区间\(\gcd\)。首先我们回忆不带区间最值操作的\(\gcd\)怎么搞:整一个差分序列(由更相减损法可得,差分序列的\(\gcd\)不改变),于是区间操作就变成了单点操作,可以递归到最底层修改然后pushup,最后求出区间\(\gcd\)之后再与\([1,l]\)区间和(也就是原数)\(\gcd\)一下就行了。现在我们加上了区间取最小值。(有点长让我分一段)

论文中提到将最大值单独扔出来,非最大值差分维护。具体的,维护区间最大值\(max\),区间次大值\(se\),非最大值的开头和结尾元素\(s,t\)和区间\(\gcd num\)

区间下传标记和修改就按照上面提到的搞。考虑如何pushup。首先我们观察到差分数组的顺序是不必要的,所以我们不必关注\(s,t\)中间到底夹了什么东西(也就是说即使最大值在它们中间也是对的)。

限于篇幅,只讨论\(max_l>max_r\)的情况。显然,\(max=max_l,se=\max(se_l,max_r)\)。我们把差分序列按照左子树序列->右子树序列->右子树最大值进行合并,也就是\(s=s_l,t=max_r\),此时\(num=\gcd(num_l,num_r,t_l-s_r,t_r-max_r)\)。最终的时间复杂度算上\(\gcd\)\(O(m\log^3n)\)

终于写完第一类了(

历史最值问题

大概分三类:

  1. 历史最大值:定义辅助数组\(B\),初始与\(A\)相同。每次操作后,使所有\(B_i=\max(B_i,A_i)\),此时\(B\)称为\(A\)的历史最大值。
  2. 历史最小值:定义类似历史最大值。
  3. 历史版本和:\(B\)初始全为\(0\),每次修改后使所有\(B_i\)加上\(A_i\),此时\(B\)称为\(A\)的历史版本和。

可以用懒标记处理的问题

以区间最大值为例。例题:P4314 CPU 监控。

考虑一个简单的问题:区间加,区间最大值,区间历史最大值。

定两个标记 \(lz_1,lz_2\),分别表示当前区间加的标记和这个标记达到过的最大值。这两个标记分别更新最大值和历史最大值。标记下推的时候先推历史最大再推区间加标记。

如果再加个区间覆盖?存个当前覆盖标记和历史最大覆盖标记就行了。注意覆盖的时候再区间加可以直接加标记。

无法用懒标记处理的单点问题

例题:【模板】线段树 3。

题意:区间加、区间取 \(\min\)、查区间和、查区间最大值、查区间历史最大值。

回忆区间最值操作的做法,我们将区间最值操作转化为对最值的区间加减操作。这个题也可以分为最大值和非最大值分别操作,每个像上一题一样上两个标记。

代码就是把两个插一块。

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <vector>
#define lson rt<<1
#define rson rt<<1|1
using namespace std;
int read(){
    int x=0,f=1;char ch=getchar();
    while(!isdigit(ch)&&ch!='-')ch=getchar();
    if(ch=='-')f=-1,ch=getchar();
    while(isdigit(ch))x=10*x+ch-'0',ch=getchar();
    return x*f;
}
int n,m,a[500010];
struct node{
    long long sum;
    int l,r,mxa,mxb,se,cnt,lz1,lz2,lz3,lz4;
}tree[2000010];
void pushup(int rt){
    tree[rt].sum=tree[lson].sum+tree[rson].sum;
    tree[rt].mxa=max(tree[lson].mxa,tree[rson].mxa);
    tree[rt].mxb=max(tree[lson].mxb,tree[rson].mxb);
    if(tree[lson].mxa==tree[rson].mxa){
        tree[rt].se=max(tree[lson].se,tree[rson].se);
        tree[rt].cnt=tree[lson].cnt+tree[rson].cnt;
    }
    else if(tree[lson].mxa>tree[rson].mxa){
        tree[rt].se=max(tree[lson].se,tree[rson].mxa);
        tree[rt].cnt=tree[lson].cnt;
    }
    else{
        tree[rt].se=max(tree[lson].mxa,tree[rson].se);
        tree[rt].cnt=tree[rson].cnt;
    }
}
void pushtag(int rt,int val1,int val2,int val3,int val4){
    tree[rt].sum+=1ll*val1*tree[rt].cnt+1ll*val2*(tree[rt].r-tree[rt].l+1-tree[rt].cnt);
    tree[rt].mxb=max(tree[rt].mxb,tree[rt].mxa+val3);
    tree[rt].mxa+=val1;
    if(tree[rt].se!=-2147483647)tree[rt].se+=val2;
    tree[rt].lz3=max(tree[rt].lz3,tree[rt].lz1+val3);
    tree[rt].lz4=max(tree[rt].lz4,tree[rt].lz2+val4);
    tree[rt].lz1+=val1;tree[rt].lz2+=val2;
}
void pushdown(int rt){
    int mx=max(tree[lson].mxa,tree[rson].mxa);
    if(tree[lson].mxa==mx)pushtag(lson,tree[rt].lz1,tree[rt].lz2,tree[rt].lz3,tree[rt].lz4);
    else pushtag(lson,tree[rt].lz2,tree[rt].lz2,tree[rt].lz4,tree[rt].lz4);
    if(tree[rson].mxa==mx)pushtag(rson,tree[rt].lz1,tree[rt].lz2,tree[rt].lz3,tree[rt].lz4);
    else pushtag(rson,tree[rt].lz2,tree[rt].lz2,tree[rt].lz4,tree[rt].lz4);
    tree[rt].lz1=tree[rt].lz2=tree[rt].lz3=tree[rt].lz4=0;
}
void build(int rt,int l,int r){
    tree[rt].l=l;tree[rt].r=r;
    if(l==r){
        tree[rt].sum=tree[rt].mxa=tree[rt].mxb=a[l];
        tree[rt].cnt=1;tree[rt].se=-2147483647;
        return;
    }
    int mid=(l+r)>>1;
    build(lson,l,mid);build(rson,mid+1,r);
    pushup(rt);
}
void updatesum(int rt,int l,int r,int val){
    if(l<=tree[rt].l&&tree[rt].r<=r){
        tree[rt].sum+=1ll*val*(tree[rt].r-tree[rt].l+1);
        tree[rt].mxa+=val;tree[rt].mxb=max(tree[rt].mxb,tree[rt].mxa);
        if(tree[rt].se!=-2147483647)tree[rt].se+=val;
        tree[rt].lz1+=val;tree[rt].lz2+=val;
        tree[rt].lz3=max(tree[rt].lz3,tree[rt].lz1);
        tree[rt].lz4=max(tree[rt].lz4,tree[rt].lz2);
        return;
    }
    pushdown(rt);
    int mid=(tree[rt].l+tree[rt].r)>>1;
    if(l<=mid)updatesum(lson,l,r,val);
    if(mid<r)updatesum(rson,l,r,val);
    pushup(rt);
}
void updatemin(int rt,int l,int r,int val){
    if(val>=tree[rt].mxa)return;
    if(l<=tree[rt].l&&tree[rt].r<=r&&tree[rt].se<val){
        val=tree[rt].mxa-val;
        tree[rt].sum-=1ll*tree[rt].cnt*val;
        tree[rt].mxa-=val;tree[rt].lz1-=val;
        return;
    }
    pushdown(rt);
    int mid=(tree[rt].l+tree[rt].r)>>1;
    if(l<=mid)updatemin(lson,l,r,val);
    if(mid<r)updatemin(rson,l,r,val);
    pushup(rt);
}
long long querysum(int rt,int l,int r){
    if(l<=tree[rt].l&&tree[rt].r<=r)return tree[rt].sum;
    pushdown(rt);
    int mid=(tree[rt].l+tree[rt].r)>>1;
    long long val=0;
    if(l<=mid)val+=querysum(lson,l,r);
    if(mid<r)val+=querysum(rson,l,r);
    return val;
}
int querymxa(int rt,int l,int r){
    if(l<=tree[rt].l&&tree[rt].r<=r)return tree[rt].mxa;
    pushdown(rt);
    int mid=(tree[rt].l+tree[rt].r)>>1,val=-2147483647;
    if(l<=mid)val=max(val,querymxa(lson,l,r));
    if(mid<r)val=max(val,querymxa(rson,l,r));
    return val;
}
int querymxb(int rt,int l,int r){
    if(l<=tree[rt].l&&tree[rt].r<=r)return tree[rt].mxb;
    pushdown(rt);
    int mid=(tree[rt].l+tree[rt].r)>>1,val=-2147483647;
    if(l<=mid)val=max(val,querymxb(lson,l,r));
    if(mid<r)val=max(val,querymxb(rson,l,r));
    return val;
}
int main(){
    n=read(),m=read();
    for(int i=1;i<=n;i++)a[i]=read();
    build(1,1,n);
    while(m--){
        int od=read(),l=read(),r=read(),val;
        if(od==1){
            val=read();updatesum(1,l,r,val);
        }
        else if(od==2){
            val=read();updatemin(1,l,r,val);
        }
        else if(od==3)printf("%lld\n",querysum(1,l,r));
        else if(od==4)printf("%d\n",querymxa(1,l,r));
        else printf("%d\n",querymxb(1,l,r));
    }
    return 0;
}

无区间最值操作的区间问题

主要三个问题:区间加减,区间历史最大值/历史最小值/历史版本和的和。

事实上这三个问题都可以被转化成简单的区间加减问题。以下设 \(A_i\) 为原数组,\(B_i\) 为上边提到的三个。

  1. 区间最小值:维护一个数组 \(C_i=A_i-B_i\),那么我们只要查区间的 \(A_i-C_i\)\(A_i\) 好办,关于 \(C_i\),容易发现每次区间加 \(x\) 都是把 \(C_i\) 变成 \(\max(C_i+x,0)\)

  2. 区间最大值:同上,维护 \(C_i=B_i-A_i\),此时区间加 \(x\) 即为把 \(C_i\) 变为 \(\min(C_i+x,0)\)

  3. 历史版本和:设当前为第 \(m+1\) 次操作,维护 \(C_i=B_i-mA_i\)。对于区间加 \(x\),相当于给 \(C_i\) 减掉 \(mx\)。那么查询就相当于查 \(C_i+mA_i\)

有区间最值操作的区间问题

jiry_2 老师搞的 Segment Beats。

三个操作:区间取 \(\max\),区间加,区间历史最小值和。

依据上边,我们维护 \(A_i,C_i\) 两个数组。\(A_i\) 是容易搞的,考虑 \(C_i\) 怎么搞。

上边两个操作相当于:对区间 \([l,r]\) 中的 \(C_i\) 变成 \(\max(C_i+x,0)\),对区间 \([l,r]\) 中为区间 \(A_i\) 最小值位置的 \(C_i\) 变成 \(\max(C_i+x,0)\)

仍然像区间最值一样拆开,拆成 \(A\) 是最小值位置 \(C\) 的最小值、次小值、最小值个数,和 \(A\) 非最小值位置的这三个东西。修改仍然暴力递归。复杂度论文说是 \(O(m\log^2n)\)

posted @ 2022-09-03 19:33  gtm1514  阅读(344)  评论(0编辑  收藏  举报