【BZOJ5416】【NOI2018】冒泡排序(动态规划)

时间:2023-03-08 17:31:18
【BZOJ5416】【NOI2018】冒泡排序(动态规划)

【BZOJ5416】【NOI2018】冒泡排序(动态规划)

题面

BZOJ

洛谷

UOJ

题解

考场推出了就是两个上升子序列,并且最长下降子序列长度不超过\(2\)。。。然后大力暴力状压\(dp\)混了\(44\)分。。。这个结论并不是很难证明,考虑一下冒泡排序的过程就好了。

实际上\(O(n^2)\)并不是很难吧。。。

先不考虑字典序的问题,设\(f[i][j]\)表示长度为\(i\)的\([1,i]\)的排列,并且第一个数为\(j\)的合法排列方案数。

考虑如何转移,如果\(j=1\),那么后面怎么放都行,所以$\displaystyle f[i][1]=\sum_{j=1}^{i-1}f[i-1][j] $。注意一下后面的数所谓的排列我们理解为抛掉第一个数之后,剩下的位置重新离散后的值。

否则不以\(1\)开头,如果下一个数比\(j\)大,那么是没有问题的,这部分就是\(\displaystyle f[i][j]\rightarrow \sum_{k=j}^{i-1}f[i-1][k]\)。

那么如果下一个数比\(j\)小,如果下个数不是\(1\)的话,显然当前\(j\)、下一个数、\(1\),就构成了一个不合法的状态。

实际上想想就是,所有小于当前数的数,也就是\([1,j-1]\)在排列中一定是相对有序的,即强制顺序排列。

那么这样子的方案数是什么呢?\(f[i-1][j-1]\)。

为啥呢?

如果\(j=2\),显然后面接一个\(1\)是没有任何问题的。

否则的话,我们把接在后面的这样子一个合法的排列给弄出来,然后把\(j\)接在开头,显然会出现\(j,j-1,1\)这个不合法的东西,那么我们做个转换,强制令\(j-1\)变成\(1\),然后剩下的\([1,j-2]\)这些数全部加一,不难发现这样子\([1,j-1]\)这些数就被强制顺序排列了。

这样子我们就有转移了:\(\displaystyle f[i][j]=\begin{cases}\sum_{k=1}^{i-1}f[i-1][k]&j=1\\\sum_{k=j-1}^{i-1}f[i-1][k]&j>1\end{cases}\)

好的,我们显然可以\(O(n^2)\)的算出\(f\)数组,考虑怎么计算答案了。

显然是前面一部分卡紧范围,然后从某个特定的位置开始,大于了给定的字典序,然后后面就可以放飞自我了。

假装我们紧紧卡住了\([1,i-1]\)这些位置,然后从\(i\)位置开始放飞自我。

假设有在\(q[i..n]\)中有\(k\)个数要小于\(q[1..i]\)中的前缀最大值。

这里分情况讨论贡献,并且以下讨论都是在前缀合法的情况下进行的。判断当前是否合法只需要维护前缀最大值、前缀次大值以及后缀最小值,实时检查是否合法即可。

显然后面要填进去的数中,大于当前前缀最大值的数是没有任何影响的,而小于前缀最小值的数则强制从小往大填。怎么强制从小往大填呢?显然任何一个以大于前缀最大值开头的填法中,这一段都被强制按照顺序填,显然开头比前缀最大值要大的时候,一定被强制按照顺序填了,所以这一部分的贡献是\(\displaystyle \sum_{j=k+1}^{n-i+1}f[n-i+1][j]\)。

然而发现似乎并没有以没有以比前缀最大值小的数中的最小值为开头的贡献。

的确没有考虑,但是因为这里要限制字典序,所以当前这里填的数必须比前缀最大值大后面才不会受到字典序的限制,所以这里就没有这一部分贡献了。。。

这样子就可以做到\(O(Tn^2)\)的复杂度。

复杂度的瓶颈在于求解\(f\)数组以及其后缀和。

显然不能在通过求解\(f\)再求解其后缀和了,这样子效率太差。

设\(\displaystyle s[n][m]=\sum_{i=m}^n f[n][i]\),即\(f\)的后缀和。

那么有:

\[\begin{aligned}
s[n][m]&=\sum_{i=m}^n f[n][i]\\
&=\sum_{i=m}^n \sum_{j=i-1}^{n-1} f[n-1][j]\\
&=\sum_{j=m-1}^{n-1} f[n-1][j]\sum_{i=m}^{j+1} 1\\
&=\sum_{j=m-1}^{n-1} f[n-1][j]+\sum_{j=m-1}^{n-1} f[n-1][j]\sum_{i=m+1}^{j+1} 1\\
&=s[n-1][m-1]+\sum_{i=m+1}^{n}\sum_{j=i-1}^{n-1}f[n-1][j]\\
&=s[n-1][m-1]+s[n][m+1]
\end{aligned}\]

明摆着找组合意义,走\(n\)步,每次可以向上走一步或者向下走一步,最终停在\(m\)位置,且中途不能走到\(0\)下面去的方案数。

行,那不就是这题??

那么推出来就是\(\displaystyle s[n][m]={2n-m\choose n-m}-{2n-m\choose n-m-1}\)

然后???

然后就做完了啊。。

#include<iostream>
#include<cstdio>
using namespace std;
#define MAX 1200001
#define MOD 998244353
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 jc[MAX],jv[MAX],inv[MAX];
int n,q[MAX],mn[MAX],ans;
int c[MAX];
int lb(int x){return x&(-x);}
void add(int x,int w){while(x<=n)c[x]+=w,x+=lb(x);}
int getsum(int x){int s=0;while(x)s+=c[x],x-=lb(x);return s;}
int C(int n,int m){if(m>n)return 0;return 1ll*jc[n]*jv[m]%MOD*jv[n-m]%MOD;}
int S(int n,int m){if(m>n)return 0;return (C(n+n-m,n-m)+MOD-C(n+n-m,n-m-1))%MOD;}
int main()
{
jc[0]=jv[0]=inv[0]=inv[1]=1;
for(int i=1;i<MAX;++i)jc[i]=1ll*jc[i-1]*i%MOD;
for(int i=2;i<MAX;++i)inv[i]=1ll*inv[MOD%i]*(MOD-MOD/i)%MOD;
for(int i=1;i<MAX;++i)jv[i]=1ll*jv[i-1]*inv[i]%MOD;
int T=read();
while(T--)
{
n=read();ans=0;if(!n){puts("0");continue;}
for(int i=1;i<=n;++i)q[i]=read(),c[i]=0;
mn[n]=q[n];for(int i=n-1;i;--i)mn[i]=min(mn[i+1],q[i]);
for(int i=1;i<=n;++i)add(q[i],1);
for(int i=1,mx=0,smx=0;i<=n;++i)
{
if(smx>mn[i])break;
int k=getsum(max(mx,q[i]));add(q[i],-1);
ans=(ans+S(n-i+1,k+1))%MOD;
if(q[i]<mx)smx=max(smx,q[i]);mx=max(mx,q[i]);
}
printf("%d\n",ans);
}
}