【agc023E】Inversions(线段树,动态规划)

题面

AT
给定 ai a i ,求所有满足 piai p i ≤ a i 的排列 p p 的逆序对数之和。

题解

首先如何计算排列p的个数。
cnt[i] c n t [ i ] 表示 aki a k ≥ i 的个数,那么满足条件的 p p 的总数就是cnt[i](ni)
大概就是从 n n 开始填数,对于每个数字i而言,它一共有 cnt[i] c n t [ i ] 个位置可以填,但是后面的数字一共占用了 ni n − i 个位置,所以还剩下 cnt[i](ni) c n t [ i ] − ( n − i ) 个位置可以填。

先讲讲 O(n2log) O ( n 2 l o g ) 的做法。
枚举任意两个位置 i,j,i<j i , j , i < j ,考虑 i,j i , j 之间形成逆序对的贡献。
分成三种情况讨论。

1. ai=aj a i = a j
显然对于任意一种满足条件的方案,要么 pi>pj p i > p j 要么 pi<pj p i < p j ,并且两两对称,所以就是总方案除 2 2
2.ai<aj
pj p j [ai+1,aj] [ a i + 1 , a j ] 这部分的值的时候,显然不会产生贡献,而当 pj[1,ai] p j ∈ [ 1 , a i ] 时,则和上面是一样的,但是注意一下,此时我们直接把 aj a j 变成了 ai a i ,会对于总方案产生影响, [ai+1,aj] [ a i + 1 , a j ] 这一段的 cnt c n t 减小了,所以用一个线段树维护一下区间积,就可以方便的维护当前状态下的总方案,那么这一部分也很好算。
3. ai>aj a i > a j
很麻烦,但是我们可以正难则反来算,用总方案减去 pi<pj p i < p j 的方案数,转化成了第二种情况,也就是将 ai a i 直接变成 aj a j

这样子用线段树维护,时间复杂度 O(n2logn) O ( n 2 l o g n ) ,用前后缀之类的东西维护可以做到 O(n2) O ( n 2 )
代码如下:

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<vector>
using namespace std;
#define ll long long
#define MAX 200200
#define MOD 1000000007
#define inv2 500000004
#define lson (now<<1)
#define rson (now<<1|1)
inline int read()
{
    int x=0;bool t=false;char ch=getchar();
    while((ch<'0'||ch>'9')&&ch!='-')ch=getchar();
    if(ch=='-')t=true,ch=getchar();
    while(ch<='9'&&ch>='0')x=x*10+ch-48,ch=getchar();
    return t?-x:x;
}
int fpow(int a,int b)
{
    int s=1;
    while(b){if(b&1)s=1ll*s*a%MOD;a=1ll*a*a%MOD;b>>=1;}
    return s;
}
int n,a[MAX],tot,ans;
int cnt[MAX];
int t[2][MAX<<2];
void Build(int now,int l,int r)
{
    if(l==r){t[1][now]=cnt[l]-1-(n-l);t[0][now]=cnt[l]-(n-l);return;}
    int mid=(l+r)>>1;
    Build(lson,l,mid);Build(rson,mid+1,r);
    t[0][now]=1ll*t[0][lson]*t[0][rson]%MOD;
    t[1][now]=1ll*t[1][lson]*t[1][rson]%MOD;
}
int Query(int now,int l,int r,int L,int R,int c)
{
    if(L<=l&&r<=R)return t[c][now];
    int mid=(l+r)>>1,ret=1;
    if(L<=mid)ret=1ll*ret*Query(lson,l,mid,L,R,c)%MOD;
    if(R>mid)ret=1ll*ret*Query(rson,mid+1,r,L,R,c)%MOD;
    return ret;
}
int main()
{
    n=read();tot=1;
    for(int i=1;i<=n;++i)++cnt[a[i]=read()];
    for(int i=n-1;i;--i)cnt[i]+=cnt[i+1];
    for(int i=1;i<=n;++i)tot=1ll*tot*(cnt[i]-(n-i))%MOD;
    if(!tot){puts("0");return 0;}
    Build(1,1,n);
    for(int i=1;i<=n;++i)
        for(int j=i+1;j<=n;++j)
        {
            if(a[i]==a[j])ans=(ans+1ll*tot*inv2)%MOD;
            else if(a[i]<a[j])
            {
                int d=1ll*tot*fpow(Query(1,1,n,a[i]+1,a[j],0),MOD-2)%MOD;
                d=1ll*d*Query(1,1,n,a[i]+1,a[j],1)%MOD;
                ans=(ans+1ll*d*inv2)%MOD;
            }
            else
            {
                int d=1ll*tot*fpow(Query(1,1,n,a[j]+1,a[i],0),MOD-2)%MOD;
                d=1ll*d*Query(1,1,n,a[j]+1,a[i],1)%MOD;
                ans=(ans+MOD-1ll*d*inv2%MOD+tot)%MOD;
            }
        }
    printf("%d\n",ans);
    return 0;
}

