- 已知一个排列进行冒泡排序需要交换次数的下界为\(\frac12\sum_{i=1}^n|i-p_i|\)。
- 定义一个冒泡排序次数能达到下界的排列为好的排列。
- 给定一个长度为\(n\)的排列,求字典序严格大于该排列的好的排列个数。
- \(n\le6\times10^5,\sum n\le2\times10^6\)
好排列的充要条件
要达到下界说明不会出现无用的交换。
而要出现无用的交换,当且仅当有一个数左边存在比它大的数且右边存在比它小的数。
因为如果这样,左边的数移到右边和右边的数移到左边的过程中,这个数的移动就会相互抵消。
这个条件的另一种表述就是不存在长度大于等于\(3\)的下降子序列,进一步也就等价于原序列可以拆分为两个上升子序列。
不考虑限制条件
考虑一次拆分的具体过程,方便起见把当前所有数中的最大值所在序列称为\(A\)序列,另一个则称为\(B\)序列。
由于所有数都需要一个归宿,因此在接下来的填写中,对于大于最大值的数我们仍然可以*填写加入\(A\)序列,而对于小于最大值的那些未填的数,我们必须把它们按照从小到大的唯一顺序加入\(B\)序列。
所以说,如果设\(f_{i,j}\)表示剩余\(i\)个数,其中有\(j\)个数大于最大值时的方案数,显然\(j\le i\)。
考虑转移,一种情况是选择一个更大的数加入\(A\)序列,得出转移:\(\sum_{k=0}^{j-1}f_{i-1,k}\)。
另一种情况是选择一个较小的数加入\(B\)序列,选法唯一,得出转移:\(f_{i-1,j}\)。
把两种转移结合起来得到:
\[f_{i,j}=\sum_{k=0}^jf_{i-1,k} \]发现这相当于是一个前缀和的形式,所以可以改写成:
\[f_{i,j}=f_{i,j-1}+f_{i-1,j} \]这是一个典型的坐标系上走路式转移,从\((0,0)\)一路走到\((i,j)\)的方案数应该是\(C_{i+j}^i\)。
但由于\(j\le i\)的限制还需要减去非法方案数,即考虑\((i,j)\)关于\(y=x-1\)的对称点\((j-1,i+1)\),走到那里的方案数为\(C_{i+j}^{j-1}\)。
因此联合起来就可以通过组合数来表示得到:
\[f_{i,j}=C_{i+j}^j-C_{i+j}^{j-1} \]字典序的限制
考虑枚举与给定序列的第一个不同位,那么只要枚举每一位先计算当前位比给定排列大的好排列个数,然后强制这一位和给定排列相同即可。
假设给定排列的当前位上的值是\(a_i\),由于之前的每一位都已经确定和给定的排列相同,所以我们可以利用树状数组轻松求出之前比\(a_i\)小的数的个数\(p\),然后计算出尚未填写的比\(a_i\)大的数的个数\(q\)。
首先我们发现,如果当前数成为了新的最大值,那么此时的\(q\)必然比之前的全部\(q\)都要更小,所以可以用一个变量\(t\)来维护\(q\)的最小值。
如果某一时刻\(t\)变成了\(0\),显然不可能构造出比给定序列字典序更大的排列了。
否则,考虑计算此时的方案数,由于要大于当前位置上的数,因此填入的不可能是剩余数中最小的数,无法加入\(B\)序列,只能从这\(t\)个数中选择一个加入\(A\)序列。
因此,类似于先前的\(DP\)转移,相当于是要填写剩下的这个长度为\(n-i\)的序列:\(\sum_{j=0}^{t-1}f_{n-i,j}\)。
根据先前得出的\(f\)数组为前缀和形式的结论,这玩意就等于\(f_{n-i+1,t-1}\),可以直接利用先前推出的组合数公式计算。
最后我们考虑计算完答案后强制这一位选\(a_i\)的过程,如果\(a_i\)是新的最大值那么直接加入\(A\)序列即可;否则因为\(B\)序列需要满足尚未填写的数从小到大被加入,因此比\(a_i\)小的数必须全部存在,即\(p=a_i-1\)。如果不合法,直接终止枚举即可。
代码:\(O(nlogn)\)
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 600000
#define X 998244353
#define C(x,y) (1LL*Fac[x]*IFac[y]%X*IFac[(x)-(y)]%X)
using namespace std;
int n,a[N+5],Fac[2*N+5],IFac[2*N+5];
I int QP(RI x,RI y) {RI t=1;W(y) y&1&&(t=1LL*t*x%X),x=1LL*x*x%X,y>>=1;return t;}
I void InitFac()//预处理阶乘和阶乘逆元
{
RI i;for(Fac[0]=i=1;i<=2*N;++i) Fac[i]=1LL*Fac[i-1]*i%X;
for(IFac[i=2*N]=QP(Fac[2*N],X-2);i;--i) IFac[i-1]=1LL*IFac[i]*i%X;
}
namespace FastIO
{
#define FS 100000
#define tc() (FA==FB&&(FB=(FA=FI)+fread(FI,1,FS,stdin),FA==FB)?EOF:*FA++)
#define pc(c) (FC==FE&&(clear(),0),*FC++=c)
int OT;char oc,FI[FS],FO[FS],OS[FS],*FA=FI,*FB=FI,*FC=FO,*FE=FO+FS;
I void clear() {fwrite(FO,1,FC-FO,stdout),FC=FO;}
Tp I void read(Ty& x) {x=0;W(!isdigit(oc=tc()));W(x=(x<<3)+(x<<1)+(oc&15),isdigit(oc=tc()));}
Ts I void read(Ty& x,Ar&... y) {read(x),read(y...);}
Tp I void writeln(Ty x) {W(OS[++OT]=x%10+48,x/=10);W(OT) pc(OS[OT--]);pc('\n');}
}using namespace FastIO;
struct TreeArray
{
int a[N+5];I void Cl() {for(RI i=1;i<=n;++i) a[i]=0;}//清空
I void U(RI x) {W(x<=n) ++a[x],x+=x&-x;}I int Q(RI x,RI t=0) {W(x) t+=a[x],x-=x&-x;return t;}//单点修改;前缀查询
}T;
I int f(CI i,CI j) {return (C(i+j-1,j)-(j>=2?C(i+j-1,j-2):0)+X)%X;}
int main()
{
RI Tt,i,t,p,q,g,ans;read(Tt),InitFac();W(Tt--)
{
for(read(n),i=1;i<=n;++i) read(a[i]);
for(T.Cl(),t=n,ans=0,i=1;i<=n;T.U(a[i++]))//枚举与给定序列第一个不同位
{
if(p=T.Q(a[i]),q=n-a[i]-(i-1-p),q<t?(t=q,g=1):g=0,!t) break;//如果t=0,说明无法构造更大的排列
if(ans=(ans+f(n-i+1,t-1))%X,!g&&p^(a[i]-1)) break;//统计答案;如果需要加入B序列但更小的数未全被加入则结束枚举
}printf("%d\n",ans);
}return clear(),0;
}