1 条题解

  • 0
    @ 2025-8-24 23:06:56

    自动搬运

    查看原文

    来自洛谷,原作者为

    avatar qwer6
    “向我们曾迷惘的路致敬!”

    搬运于2025-08-24 23:06:56,当前版本为作者最后更新于2024-12-20 21:20:32,作者可能在搬运后再次修改,您可在原文处查看最新版

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

    以下是正文


    这道题还挺有意思,需要一步一步分析下来,最后才能发现正解算法。那我们可以来看点部分分想想正解是怎么来的。

    子任务一

    一个暴力就可以解决的问题。

    具体的,我们枚举每一个区间,开一个 cntcnt 数组,记录每一个数字出现的次数。然后,我们记录恰好出现一次,两次,三次,四次的数字个数,一边扩大区间,一边统计即可。

    for(int l=1;l<=n;l++){
        fill(cnt+1,cnt+n+1,0);
        cnt1=cnt2=cnt3=cnt4=0;
        for(int r=l,x;r<=n;r++){
            x=cnt[a[r]];
            if(x==0)cnt1++;
            else if(x==1)cnt1--,cnt2++;
            else if(x==2)cnt2--,cnt3++;
            else if(x==3)cnt3--,cnt4++;
            else if(x==4)cnt4--;
            cnt[a[r]]++;
            if(k>=1&&!cnt1)continue;
            if(k>=2&&!cnt2)continue;
            if(k>=3&&!cnt3)continue;
            if(k>=4&&!cnt4)continue;
            ans++;
        }
    }
    

    子任务二

    我们可以凭借敏锐的直觉感受到合法区间的长度应该是 k×(k+1)2\frac {k\times (k+1)}{2},为什么呢?我们知道一共只有 kk 种数,而满足条件的话,区间中每一种数出现的次数显然不能相同,且分别是 11kk 次,然后相加即可。

    那这部分可以使用一个双指针来解决。

    //这部分的代码着实有点抽象了,有种复沓的美
    len=k*(1+k)/2;
    for(int i=1,x;i<=len;i++){
        x=cnt[a[i]];
        if(x==0)cnt1++;
        else if(x==1)cnt1--,cnt2++;
        else if(x==2)cnt2--,cnt3++;
        else if(x==3)cnt3--,cnt4++;
        else if(x==4)cnt4--;
        cnt[a[i]]++;
    }
    if(k==1&&cnt1)ans++;
    else if(k==2&&cnt1&&cnt2)ans++;
    else if(k==3&&cnt1&&cnt2&&cnt3)ans++;
    else if(k==4&&cnt1&&cnt2&&cnt3&&cnt4)ans++;
    for(int r=len+1,x;r<=n;r++){
        x=cnt[a[r-len]];
        if(x==1)cnt1--;
        else if(x==2)cnt2--,cnt1++;
        else if(x==3)cnt3--,cnt2++;
        else if(x==4)cnt4--,cnt3++;
        else if(x==5)cnt4++;
        cnt[a[r-len]]--;
        x=cnt[a[r]];
        if(x==0)cnt1++;
        else if(x==1)cnt1--,cnt2++;
        else if(x==2)cnt2--,cnt3++;
        else if(x==3)cnt3--,cnt4++;
        else if(x==4)cnt4--;
        cnt[a[r]]++;
        if(k==1&&cnt1)ans++;
        else if(k==2&&cnt1&&cnt2)ans++;
        else if(k==3&&cnt1&&cnt2&&cnt3)ans++;
        else if(k==4&&cnt1&&cnt2&&cnt3&&cnt4)ans++;
    }
    write(ans),Nxt;
    

    子任务三

    这是最核心的一个子任务,因为它直接提示了我们正解的做法。

    我们考虑 k=1k=1 的特殊情况,记录 fif_i 表示 ii 到当前右端点的区间中,有多少个恰好出现一次的数字。

    那这就变成了一个区间加的问题了,具体的,我们记 preipre_i 表示 aia_i 这个值上一次出现的位置,那么对于 [prei+1,i][pre_i+1,i] 的这一段区间,aia_i 这个数显然从原来没有贡献到现在有贡献,区间加一,对于 [preprei+1,prei][pre_{pre_i}+1,pre_i] 这一段区间,aia_i 从有贡献到没有贡献,区间减一,那以 ii 为右端点的合法区间就是 [1,i][1,i] 中有多少个不为 00 的位置。

    这显然可以使用一个线段树进行优化。

    /*线段树代码*/
    struct Node{
        int mi,cnt;
        Node friend operator +(Node a,Node b){
            Node res;
            res.mi=min(a.mi,b.mi);
            res.cnt=0;
            if(res.mi==a.mi)res.cnt+=a.cnt;
            if(res.mi==b.mi)res.cnt+=b.cnt;
            return res;
        }
        Node operator +(const int &a)const{
            return {mi+a,cnt};
        }
    };
    struct Segment_tree{
        Node c[N<<2];
        int tag[N<<2];
        #define ls p<<1
        #define rs p<<1|1
        #define mid (l+r>>1)
        void pushup(int p){c[p]=c[ls]+c[rs];}
        void Tag(int p,int v){
            c[p]=c[p]+v;
            tag[p]+=v;
        }
        void pushdown(int p){
            if(!tag[p])return ;
            Tag(ls,tag[p]);
            Tag(rs,tag[p]);
            tag[p]=0;
        }
        void build(int p,int l,int r){
            if(l==r)return void(c[p]={0,1});
            build(ls,l,mid),build(rs,mid+1,r);
            pushup(p);
        }
        void change(int p,int l,int r,int L,int R,int v){
            if(L<=l&&r<=R)return Tag(p,v);
            pushdown(p);
            if(mid>=L)change(ls,l,mid,L,R,v);
            if(mid<R)change(rs,mid+1,r,L,R,v);
            pushup(p);
        }
        int query(){
            if(c[1].mi==0)return n-c[1].cnt;
            return n;
        }
    }
    
    /*主函数*/
    for(int i=1;i<=n;i++){
        pre[i]=las[a[i]];
        las[a[i]]=i;
    }
    Set.build(1,1,n);
    for(int i=1;i<=n;i++){
        Set.change(1,1,n,pre[i]+1,i,1);
        if(pre[i])Set.change(1,1,n,pre[pre[i]]+1,pre[i],-1);
        ans+=Set.query();
        //查询 1~n 中有多少个不为 0 的节点,和查询 1~i 中有多少个不为 0 的节点是等价的,因为 i+1~n 显然为 0
    }
    write(ans);
    

    4.正解

    我们回忆一下子任务三的求解过程,可以分为两步。第一步,区间修改;第二步,求有多少个不为 00 的位置。

    看着着实有点眼熟。

    这不是求矩形面积并的过程吗?

    我们将右端点视作矩形的宽,左端点视作矩形的长,就可以很明显的发现这个结论。

    我们沿着这个思路想下去,会发现这样一件事:最后的答案其实就是求区间中拥有恰好出现一次,两次,三次,四次的合法区间各自的矩形面积并求交的答案。

    但是求交着实不太好求,所以我们做一点容斥,把交搞成多个并相减的形式即可。

    /*线段树的代码和上面是一样的,这里就不放了*/
    for(int st=1,tp;st<(1<<k);st++){
        Set.build(1,1,n);
        tp=(cnt[st]&1)?1:-1;//容斥原理,奇加偶减,cnt[st] 表示 st 在二进制下有几个 1
        for(int i=1,now;i<=n;i++){
            now=i;
            for(int j=0;j<k;j++){
                if(st&(1<<j)){
                    Set.change(1,1,n,pre[now]+1,now,1);
                    if(pre[pre[now]]<pre[now])Set.change(1,1,n,pre[pre[now]]+1,pre[now],-1);
                }
                now=pre[now];
                if(!now)break;
            }
            ans+=tp*(Set.query());
        }
    }
    
    • 1

    信息

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