而这种方法的复杂度瓶颈在于枚举任意两个位置之间逆序对的贡献。
考虑这个能否优化。
首先我们记 Di=cnt[i]1(ni)cnt[i](ni) D i = c n t [ i ] − 1 − ( n − i ) c n t [ i ] − ( n − i ) ,如果要修改某个区间的总方案数,等价于用全局的总方案数乘上某一段区间。设 S S 为全局总方案数,那么强制求改a[i],a[j]成一样的总方案数就是: S×maxi=min+1Di S × ∏ i = m i n + 1 m a x D i 。这个东西很显然可以写成前缀的形式,也就是 S=maxi=1Dimini=1Di S = ∏ i = 1 m a x D i ∏ i = 1 m i n D i
这样子只需要维护前缀积然后求个和就好了。
注意下 Di D i 可能为 0 0 <script type="math/tex" id="MathJax-Element-3466">0</script>,所以求的时候要分段计算一下贡献就好了,具体实现见代码。

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<vector>
using namespace std;
#define ll long long
#define MAX 200200
#define MOD 1000000007
#define inv2 500000004
inline int read()
{
    int x=0;bool t=false;char ch=getchar();
    while((ch<'0'||ch>'9')&&ch!='-')ch=getchar();
    if(ch=='-')t=true,ch=getchar();
    while(ch<='9'&&ch>='0')x=x*10+ch-48,ch=getchar();
    return t?-x:x;
}
int fpow(int a,int b)
{
    int s=1;
    while(b){if(b&1)s=1ll*s*a%MOD;a=1ll*a*a%MOD;b>>=1;}
    return s;
}
int n,a[MAX],tot,ans;
int cnt[MAX];
int c1[MAX],c2[MAX];
int lb(int x){return x&(-x);}
void add(int x,int w){while(x<=n)c1[x]=(c1[x]+w)%MOD,++c2[x],x+=lb(x);}
int getsum1(int x){int ret=0;while(x)ret=(ret+c1[x])%MOD,x-=lb(x);return ret;}
int getsum2(int x){int ret=0;while(x)ret+=c2[x],x-=lb(x);return ret;}
int D[MAX],invD[MAX],zero[MAX],S[MAX];
int main()
{
    n=read();tot=1;
    for(int i=1;i<=n;++i)++cnt[a[i]=read()];
    for(int i=n-1;i;--i)cnt[i]+=cnt[i+1];
    for(int i=1;i<=n;++i)tot=1ll*tot*(cnt[i]-=(n-i))%MOD;
    if(!tot){puts("0");return 0;}
    S[0]=D[0]=1;
    for(int i=1;i<=n;++i)
    {
        int x=1ll*(cnt[i]-1)*fpow(cnt[i],MOD-2)%MOD;
        if(!x)S[zero[i]=zero[i-1]+1]=i,D[i]=D[i-1];
        else zero[i]=zero[i-1],D[i]=1ll*D[i-1]*x%MOD;
        invD[i]=fpow(D[i],MOD-2);
    }
    for(int i=1;i<=n;++i)
    {
        ans=(ans+1ll*(getsum1(a[i])-getsum1(S[zero[a[i]]]-1)+MOD)*D[a[i]]%MOD*tot%MOD*inv2%MOD)%MOD;
        add(a[i],invD[a[i]]);
    }
    for(int i=1;i<=n;++i)c1[i]=c2[i]=0;
    for(int i=n;i;--i)
    {
        ans-=1ll*(getsum1(a[i]-1)-getsum1(S[zero[a[i]]]-1)+MOD)*D[a[i]]%MOD*tot%MOD*inv2%MOD;
        ans=(ans+1ll*getsum2(a[i]-1)*tot)%MOD;ans=(ans+MOD)%MOD;
        add(a[i],invD[a[i]]);
    }
    printf("%d\n",ans);
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值