P4062 [Code+#1]Yazid 的新生舞会(区间绝对众数+分治/树状数组维护高维前缀和)

P4062 [Code+#1]Yazid 的新生舞会

杭电多校懂得都懂

Code1

分治

比较喜欢分治的做法,非常好写。skylee大佬题解


首先对于任何一个区间来说,由于两个端点不确定性非常难以一次性统计多组区间,因为它们没有相似之处。

考虑分治,花费 log ⁡ \log log的代价使得当前考虑的区间必须经过 mid \text{mid} mid,意味着当前区间左端点必须在 [ l , mid ] [\text{l},\text{mid}] [l,mid]区间内部,而右端点必须在 [ mid + 1 , r ] [\text{mid}+1,\text{r}] [mid+1,r]区间内部,这样的区间特殊在一定经过mid使得可以先预处理左端点的一些信息,然后枚举右端点一次性统计多个区间。

首先枚举可能作为区间绝对众数的数 v \text v v,然后考虑哪些上述区间能使得该数作为区间的绝对众数。

vl , vr \text{vl},\text{vr} vl,vr分别是可能使 v \text v v作为区间 [ vl , vr ] [\text{vl},\text{vr}] [vl,vr]的绝对众数。
cnt vl \text{cnt}_{\text {vl}} cntvl vl → mid \text{vl}\to \text{mid} vlmid 数字 v \text v v出现的次数。
cnt vr \text{cnt}_{\text {vr}} cntvr mid + 1 → vr \text{mid}+1\to \text {vr} mid+1vr 数字 v \text v v出现的次数。

cnt vr + cnt vl > 1 2 [ r − l + 1 ] \text{cnt}_{\text {vr}}+\text{cnt}_{\text {vl}}>\frac{1}{2}[r-l+1] cntvr+cntvl>21[rl+1]
从上面式子可以得出
2 cnt vl + l − 1 > r − 2 cnt vr 2\text{cnt}_{\text {vl}}+l-1>r-2\text{cnt}_{\text {vr}} 2cntvl+l1>r2cntvr

考虑枚举 vr \text{vr} vr,我们只需要统计有哪些左端点 vl \text{vl} vl满足上面式子即可,显然可以预处理+前缀和一次性统计。


还有一个问题就是哪些数可能是区间的绝对众数?

如果一个数 v v v是区间 [ vl , vr ] [\text{vl},\text{vr}] [vl,vr]的绝对众数,那么它一定是区间 [ vl , mid ] [\text{vl},\text{mid}] [vl,mid]以及 [ mid + 1 , vr ] [\text{mid}+1,\text{vr}] [mid+1,vr]的绝对众数,于是可以提前预处理。

显然能够满足上述条件作为区间绝对众数的种类不会很多。上面大佬题解说是 log ⁡ \log log量级的。

时间复杂度 O ( n log ⁡ 2 n ) O(n\log ^2n) O(nlog2n)

#include<bits/stdc++.h>
using namespace std;
using ll=long long;
template <class T=int> T rd()
{
    T res=0;T fg=1;
    char ch=getchar();
    while(!isdigit(ch)) {if(ch=='-') fg=-1;ch=getchar();}
    while( isdigit(ch)) res=(res<<1)+(res<<3)+(ch^48),ch=getchar();
    return res*fg;
}
const int N=500010;
int a[N],n;
ll ans;
int b[N],cnt;
int mp[N],in[N];
int num[2*N];
void solve(int l,int r)
{
    if(l==r) return ans++,void();
    
    int mid=l+r>>1;
    solve(l,mid),solve(mid+1,r);
    
    cnt=0;
    for(int i=mid;i>=l;i--)
    {
        mp[a[i]]++;
        if(mp[a[i]]>(mid-i+1)/2)
        {
            if(in[a[i]]) continue;
            in[a[i]]=1;
            b[++cnt]=a[i];
        }
    }
    for(int i=l;i<=mid;i++) mp[a[i]]--;
    
    for(int i=mid+1;i<=r;i++)
    {
        mp[a[i]]++;
        if(mp[a[i]]>(i-mid)/2)
        {
            if(in[a[i]]) continue;
            in[a[i]]=1;
            b[++cnt]=a[i];
        }
    }
    for(int i=mid+1;i<=r;i++) mp[a[i]]--;
    for(int i=l;i<=r;i++) in[a[i]]=0;
    
    //for(int i=1;i<=cnt;i++) cout<<b[i]<<" \n"[i==cnt];
    
    for(int i=1;i<=cnt;i++)
    {
        int cur=0;
        int L=3*n,R=0;
        for(int j=mid;j>=l;j--)
        {
            if(a[j]==b[i]) cur++;
            
            num[2*cur+j-1]++;
            L=min(L,2*cur+j-1);
            R=max(R,2*cur+j-1);
        }
        for(int i=R;i>L;i--) num[i-1]+=num[i];
        
        cur=0;
        for(int j=mid+1;j<=r;j++)
        {
            if(a[j]==b[i]) cur++;
            ans+=(num[max(L,j-2*cur+1)]);
        }
        for(int i=R;i>=L;i--) num[i]=0;
    }
    
}
int main()
{
    n=rd();rd();
    for(int i=1;i<=n;i++) a[i]=rd();
    solve(1,n);
    printf("%lld\n",ans);
    return 0;
}
Code2

OMG_wc大佬题解
Zechariah大佬题解

其实赛时的将此问题转化成了下面的问题:如何求小于一段公差为1的等差数列的个数?

  • 求在 a 1 → n a_{1\to n} a1n中求小于 x x x的个数,小于 x + 1 x+1 x+1的个数,小于 x + 2 x+2 x+2的个数,小于 x + k x+k x+k的个数把他们累加。

然后就死了???这不就是前缀和然后区间询问吗?wtcl

开个桶,然后做一个前缀和,然后就是个区间询问 [ x , x + k ] [x,x+k] [x,x+k]的问题。。。


此题首先记录每个数出现的位置,单独考虑每个数作为区间的绝对众数。
不妨设当前考虑的数为 v \text v v

v \text v v能作为区间 ( L , R ] (\text L,\text R] (L,R]的众数的充要条件是
cnt R − cnt L > R − L 2 \text{cnt}_{\text R}-\text{cnt}_{\text L}>\frac {\text R-\text L}{2} cntRcntL>2RL

2 cnt L − L < 2 cnt R − R , L < R 2\text{cnt}_{\text L}-\text L<2\text{cnt}_{\text R}-\text R ,\text{L}<\text R 2cntLL<2cntRR,L<R

b L = 2 cnt L − L \text b_{\text L}=2\text{cnt}_{\text L}-\text L bL=2cntLL

于是转化成二维偏序:
L < R b L < b R \text{L}<\text R \\ \text b_{\text L}<\text b_{\text R} L<RbL<bR

显然我们枚举右端点,用个树状数组就可以统计,但是复杂度不行。

观察每个数出现的位置将整个区间划分为下面模式
[ 1 , …   ) [ …   ) [ …   ) [ … , n+1 ) \color{blue}[1,\dots)[\dots)\color{red}[\dots)\color{black}[\dots,\text{n+1}) [1,)[)[)[,n+1)
上面 ) ) )代表的就是该数出现的位置。
对于每个 [ …   ) \color{red}[\dots) [)内部 b i \text b_{\text i} bi是单调下降,且公差为1,显然当区间右端点在此区间内部时,区间左端点不可能在其内部。换句话说就是只有前面的即 [ 1 , …   ) [ …   ) \color{blue}[1,\dots)[\dots) [1,)[)可能对区间右端点在 [ …   ) \color{red}[\dots) [)产生贡献。

