浅谈区间最值操作与历史最值问题

浅谈树状数组与线段树:https://www.cnblogs.com/AKMer/p/9946944.html

区间最值问题

Gorgeous Sequence为例:

对于线段树上每个结点,我们维护最大值,严格次大值,区间和,最大值个数即可。对于修改操作,分为三种情况讨论:

1、如果当前结点的最大值小于等于\(a\)的话,直接退出,因为取\(max\)操作不会对当前区间造成任何影响。

2、如果当前结点的最大值大于\(a\)但是次大值小于等于\(a\),所以最大值都会被改成\(a\),其它值不受影响。给当前点打上一个区间与\(a\)\(min\)的标记和区间然后减去最大值个数乘以最大值与\(a\)的差值。

3、如果当前结点的次大值大于\(a\),那么就暴力递归它的两个子树继续做。

时间复杂度证明:

首先第一种和第二种情况都是\(O(1)\)的,我们不用考虑。那么对于第三种情况,怎么证明时间复杂度呢?

这个时候,吉如一线段树最重要的思想来了。

不要试着证明每次的复杂度多么多么优秀,总复杂度总会给你一个满意的答案。

我们记录\(fake\)为当前线段树的权值,\(fake\)记录的是所有结点的权值种数和。一开始最多是\(nlogn\)的。

对于第三种情况,意味着当前结点的最大值和次大值都将消失不见,被\(a\)取而代之。也就是说,你递归了多少个结点,\(fake\)的权值就至少会降低多少。\(fake\)最少可以降到\(2n\)(每个结点的权值都只有一种),所以总复杂度是\(O(nlogn)\)的。

于是乎,吉如一线段树处理这种问题的复杂度就是\(O(nlogn)\)的。

那么假如加上加标记呢?我们考虑一下,每次区间加最后终止的结点一共会有\(logn\)个,所以这会使\(fake\)的权值增加\(mlogn\),最终复杂度还是可以接受的。

时间复杂度:\(O((n+m)logn)\)

空间复杂度:\(O(n)\)

代码如下:

#include <cstdio>
#include <algorithm>
using namespace std;
typedef long long ll;

const int maxn=1e6+5;

int n,m;
int a[maxn];

int read() {
    int x=0,f=1;char ch=getchar();
    for(;ch<'0'||ch>'9';ch=getchar())if(ch=='-')f=-1;
    for(;ch>='0'&&ch<='9';ch=getchar())x=x*10+ch-'0';
    return x*f;
}

struct segment_tree {
    ll sum[maxn<<2];
    int mx[maxn<<2],se[maxn<<2];
    int tag[maxn<<2],cnt[maxn<<2];

    void update(int p) {
        sum[p]=sum[p<<1]+sum[p<<1|1];
        mx[p]=max(mx[p<<1],mx[p<<1|1]);
        cnt[p]=(mx[p]==mx[p<<1])*cnt[p<<1];
        cnt[p]+=(mx[p]==mx[p<<1|1])*cnt[p<<1|1];
        if(mx[p]==mx[p<<1])se[p]=se[p<<1];
        else se[p]=mx[p<<1];
        if(mx[p]==mx[p<<1|1])se[p]=max(se[p],se[p<<1|1]);
        else se[p]=max(se[p],mx[p<<1|1]);
    }

    void build(int p,int l,int r) {
        tag[p]=-1;
        if(l==r) {
            sum[p]=mx[p]=a[l];
            se[p]=-1,cnt[p]=1;
            return;
        }
        int mid=(l+r)>>1;
        build(p<<1,l,mid);
        build(p<<1|1,mid+1,r);
        update(p);
    }

    void solve(int p,int limit) {
        if(mx[p]<=limit)return;
        if(se[p]<=limit&&limit<=mx[p]) {
            sum[p]-=1ll*(mx[p]-limit)*cnt[p];
            mx[p]=limit,tag[p]=limit;return;
        }
        solve(p<<1,limit),solve(p<<1|1,limit);
        update(p);
    }

    void push_down(int p) {
        if(~tag[p]) {
            solve(p<<1,tag[p]);
            solve(p<<1|1,tag[p]);
            tag[p]=-1;
        }
    }

    void Min(int p,int l,int r,int L,int R,int v) {
        if(L<=l&&r<=R) {solve(p,v);return;}
        int mid=(l+r)>>1;push_down(p);
        if(L<=mid)Min(p<<1,l,mid,L,R,v);
        if(R>mid)Min(p<<1|1,mid+1,r,L,R,v);
        update(p);
    }

    int queryMx(int p,int l,int r,int L,int R) {
        if(L<=l&&r<=R)return mx[p];
        int mid=(l+r)>>1,res=0;push_down(p);
        if(L<=mid)res=max(res,queryMx(p<<1,l,mid,L,R));
        if(R>mid)res=max(res,queryMx(p<<1|1,mid+1,r,L,R));
        return res;
    }

    ll querySum(int p,int l,int r,int L,int R) {
        if(L<=l&&r<=R)return sum[p];
        int mid=(l+r)>>1;ll res=0;
        push_down(p);
        if(L<=mid)res+=querySum(p<<1,l,mid,L,R);
        if(R>mid)res+=querySum(p<<1|1,mid+1,r,L,R);
        return res;
    }
}T;

int main() {
    int Test=read();
    while(Test--) {
        n=read(),m=read();
        for(int i=1;i<=n;i++)
            a[i]=read();
        T.build(1,1,n);
        for(int i=1;i<=m;i++) {
            int opt=read(),l=read(),r=read();
            if(!opt) {
                int v=read();
                T.Min(1,1,n,l,r,v);
            }
            if(opt==1)printf("%d\n",T.queryMx(1,1,n,l,r));
            if(opt==2)printf("%lld\n",T.querySum(1,1,n,l,r));
        }
    }
    return 0;
}

历史最大值

这个东西不好讲。。。

一般都是一个数组\(a\)一个数组\(b\)\(b_i\)记录\(a_i\)曾经到达过的最大/最小值或者是每次操作完之后\(a_i\)的和。然后在\(a\)上操作来操作去,冷不丁的问你\(b\)数组的区间最大/最小值或者是区间和。

这种问题一般分为四类(第二类和第四类如果我碰见了果断暴力分走起)

第一类:可以用延迟标记处理的问题

这类问题一般用延迟标记可以解决,不过要记录标记从上一次下传标记到现在这一次的最值。维护的东西比较多,但是并不会涉及什么骚操作,对合并标记功法要求很高。

第二类:无法用延迟标记处理的问题

都说了不能用延迟标记处理了就弃了吧~根本不会

第三类:无区间最值操作的区间问题

一般都要用到辅助数组,转化成传统的问题。

第四类:有区间最值操作的区间问题

无区间最值操作都不会了,有区间最值还玩屁~根本不会

参考资料:国家集训队2016论文集——吉如一《区间最值操作与历史最值问题》

竞赛是灵活的,所以我觉得吉司机线段树要考也不会变到哪去,只要掌握这种以全局来分析问题复杂度的思想,就没问题了。这就是我不学历史最值问题的理由

猜你喜欢

转载自www.cnblogs.com/AKMer/p/10225100.html