吉司机线段树

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

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

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

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

区间最值问题

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

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

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

代码很好写,大力分讨即可。复杂度玄学,O(能过),但是不超过O(mlog2n)

#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(mlog2n)的(大概思路差不多是证明不同类的标记有mlogn个然后每个标记至少需要找到子树标记的lca,这个过程是logn的,因为不只有一类标记)。

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

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

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

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

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

限于篇幅,只讨论maxl>maxr的情况。显然,max=maxl,se=max(sel,maxr)。我们把差分序列按照左子树序列->右子树序列->右子树最大值进行合并,也就是s=sl,t=maxr,此时num=gcd(numl,numr,tlsr,trmaxr)。最终的时间复杂度算上gcdO(mlog3n)

终于写完第一类了(

历史最值问题

大概分三类:

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

可以用懒标记处理的问题

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

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

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

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

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

例题:【模板】线段树 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;
}

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

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

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

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

  2. 区间最大值:同上,维护 Ci=BiAi,此时区间加 x 即为把 Ci 变为 min(Ci+x,0)

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

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

jiry_2 老师搞的 Segment Beats。

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

依据上边,我们维护 Ai,Ci 两个数组。Ai 是容易搞的,考虑 Ci 怎么搞。

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

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

posted @   gtm1514  阅读(362)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(1)
点击右上角即可分享
微信分享提示