而且重要的是 [ …   ) \color{red}[\dots) [) b j \text b_{\text j} bj连续的,即对于每一个 [ …   ) \color{red}[\dots) [) b j \text b_{\text j} bj我们需要在 [ 1 , …   ) [ …   ) \color{blue}[1,\dots)[\dots) [1,)[)找到 b i < b j \text b_{\text i}<\text b_{\text j} bi<bj,显然就是最开始那个问题,只需要求个前缀和然后区间询问即可。

每次过考虑 [ …   ) \color{red}[\dots) [)后进行区间修改,将 [ …   ) \color{red}[\dots) [)内部的 b j \text b_{\text j} bj插入数据结构中。


区间修改+前缀和区间询问
树状数组的话可以把差分转化为单点修改,那么本次要做到区间询问就意味着要用树状数组维护三维前缀和
∑ i = 1 n ∑ j = 1 i ∑ k = 1 j d k = 1 2 [ ( n 2 + 3 n + 2 ) ∑ i = 1 n d i − ( 2 n + 3 ) ∑ i = 1 n i ⋅ d i + ∑ i = 1 n i 2 ⋅ d i ] \sum_{i=1}^{n}\sum_{j=1}^i\sum_{k=1}^jd_k=\frac{1}{2}[(n^2+3n+2)\sum_{i=1}^nd_i-(2n+3)\sum_{i=1}^ni·d_i+\sum_{i=1}^ni^2·d_i] i=1nj=1ik=1jdk=21[(n2+3n+2)i=1ndi(2n+3)i=1nidi+i=1ni2di]

#include<bits/stdc++.h>
using namespace std;
using ll=long long;
template <class T=int> T rd()
{
    T res=0;T fg=1;
    char ch=getchar();
    while(!isdigit(ch)) {if(ch=='-') fg=-1;ch=getchar();}
    while( isdigit(ch)) res=(res<<1)+(res<<3)+(ch^48),ch=getchar();
    return res*fg;
}
const int N=500010;
int a[N],n;
vector<int> loc[N];
ll c0[N<<1],c1[N<<1],c2[N<<1];
void update(int k,ll v,int n)
{
    ll i=k*v,ii=1ll*k*k*v;
    while(k<=n)
    {
        c0[k]+=v;
        c1[k]+=i;
        c2[k]+=ii;
        k+=k&-k;
    }
}
ll qsum(int k)
{
    ll ans=0;
    ll k1=1ll*k*k+3*k+2,k2=2*k+3;
    while(k)
    {
        ans+=k1*c0[k]-k2*c1[k]+c2[k];
        k-=k&-k;
    }
    return ans>>1;
}
int main()
{
    n=rd();rd();
    for(int i=1;i<=n;i++) a[i]=rd(),loc[a[i]].push_back(i);

    ll ans=0;
    const int Bs=n+1;
    
    for(int i=0;i<n;i++)
    {
        if(loc[i].empty()) continue;
        
        int pre=0;
        loc[i].push_back(n+1);
        
        for(int j=0;j<loc[i].size();j++)
        {
            int R=2*j-pre+Bs,L=2*j-(loc[i][j]-1)+Bs;
            ans+=qsum(R-1)-qsum(max(0,L-2));// 严格小于
            update(L,1,n<<1|1);
            update(R+1,-1,n<<1|1);
            pre=loc[i][j];
        }
        pre=0;
        for(int j=0;j<loc[i].size();j++)
        {
            int R=2*j-pre+Bs,L=2*j-(loc[i][j]-1)+Bs;
            update(L,-1,n<<1|1);
            update(R+1,+1,n<<1|1);
            pre=loc[i][j];
        }
    }
    printf("%lld\n",ans);
    return 0;
}

要加油哦~

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值