“21 天好习惯”第一期-16

Array

原题链接:P4062 [Code+#1]Yazid 的新生舞会

解法一

分块 O ( n n ) {O(n\sqrt{n})} O(nn )

对于所有 c n t x ≤ n {cnt_x\le \sqrt{n}} cntxn x {x} x,暴力扫描所有长度不大于 2 n {2\sqrt{n}} 2n 的区间,即枚举左端点,然后往右扫描,长度不大于 2 n {2\sqrt{n}} 2n ,然后维护众数出现的次数,当众数出现的次数大于区间的一半时,该区间对答案的贡献加一。这里注意不要把 c n t x > n {cnt_x> \sqrt{n}} cntx>n x {x} x维护进众数里,后面会单独计算每个 c n t x > n {cnt_x> \sqrt{n}} cntx>n x {x} x为众数的所有合法区间。

如果数组只由 0 、 1 {0、1} 01组成,计算以 1 {1} 1为众数的合法区间数量。 s i s_i si表示前缀和。那么满足条件的区间需要满足: s R − s L > R − L + 1 2 ⇔ 2 ∗ s R − R > 2 ∗ s L − L s_R-s_L>\frac{R-L+1}{2} \Leftrightarrow 2*s_R-R>2*s_L-L sRsL>2RL+12sRR>2sLL
r e s {res} res表示右端点取 R − 1 {R-1} R1,左端点合法的数量,考虑到 R − 1 {R-1} R1后移, 2 ∗ s R − R {2*s_R-R} 2sRR等于 2 ∗ s R − 1 − ( R − 1 ) {2*s_{R-1}-(R-1)} 2sR1(R1)加一或者减一,那么右端点取 R {R} R后左区间和法的数量为 r e s + c n t [ 2 ∗ s R − 1 − ( R − 1 ) ] {res+cnt[2*s_{R-1}-(R-1)]} res+cnt[2sR1(R1)] r e s − c n t [ 2 ∗ s R − 1 − ( R − 1 ) − 1 ] {res-cnt[2*s_{R-1}-(R-1)-1]} rescnt[2sR1(R1)1]

对于所有 c n t x > n {cnt_x> \sqrt{n}} cntx>n x {x} x,这些不同的数字不会超过 n {\sqrt{n}} n 个,枚举每种 x {x} x为众数的贡献,用上面的方法。

在洛谷上交18s左右,改一下再交hdu应该会T,最慢的做法

原题的代码

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=5e5+7,maxm=1e6+7,m=5e5+1;
int a[maxn],f2[maxn],cnt[maxm];
bool vis[maxn];

unordered_set<int>s;
signed main() {
    int n,t;
    scanf("%d%*d",&n);
    t=sqrt(n);
    for(int i=1; i<=n; ++i) {
        scanf("%d",&a[i]);
        s.insert(a[i]);
        ++cnt[a[i]];
    }
    ll ans(0);
    int c2(0);
    for(int num:s) {
        if(cnt[num]<t) vis[num]=1;
        else f2[++c2]=num;
        cnt[num]=0;
    }
    for(int i=1,r,maxx; i<=n; ++i) {
        r=i+2*t-1;
        if(r>n) r=n;
        maxx=0;
        for(int l=i; l<=r; ++l) {
            if(vis[a[l]]) ++cnt[a[l]];
            maxx=maxx<cnt[a[l]]?cnt[a[l]]:maxx;
            if(maxx>(l-i+1)/2) ++ans;
        }
        for(int l=i; l<=r; ++l) {
            if(vis[a[l]]) --cnt[a[l]];
        }
    }
    for(int i=1,sum,res; i<=c2; ++i) {
        res=sum=0;
        for(int k=0;k<maxm;k++) cnt[k]=0;
        ++cnt[m];
        for(int j=1; j<=n; ++j) {
            if(a[j]==f2[i]) res+=cnt[2*sum-(j-1)+m],++sum;
            else res-=cnt[2*sum-(j-1)-1+m];
            ans+=res;
            ++cnt[2*sum-j+m];
        }
    }
    printf("%lld\n",ans);
    return 0;
}

解法二、三

思路

求数 x {x} x为众数的区间数时,原数组中等于 x {x} x的数看作 1 {1} 1,不等于 x {x} x的数看作 − 1 {-1} 1,答案是把每个位置前缀和后去数前面有几个前缀和比当前缀和小的。

求数 1 {1} 1为众数的区间数,原数组 { 1   1   2   2   2   1   1 } {\{1\ 1\ 2\ 2\ 2\ 1\ 1\}} {1 1 2 2 2 1 1},化为数组 { 0   1   2   1   0   − 1   0   1 } {\{0\ 1\ 2\ 1\ 0\ -1\ 0\ 1\}} {0 1 2 1 0 1 0 1},每个位置的贡献为 { 1   2   1   0   0   1   4 } {\{1\ 2\ 1\ 0\ 0\ 1\ 4\}} {1 2 1 0 0 1 4}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ooJbXX1f-1636734750035)(https://uploadfiles.nowcoder.com/images/20210807/137609403_1628307719899/5B814454E3D353962B59FFBBBBEEC0AB “图片标题”)]

树状数组 O ( n l o g n ) {O(nlog n)} O(nlogn)

这个跑得最快了

d d d表示处理后的数组,前缀和 T i = ∑ j = 1 i d j T_i=\sum_{j=1}^{i}{d_j} Ti=j=1idj,假设当前位置的前缀和为 x {x} x,那么当前位置的贡献为 G x − 1 = ∑ i = 1 x − 1 T i {G_{x-1}=\sum_{i=1}^{x-1}T_i} Gx1=i=1x1Ti

求数 x {x} x为众数的区间数时,如果有 k {k} k x {x} x,那么可以分成 k + 1 {k+1} k+1个区间(以每个 0 {0} 0开头或 x {x} x开头,到下个 x {x} x之前的数),当 x = 1 {x=1} x=1时,上面的原数组可以分为 { 1 } , { 1   2   2   2 } , { 1 } , { 1 } {\{1\},\{1\ 2\ 2\ 2\},\{1\},\{1\}} {1},{1 2 2 2},{1},{1}。可以发现同一段是不会有任何贡献的,因为同一段的前缀和是递减的。

假设某一段为 [ x , y ] {[x,y]} [x,y],那么我们可以每次询问 G y − 1 − G x − 2 G_{y-1}-G_{x-2} Gy1Gx2计算这一段区间对答案的总贡献,然后然后对 d x + = 1 , d y + 1 − = 1 {d_x+=1,d_{y+1}-=1} dx+=1,dy+1=1

找到怎么用数据结构维护 G x {G_x} Gx,假设 c i {c_i} ci表示 d i {d_i} di的差分数组。

G x = ∑ i = 1 x T i {G_{x}=\sum_{i=1}^{x}T_i} Gx=i=1xTi

= ∑ i = 1 x ∑ j = 1 i d j {=\sum_{i=1}^{x}\sum_{j=1}^{i}d_j} =i=1xj=1idj

= ∑ i = 1 x ∑ j = 1 i ∑ k = 1 j c k {=\sum_{i=1}^{x}\sum_{j=1}^{i}\sum_{k=1}^{j}c_k} =i=1xj=1ik=1jck

= ∑ k = 1 x c k ∗ ( 1 + 2 + 3 + . . . + ( x − k + 1 ) ) {=\sum_{k=1}^{x}c_k*(1+2+3+...+(x-k+1))} =k=1xck(1+2+3+...+(xk+1))

= ∑ k = 1 x c k ∗ ( x − k + 1 ) ( x − k + 2 ) 2 {=\sum_{k=1}^{x}c_k*\frac{(x-k+1)(x-k+2)}{2}} =k=1xck2(xk+1)(xk+2)

= ∑ k = 1 x c k ∗ ( 1 + 2 + 3 + . . . + ( x − k + 1 ) ) {=\sum_{k=1}^{x}c_k*(1+2+3+...+(x-k+1))} =k=1xck(1+2+3+...+(xk+1))

= ∑ k = 1 x ( x + 2 ) ∗ ( x + 1 ) 2 d k + 2 x + 3 2 d k ∗ k + 1 2 d k ∗ k 2 {=\sum_{k=1}^{x}}\frac{(x+2)*(x+1)}{2}d_k+\frac{2x+3}{2}d_k*k+\frac{1}{2}d_k*k^2 =k=1x2(x+2)(x+1)dk+22x+3dkk+21dkk2

于是我们只需要用树状数组维护 d i , d i ∗ i , d i ∗ i 2 d_i,d_i*i,d_i*i^2 di,dii,dii2。用线段树的话,如果直接维护前缀和 T i {T_i} Ti,可能常数会大些,需要的空间会更大。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=1e6+7,maxm=2e6+7,m=1e6+1;
ll t1[maxm],t2[maxm],t3[maxm];
inline void add(int x,ll d) {
    ll k=x;
    while(x<maxm) {
        t1[x]+=d;
        t2[x]+=d*k;
        t3[x]+=d*k*k;
        x+=x&-x;
    }
}
inline ll sum(int x) {
    ll res(0),k=x;
    while(x) {
        res+=t1[x]*(k+2)*(k+1)-t2[x]*(2*k+3)+t3[x];
        x-=x&-x;
    }
    return res/2;
}
int a[maxn];
vector<int>v[maxn];
unordered_set<int>s;
signed main() {
    int T,n;
    scanf("%d",&T);
    while(T--) {
        scanf("%d",&n);
        s.clear();
        for(int i=1; i<=n; ++i) {
            scanf("%d",&a[i]);
            s.insert(a[i]);
            v[a[i]].emplace_back(i);
        }
        ll ans(0);
        int x,y,pre,cnt;
        for(int num:s) {
            v[num].emplace_back(n+1);
            pre=cnt=0;
            for(int i:v[num]) {
                y=2*cnt-pre+m,x=2*cnt-(i-1)+m;
                ans+=sum(y-1)-sum(x-2);
                add(x,1);
                add(y+1,-1);
                ++cnt;
                pre=i;
            }
            pre=cnt=0;
            for(int i:v[num]) {
                y=2*cnt-pre+m,x=2*cnt-(i-1)+m;
                add(x,-1);
                add(y+1,1);
                ++cnt;
                pre=i;
            }
            v[num].clear();
        }
        printf("%lld\n",ans);
    }
    return 0;
}

O ( n ) {O(n)} O(n)

考虑到前缀和的连续性,取 x {x} x为众数,原数组中等于 x {x} x的数看作 1 {1} 1,不等于 x {x} x的数看作 − 1 {-1} 1

s u m {sum} sum表示处理后的数组前 i − 1 {i-1} i1项的前缀和, r e s {res} res表示第 i − 1 {i-1} i1项的贡献, x {x} x表示第 i {i} i项的贡献, y {y} y表示处理后的数组前 i {i} i项的前缀和。

a i = = x {a_i==x} ai==x时,明显有 z = r e s + c n t [ s u m ] , y = s u m + 1 {z=res+cnt[sum],y=sum+1} z=res+cnt[sum],y=sum+1;反之,有 z = r e s − c n t [ s u m − 1 ] , y = s u m − 1 {z=res-cnt[sum-1],y=sum-1} z=rescnt[sum1],y=sum1

如果这样做复杂度是 O ( n 2 ) O(n^2) O(n2)的。假设前面出现的最小的前缀和为 m i n n {minn} minn,当 x < = m i n n x<=minn x<=minn时,可以直接跳到下一个 a i = = x {a_i==x} ai==x的位置,因为被跳过的这一段对答案没有贡献,然后用差分数组标记,在起点加一,在终点减一。

code:

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=1e6+7;
int a[maxn];
vector<int>v[maxn];
unordered_set<int>s;
unordered_map<int,int>f1,f2;
signed main() {
    int T,n;
    scanf("%d",&T);
    while(T--) {
        scanf("%d",&n);
        s.clear();
        for(int i=1;i<=n;++i) {
            scanf("%d",&a[i]); s.insert(a[i]); v[a[i]].emplace_back(i);
        }
        ll ans(0),res;
        int sum,minn,k;
        for(int num:s) {
            v[num].emplace_back(n+1);
            f1.clear(),f2.clear();
            res=k=minn=sum=0;
            for(int i=1;i<=n;++i) {
                if(a[i]!=num&&sum==minn) {
                    int len=v[num][k]-i-1;
                    --f2[sum+1];
                    ++f2[sum-len];
                    i+=len;
                    sum-=len+1;
                }
                else if(a[i]==num) {
                    f1[sum]+=f2[sum];
                    f2[sum+1]+=f2[sum];
                    f2[sum]=0;
                    ++f1[sum];
                    res+=f1[sum];
                    ++sum;
                    ans+=res;
                    ++k;
                }
                else {
                    ++f1[sum];
                    --sum;
                    res-=f1[sum];
                    ans+=res;
                }
                if(minn>sum) minn=sum;
            }
            v[num].clear();
        }
        printf("%lld\n",ans);
    }
    return 0;
}

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值