1 条题解

  • 0
    @ 2025-8-24 21:47:49

    自动搬运

    查看原文

    来自洛谷,原作者为

    avatar I_AM_HelloWord
    **

    搬运于2025-08-24 21:47:49,当前版本为作者最后更新于2018-01-27 21:13:12,作者可能在搬运后再次修改,您可在原文处查看最新版

    自动搬运只会搬运当前题目点赞数最高的题解,您可前往洛谷题解查看更多

    以下是正文


    此题有三种做法:

    1.平衡树套线段树

    2.线段树套线段树

    3.整体二分

    这里介绍一下后两种做法。

    我先写了个200+行的线段树套平衡树(注意顺序),然后效率是O(nlog3n)O(nlog^3n),简直爆炸。

    2.线段树套线段树

    为什么线段树套平衡树是O(nlog3n)O(nlog^3n)的呢?因为我们需要查询的是权值,然而我外层是线段树,所以先二分答案,然后分解区间,到了各个平衡树时,又相当于二分了一次位置,实在是没必要。

    因此,这里,我们的线段树套线段树,确切的说应该是权值线段树套区间线段树。

    外层是权值线段树,实质上就是一个二分答案的过程,然后运用类似归并树和主席树的思想,我们设询问区间为[ql,qr][ql,qr],答案区间为[l,r][l,r],取其midmid,然后计算在左儿子[l,mid][l,mid]和询问区间[ql,qr][ql,qr]中的数的个数,那么我们就可以判断答案是大于midmid还是小于midmid的了。而在权值线段树的每一个节点上都建一个区间线段树,来维护该权值在[1,n][1,n]所有区间上的出现次数,然后维护一个区间和就好了。

    还有就是防止MLE,在区间线段树上搞个动态开点就好了。

    算法设计基本上就是这样了,具体细节就看代码了。

    参考代码:

    #include<cstdio>
    #include<algorithm>
    #include<cstring>
    #include<vector>
    #include<iostream>
    #include<ctime>
    #define rint register int
    using namespace std;
    typedef long long ll;
    const int inf = 0x7fffffff;
    const int N=5e4+5;
    const int Tree=N*400;
    int n,m,node=0,totn;
    int ql[N],qr[N],op[N],b[N],k[N];
    int rt[N<<2],tag[Tree];
    ll sz[Tree];
    struct Tnode{
        int L,R;
    }T[Tree];
    inline int read(){
        int x=0,f=1;char ch=getchar();
        while (ch<'0' || ch>'9'){if (ch=='-')f=-1;ch=getchar();}
        while ('0'<=ch && ch<='9'){x=(x<<3)+(x<<1)+(ch^48);ch=getchar();}
        return x*f;
    }
    #define lc p<<1
    #define rc p<<1|1
    inline void pushdown(int p,int l,int r){
        int &v=tag[p],mid=(l+r)>>1;
        if (!T[p].L)T[p].L=++node;
        if (!T[p].R)T[p].R=++node;
        tag[T[p].L]+=v,tag[T[p].R]+=v;
        sz[T[p].L]+=v*(mid-l+1),sz[T[p].R]+=v*(r-mid);
        v=0;
    }
    inline void update(int &p,int ql,int qr,int l=1,int r=n){
        if (!p)p=++node;
        if (ql<=l && r<=qr){
            ++tag[p];sz[p]+=r-l+1;
            return;
        }
        if (tag[p])pushdown(p,l,r);
        rint mid=(l+r)>>1;
        if (ql<=mid) update(T[p].L,ql,qr,l,mid);
        if (mid<qr) update(T[p].R,ql,qr,mid+1,r);
        sz[p]=sz[T[p].L]+sz[T[p].R];
    }
    inline ll getsum(int &p,int ql,int qr,int l=1,int r=n){
        if (!p)return 0;
        if (ql<=l && r<=qr) return sz[p];
        if (tag[p])pushdown(p,l,r);
        rint mid=(l+r)>>1;
        ll tt=0;
        if (ql<=mid) tt+=getsum(T[p].L,ql,qr,l,mid);
        if (mid<qr) tt+=getsum(T[p].R,ql,qr,mid+1,r);
        return tt;
    }
    inline void add(int ql,int qr,int k,int p=1,int l=1,int r=totn){
        update(rt[p],ql,qr);
        if (l==r) return;
        rint mid=(l+r)>>1;
        if (k<=mid) add(ql,qr,k,lc,l,mid);
            else add(ql,qr,k,rc,mid+1,r);
    }
    inline int query(int ql,int qr,ll k,int p=1,int l=1,int r=totn){
        if (l==r) return b[l];
        rint mid=(l+r)>>1;
        ll tt=getsum(rt[rc],ql,qr);
        if (tt<k) return query(ql,qr,k-tt,lc,l,mid);
            else return query(ql,qr,k,rc,mid+1,r);
    }
    int main(){
        n=read(),m=read();
        for (rint i=1;i<=m;++i){
            op[i]=read(),ql[i]=read(),qr[i]=read(),k[i]=read();
            if (op[i]==1)b[++totn]=k[i];
        }
        sort(b+1,b+totn+1);
        totn=unique(b+1,b+totn+1)-b-1;
        for (rint i=1;i<=m;++i)
            if (op[i]==1)k[i]=lower_bound(b+1,b+totn+1,k[i])-b;
        for (rint i=1;i<=m;++i){
            if (op[i]==1){
                add(ql[i],qr[i],k[i]);
            }else{
                printf("%d\n",query(ql[i],qr[i],k[i]));
            }
        }
        return 0;
    }
    

    3.整体二分

    这应该属于练整体二分的一道比较基础的题目了。

    何谓整体二分?就是直接一起二分所有的询问操作的答案,然后暴力扫描当前操作区间,将其划分为答案的左子区间与右子区间两个部分。

    那么以什么为划分依据呢?看看这个操作对于左子区间有没有贡献。如果没有,那么就划分到右子区间中,然后将这个操作的权值更改为这个贡献减去所需的贡献,反之,则划分到左子区间中,同时将这个操作的贡献加入某一个容器,为询问操作服务。

    这么说可能有点晕。就这道题说的话,应该是这样:

    我们设尚未解决的操作区间为[ql,qr][ql,qr],答案区间为[l,r][l,r],令当前答案为midmid

    则若该操作是添加操作,如果其添加的C<=midC<=mid,这此次操作对于左子区间有贡献,加入左子区间中,并将区间线段树中的区间[q[i].l,q[i].r][q[i].l,q[i].r]整体加11.

    反之,则将操作加入到右子区间中。

    若该操作是询问操作,如果当前的midmid在线段树中查询到的,比它大的数的个数query()>=q[i].kquery()>=q[i].k,则证明该询问操作应该在右子区间内可以找到答案。反之,则将q[i].k=query()q[i].k-=query(),减去此次查询的贡献,然后将询问操作添加到左子区间中。

    参考代码:

    #include<cstdio>
    #include<algorithm>
    #include<cstring>
    #define rint register int
    using namespace std;
    typedef long long ll;
    const int N=5e4+5;
    struct Ask{
        int op,l,r,id;
        ll v;
    }q[N],tl[N],tr[N];
    int tag[N<<2],rec[N<<2],ans[N];
    ll sum[N<<2];
    int n,m,Q;
    inline int read(){
        int x=0,f=1;char ch=getchar();
        while (ch<'0' || ch>'9'){if (ch=='-')f=-1;ch=getchar();}
        while ('0'<=ch && ch<='9'){x=(x<<3)+(x<<1)+(ch^48);ch=getchar();}
        return x*f;
    }
    #define lc p<<1
    #define rc p<<1|1
    inline void pushdown(int p,int l,int r){
        if (rec[p]){
            rec[p]=0;
            tag[lc]=tag[rc]=sum[lc]=sum[rc]=0;
            rec[lc]=1,rec[rc]=1;
        }
        if (tag[p]){
            rint mid=(l+r)>>1;
            tag[lc]+=tag[p],tag[rc]+=tag[p];
            sum[lc]+=tag[p]*(mid-l+1);
            sum[rc]+=tag[p]*(r-mid);
            tag[p]=0;
        }
    }    
    inline void add(int ql,int qr,int w,int p=1,int l=1,int r=n){
        if (ql<=l && r<=qr){
            tag[p]+=w;sum[p]+=w*(r-l+1);
            return;
        }
        if (tag[p] || rec[p])pushdown(p,l,r);
        rint mid=(l+r)>>1;
        if (ql<=mid) add(ql,qr,w,lc,l,mid);
        if (mid<qr) add(ql,qr,w,rc,mid+1,r);
        sum[p]=sum[lc]+sum[rc];
    }
    inline ll query(int ql,int qr,int p=1,int l=1,int r=n){
        if (ql<=l && r<=qr)return sum[p];
        rint mid=(l+r)>>1;
        ll tt=0;
        if (tag[p] || rec[p])pushdown(p,l,r);
        if (ql<=mid) tt+=query(ql,qr,lc,l,mid);
        if (mid<qr) tt+=query(ql,qr,rc,mid+1,r);
        return tt;
    }
    inline void solve(int st,int en,int l,int r){
        if (l==r){
            for (rint i=st;i<=en;++i)
                if (q[i].op==2) ans[q[i].id]=l;
            return;
        }
        rint mid=(l+r)>>1;
        bool fl=0,fr=0;
        rint L=0,R=0;
        rec[1]=1;tag[1]=sum[1]=0;
        for (rint i=st;i<=en;++i)
            if (q[i].op==1){
                if (q[i].v>mid){
                    add(q[i].l,q[i].r,1);
                    tr[++R]=q[i];
                }else
                    tl[++L]=q[i];
            }else{
                ll val=query(q[i].l,q[i].r);
                if (val<q[i].v){
                    q[i].v-=val;
                    fl=1;
                    tl[++L]=q[i];
                }else{
                    fr=1;
                    tr[++R]=q[i];
                }
            }
        for (rint i=1;i<=L;++i) q[st+i-1]=tl[i];
        for (rint i=L+1;i<=L+R;++i) q[st+i-1]=tr[i-L];
        if (fl) solve(st,st+L-1,l,mid);
        if (fr) solve(st+L,en,mid+1,r);
    }
    int main(){
        n=read(),m=read();
        for (rint i=1;i<=m;++i){
            q[i].op=read(),q[i].l=read(),q[i].r=read(),q[i].v=read();
            if (q[i].op==2)q[i].id=++Q;
        }
        solve(1,m,-n,n);
        for (rint i=1;i<=Q;++i)
            printf("%d\n",ans[i]);
        return 0;
    }
    

    貌似一比,树套树还好写一点哈?

    • 1

    信息

    ID
    2405
    时间
    1500ms
    内存
    500MiB
    难度
    6
    标签
    递交数
    0
    已通过
    0
    上传者