[NOI2018] 冒泡排序
一、题目
二、解法
因为 \(\frac{1}{2}\sum_{i=1}^n|i-p_i|=\sum_{i=1}^n\max(i-p_i,0)\),我们可以考虑后者。
设 \(d_i\) 表示位置 \(i\) 的逆序对数,显然有 \(d_i\geq \max(i-p_i,0)\),因为如果 \(p_i\leq i\),那么前面比 \(p_i\) 大的数至少有 \(i-p_i\) 个。
这说明必须每个位置都要满足 \(d_i=\max(i-p_i,0)\) 才是好排列,这等价于对于所有位置 \(i\),要么 \(p_i\) 是前缀最大值,要么所有比 \(p_i\) 小的数都在 \([1,i)\) 中出现过了。
得到这个结论之后我们不考虑字典序限制,设 \(dp[i][j]\) 表示填了前 \(i\) 个位置最大值是 \(j\),那么转移就是填上一个更大的值,或者是填上前面所有数的 \(\tt mex\),所以转移的形式其实很简洁:
此外还需要保证 \(i\leq j\),我们可以把它改写成前缀和的形式:
考虑转移的组合意义,这相当于在二维平面上行走,从 \((0,0)\) 开始,每次可以向上走或者向右走,要求不越过 \(y=x\),最后走到 \((n,n)\) 的方案数。显然这是卡特兰数,表达式是 \({2n\choose n}-{2n\choose n+1}\)
显然考虑字典序限制,显然的方法是保证前缀相同,下一个位置 \(=p_i+1\),后面位置随意填的排列数。
所以我们在保证前缀 \(i-1\) 合法的情况下,从 \((i-1,\max_{j=1}^ip_j+1)\) 开始行走到 \((n,n)\) 的方案数。我们已知从 \((x,y)\) 行走到 \((n,n)\) 的方案数是 \({2n-x-y\choose n-x}-{2n-x-y\choose n-x+1}\)
#include <cstdio>
#include <iostream>
using namespace std;
const int M = 2000005;
#define int long long
const int MOD = 998244353;
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int T,n,m,ans,fac[M],inv[M],b[M];
void init(int n)
{
fac[0]=inv[0]=inv[1]=1;
for(int i=1;i<=n;i++) fac[i]=fac[i-1]*i%MOD;
for(int i=2;i<=n;i++) inv[i]=inv[MOD%i]*(MOD-MOD/i)%MOD;
for(int i=2;i<=n;i++) inv[i]=inv[i]*inv[i-1]%MOD;
}
int C(int n,int m)
{
if(n<m || m<0) return 0;
return fac[n]*inv[m]%MOD*inv[n-m]%MOD;
}
int walk(int x,int y,int n,int m)
{
if(x>n || y>m || x<0 || y<0) return 0;
return C(n+n-x-y,n-x)-C(n+n-x-y,n-x+1);
}
void add(int x)
{
for(int i=x;i<=n;i+=(i&-i)) b[i]++;
}
int ask(int x)
{
int r=0;
for(int i=x;i>0;i-=(i&-i)) r+=b[i];
return r;
}
void work()
{
n=read();ans=0;
for(int i=1;i<=n;i++) b[i]=0;
for(int i=1,mx=0,fl=1;i<=n;i++)
{
int x=read();mx=max(mx,x);
if(fl) ans=(ans+walk(i-1,mx+1,n,n))%MOD;
add(x);
if(x!=mx && ask(x)<x) fl=0;
}
printf("%lld\n",(ans+MOD)%MOD);
}
signed main()
{
T=read();init(2e6);
while(T--) work();
}