P4769-[NOI2018]冒泡排序【组合数学,树状数组】
正题
题目链接:https://www.luogu.com.cn/problem/P4769
题目大意
有一个冒泡排序的算法
输入:一个长度为 n 的排列 p[1...n]
输出:p 排序后的结果。
for i = 1 to n do
for j = 1 to n - 1 do
if(p[j] > p[j + 1])
交换 p[j] 与 p[j + 1] 的值
然后给出一个排列\(a\),求在所有字典序大于\(a\)的排列\(p\)中冒泡排序交换次数恰好为\(\sum_{i=1}^n|i-p_i|\)的排列数。
\(1\leq n\leq 6\times 10^5,\sum n\leq 2\times 10^6\)
解题思路
打一下表发现合法的排列条件是最长下降子序列不超过\(2\)。
然后我们先不考虑字典序限制条件怎么做,我们设\(f_{i,1/2}\)表示目前下降子序列长度为\(1/2\)中末尾最大的那个。
那么\(f_{i,1}\)就是目前出现的数中最大的,然后如果我们从前往后填数,那么如果\(\leq f_{i,2}\)的数中有没有填进去的,肯定不合法,所以\(f_{i,2}\)肯定比目前没有填进去的数中所有数字都小,不需要考虑。
设\(g_{i,j}\)表示目前还剩下\(i\)个数没填,其中\(f_{i,1}\)大于其中的\(j\)个数,那么有\(g_{i,j}\)可以转移到\(g_{i-1,j-1}\)(填在最底)和\(g_{i-1,k}(k\geq j)\)(填在\(j\)上面)。
我们考虑快速的求出每个\(g\),反过来就是\(g_{i,j}\)转移到\(g_{i+1,j+1}\)和\(g_{i+1,j}\)。
我们维护一个\(h_{i,j}=g_{i,i-j}\),那么每次的转移就是\(h_{i,j}\)转移到\(h_{i,k}(j\leq k\leq i)\)
这个转移很像卡特兰数的要求,每次可以往下或者往右,但是不能超过对角线。
这样来说移动到位置\((n,m)(m\leq n)\)的话方案数就是\(\binom{n+m}{m}-\binom{n+m}{m-1}\)。
然后就是枚举第一个超过该字典序的位置,这样前面的方案固定,剩下的数可以用树状数组计算得出,再用组合数求答案即可。
时间复杂度:\(O(n\log n)\)
code
#include<cstdio>
#include<cstring>
#include<algorithm>
#define ll long long
#define lowbit(x) (x&-x)
using namespace std;
const ll N=6e5*2,P=998244353;
ll T,n,t[N],a[N],fac[N],inv[N],ans;
ll C(ll n,ll m)
{if(m<0)return 0;return fac[n]*inv[m]%P*inv[n-m]%P;}
void Change(ll x,ll val){
while(x<=n){
t[x]+=val;
x+=lowbit(x);
}
return;
}
ll Ask(ll x){
ll ans=0;
while(x){
ans+=t[x];
x-=lowbit(x);
}
return ans;
}
ll F(ll n,ll m){
m=n-1-m;if(!m)return 0;m--;
return (C(n+m,m)-C(n+m,m-1)+P)%P;
}
signed main()
{
// freopen("inverse3.in","r",stdin);
fac[0]=inv[0]=inv[1]=1;
for(ll i=2;i<N;i++)inv[i]=P-inv[P%i]*(P/i)%P;
for(ll i=1;i<N;i++)fac[i]=fac[i-1]*i%P,inv[i]=inv[i-1]*inv[i]%P;
scanf("%lld",&T);
while(T--){
scanf("%lld",&n);ans=0;
for(ll i=1;i<=n;i++)
scanf("%lld",&a[i]),Change(a[i],1);
for(ll i=1,mx=0;i<=n;i++){
Change(a[i],-1);mx=max(mx,a[i]);
(ans+=F(n-i+1,Ask(mx)))%=P;
if(a[i]<mx&&Ask(a[i]))
break;
}
for(int i=1;i<=n;i++)t[i]=0;
printf("%lld\n",ans);
}
return 0;
}