好久没有做一道这么对胃口的题了……
大致题意:给出一个1..n的全排列,但是其中有些数字被替换成了-1。我们设给出数列中-1的个数为x,那么原来的全排列就有x!种,而且告诉你取这x!种每一种的概率相等,现在问你原排列的逆序对的期望是多少。
很显然,这个数列分为两个部分,一个部分是已经给出的正整数,另一部分是-1。那么相应的,逆序对可以分为三个部分,一个是正整数部分内部逆序对,一个是-1部分内部逆序对,最后是两个之间的逆序对。而这个二者之间的逆序对又可以分为,-1部分在前和正整数部分在前两种。
对于第一部分来说,相当于已经给出了所有数字直接去就一遍逆序对,这个是裸的求逆序对,各种NlogN的方法xjb搞即可,这里我用了树状数组。现在考虑第二部分,这x个-1内部的逆序对。这x个数字可能是离散的,但是逆序对只考虑大小关系而不考虑差值,所以可以看作这x个数字是1..x。那么问题就变成了1..x的所有排列,期望的逆序对数是多少。我们考虑dp[i]表示i个数字的期望逆序对数。那么显然有转移方程:
进一步我们可以推出通项公式:
所以第二部分完成。现在考虑第三部分,我们首先考虑正整数部分在前,也即只需考虑每一个正整数对其后面的-1产生的贡献。对于在位置i的正整数ai,它会对i之后的所有-1的位置产生影响。对于i之后的每一个-1的位置,产生的影响是所有未出现的正整数中小于ai的数字个数。我们设i之后为-1的位置的个数为pos,同时未出现过的比ai小的数字个数为t,那么这个ai产生的贡献就是:
如此统计所有正整数ai的贡献和即可完成这一部分。
对于-1在前的部分,直接做似乎不太好算。我们考虑仿照正整数在前的做法,还是以正整数为参考,但是却是反过来做。具体来说,对于在位置i的正整数ai,计算它与i之前所有-1的位置产生的贡献。与上面类似,对于每个ai都计算贡献,贡献和就是这一部分的答案。
最后把这三个部分,准确的说四个部分的答案加到一起就是我们要的结果。
至于复杂度的话,主要在第一部分求逆序对的NlogN和第三部分统计每个贡献的时候,需要logN确定未出现的正整数中比某个值大或者小的数字个数。因此总的复杂度为O(NlogN)。具体见代码:
#include <bits/stdc++.h>
#define LL long long
using namespace std;
const int N = 200010;
const int mod = 998244353;
const int inv2 = (mod + 1) / 2;
int c[N],a[N],s[N],n,m;
LL ans,inv;
inline void update(int x,int y)
{
for(int i=x;i<N;i+=i&-i)
c[i]+=y;
}
inline int getsum(int x)
{
int res=0;
for(int i=x;i;i-=i&-i)
res+=c[i];
return res;
}
inline int qpow(int x,int n)
{
int res=1;
while(n)
{
if (n&1) res=(LL)res*x%mod;
x=(LL)x*x%mod; n>>=1;
}
return res;
}
int main()
{
ans=m=0;
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
s[i]=s[i-1];
scanf("%d",&a[i]);
if (a[i]==-1) m++;
else
{
ans+=i-1-m-getsum(a[i]);
update(a[i],1); s[i]++;
}
}
ans%=mod; inv=qpow(m,mod-2);
ans=(ans+(LL)m*(m-1)/2%mod*inv2%mod)%mod;
for(int i=1;i<=n;i++)
{
if (a[i]==-1) continue;
ans=(ans+(LL)(n-i-s[n]+s[i])*(a[i]-1-getsum(a[i]-1))%mod*inv%mod)%mod;
ans=(ans+(LL)(i-1-s[i-1])*(n-a[i]-getsum(n)+getsum(a[i]))%mod*inv%mod)%mod;
}
printf("%lld\n",ans);
return 0;
}