题解 P4769 【[NOI2018]冒泡排序】

wu3412790

2019-03-19 18:51:42

Solution

不知道大家是怎么做的,退役已久的老博士硬推了3小时。 首先分析一个排列$P$什么时候达到最少交换次数,按照下界的证明,每一次交换都应该使得$\delta(P):=\sum_{i=1}^n |i-p_i|$减少2,否则就并不能达到下界。 这样一来,我们可以观察到,如果$p_i\leq i$,那么这个数从始至终向右移动。类似地,如果$p_i\geq i$,这个数只能向左移动。所以如果$p_i\leq i$,那么$p_i$之前的数应该都比它小,否则会迫使$p_i$向左移动。同样,如果$p_i\geq i$,那么$p_i$之后的数都应该比他大。 这可以总结为,不存在三个指标$i<j<k$满足$p_i>p_j>p_k$,否则考虑$p_j$若$p_j\geq j$则它后面的数$p_k$比它大,若$p_j\leq j$则它前面的数$p_i$比它小,都将导致矛盾。反之如果排列中不存在三个下降的数,那么任意一次交换,必然都使得$\delta(P)$减少2,这是因为,如果我们交换了$p_i$和$p_{i+1}$,那么必有$p_i>p_{i+1}$,这时由于不存在三个下降的数,应有$p_i$前的数都比它小,$p_{i+1}$后的数都比它大。所以$i<p_i$且,$p_{i+1}<i+1$,这表明这次交换使得$\delta(P)$减少了2。从而每一次交换都会使$\delta(P)$减少2,故交换次数达到了下界。 这样一来,我们面对的将是一个纯粹的组合问题,问字典序大于某个排列的排列中,有多少排列不包含长度为3的下降子序列,下文中将称这个性质为"漂亮的“。字典序的处理比较常规,只需要枚举从哪一位开始第一次大于给定串就可以,这样我们就需要处理如下子问题: 固定前缀的排列中,有多少个是“漂亮的”? 首先应判断前缀中是否有长度$\geq 3$的下降子序列,如果有,方案数为0,还需判断一下,剩下数中的最小者是否能和前缀中的两个数组成下降子序列,这些都可以用简单的前缀数组解决,属于基本功。 接下来,如果上述情况未发生,注意到前缀对我们的影响将只剩下前缀中的最大数,因为我们已经判断前缀中取两数或三数,无法组成下降子序列。所以问题进一步简化为,在$m$个数的排列中,如果第一个数是$x$,有多少这样的排列不包含长度为3的下降子序列?这个问题递归性明显,我们尝试建立递推式。以下记上述子问题的方案数为$f(x,m)$。 当$x=m$时,$\{1,2,...,m-1\}$都在$m$后面,所以他们必然是有序的,这样只有$1$方案,故$f(m,m)=1$。 当$x=1,2$时,后面的$m-1$个数不可能和$x$形成长度为3的下降子序列,故后面$m-1$的排列方案为$\sum_{i=1}^{n-1} f(i,n-1)$种,即长度为$n-1$的不包含长度为$3$的下降子序列的排列总数。 当$3\leq x\leq n-1$时,考虑$x$后第一个大于x的数,设它的位置为$i\in \{2,...,x+1\}$,大小是$j\in \{x+1,...,m\}$。注意到$\{1,2,...,x-1\}$在$x$之后,故他们在排列中必然有序,这表明第$2$到第$i-1$个数必然是$1$到$i-2$,这也说明了为什么i的取值范围是$2$到$x+1$。这样一来,前$i$个数对后面的影响,可以简化为一个$j$,这样就把方案数转化到子问题$f(j-i+1,m-i+1)$上。所以我们得到递推式: $ f(x,m)=\sum_{2\leq i\leq x+1\leq j \leq m}f(j-i+1,m-i+1) $ ,直接按照这个公式递推,复杂度较高,期望得分60分左右。 我们注意到$f$的相邻项之间存在大量重复,可将递推式简化为, $ f(x,m)=f(x-1,m-1)+f(x+1,m) $ 按照这个公式递推,可以得到80分。 想要得到满分,我们还需要进一步求出$f$的通项。观察上式,我们容易联想到组合数的递推关系, $ C(n,m)=C(n-1,m-1)+C(n-1,m) $ 于是我们试图将递推式往这个方向变形。令$g(x,m)=f(m-x,m)$,则我们可以得到$g(x,m)=g(x-1,m)+g(x,m-1)$, 其中边界条件为$g(0,i)=1$以及$g(j,i)=0,j\geq i$。至此,递推式已经完全拥有的组合数的组合解释,考虑一个$n\times m$的方格,要从点$(0,1)$走到点$(x,m)$,每次只能向右或向下行走,且不能碰到对角线(即横坐标永远小于纵坐标,对应上面的边界条件 $g(j,i)=0,j\geq i$),问有几种方案。这个模型相当的经典,他就是我们的老朋友"卡塔兰数",当然这里是广义的:有$x$个$-1$和$m$个$+ 1$,$m>x$,问有多少序列的任意前缀和都大于0。这可以用我们熟悉的翻转第一个碰到对角线的点的前缀的方法,或者构造满足边界条件的公式并使用数学归纳法证明,无论使用哪种方案,我们都可以得到 $ g(a,b)=C(a,a+b-1)-C(a-1,a+b-1) $ 验证一下·,带入$a=b-1=n$ (注意我们是从(0,1)出发),我们就得到了常规的卡塔兰数的公式$C_n=C(n,2n)-C(n-1,2n)$。 在推出上述公式以后,代码方面难度极低,只需要处理好一些必备的前缀数组,会算组合数模$P$就行,这都在提高组的要求以内,想必对NOI选手是小菜一碟。 ```cpp #include <iostream> #include <cstring> using namespace std; long long const N=1e6+3e5,P=998244353; int n,t,p[N],r[N],sr[N],pr[N]; long long ans,fac[N],inv[N]; long long power(long long x, long long k){ k=(k+P-1) % (P-1); long long ans=1; for (long long i=1;i<=k;i*=2,x=(x*x) % P) if (k & i) ans=(ans*x) % P; return ans; } long long c(long long a, long long b){ if (a<0) return 0; return fac[b]*inv[a] % P * inv[b-a] % P; } long long g(long long a, long long b){ return (c(a,a+b-1)-c(a-1,a+b-1)+P) % P; } int main(){ ios::sync_with_stdio(false); fac[0]=inv[0]=1; for (int i=1;i<N;i++){ fac[i]=fac[i-1]*i % P; inv[i]=power(fac[i],P-2); } cin>>t; while (t--){ cin>>n; ans=0; for (int i=1;i<=n;i++){ cin>>p[i]; r[i]=max(r[i-1],p[i]); if (r[i-1]>p[i]) pr[i]=max(pr[i-1],p[i]); else pr[i]=pr[i-1]; } sr[n]=p[n]; for (int i=n-1;i>=1;i--) sr[i]=min(sr[i+1],p[i]); for (int i=1;i<=n;i++){ if (r[i]<n) ans=(ans+g(n-r[i]-1,n-i+2)) % P; if (pr[i-1]>p[i]) break; if (i<n && p[i]<r[i-1] && (p[i]>sr[i+1])) break; } cout<<ans<<endl; } return 0; } ```