P4769 [NOI2018] 冒泡排序
闲话:之前在做这道题的时候猜了一个结论,最长上升子序列的个数不能超过二,当时证着证着觉得假了。。。
首先一个序列如果能满足下界,那么其中的每一个元素必然是只向其目标方向移动。又由于,一个数的移动情况与其左边比其大的数的个数和右边比其小的数的个数决定,而且必然是先向左再向右,所以对于任意一个数,不能同时存在左边有数比其大,右边有数比其小。
上面这个性质,再转换一下,等价于不存在长度大于等于三的下降子序列。于是啊,题目中奇怪的需要满足冒泡排序下界的这个性质,就被我们转化为了比较优美的一个性质了。
我们暂且先不考虑题目要求的字典序严格大于,即单纯的求好排列的个数,这个我们显然可以直接 \(\text{dp}\) 。我们先来考虑一波长度大于等于三的下降子序列的性质,不妨假设此时存在一个长度大于等于三的下降子序列。
- 如果这个子序列的第一个数已经被我们填入了,那么其剩下部分不能出现长度大于等于二的下降子序列。
- 如果这个子序列的前两个数已经被我们填入了,那么其剩下部分不能存在比第二个数小的数。
所以说,我们便需要设计一种状态,使得其满足我们列出的两个性质。不妨令 \(f_{i,j}\) 表示已经填入 \(i\) 个数,且其中最大的数是 \(j\) ,且满足不存在没填入的数比已经填入的数的长度为 \(2\) 的下降子序列的后一个数小。我们假设当前填入的数为 \(k\) ,那么此时分两种情况:
- \(k<j\) ,那么我们便不会更新 \(j\) ,即 \(f_{i+1,j}\leftarrow f_{i,j}\) 。但是此时需要满足剩下的数不存在比 \(k\) 小的,即 \(k\) 是没填入的数中最小的。
- \(k>j\) ,那么我们便会更新 \(j\) ,即 \(f_{i+1,k}\leftarrow f_{i,j}\) 。
可以发现,两者的转移都是唯一的。但是,我们需要判断是否存在上面两种情况的转移,可以证明,除当 \(i=j\) 时第一种情况不能转移外,其余情况均能转移。
注:这种 \(\text{dp}\) 的设计方法还是很有启发性的,虽然说一般 \(\text{dp}\) 都是这么搞的,但是感觉条理分明地去分析我们需要 \(\text{dp}\) 的对象的性质,并将其加入我们的状态设计中,还是需要一定的锻炼的。
这个 \(\text{dp}\) 直接暴力做是 \(O(n^2)\) 的,不太行,但是手搓可以发现,这个转移等价于卡特兰数的转移,所以我们就得到了 \(O(n)\) 的解法了。
我们此时便再来考虑一下字典序的情况。字典序的比较实际上是只需要比较 \(\text{lcp}\) 的下一位的大小即可,所以我们就存在了一个想法,即在前 \(i-1\) 位填入和 \(q\) 一样的数,并在 \(i\) 填入大于 \(q_i\) 的数,我们便能得到大于其字典序的排列了。易发现,我们这个操作是很容易与我们前面的 \(\text{dp}\) 过程结合在一起的。
具体的,我们令 \(p_i=\max_{j=1}^{i}q_j\) ,那么对于在第 \(i\) 个位置能得到大小关系的排列,实际上就是在 \(f_{i-1,p_{i}+1}\) 的位置贡献了 \(1\) ,我们考虑快速计算这个位置的 \(1\) 对于 \(f_{n,n}\) 的贡献,还是利用类似卡特兰数的做法,我们令总方案减去跨过中轴的方案即可,即 \(\binom{2n-i-p_{i}}{n-i+1}-\binom{2n-i-p_{i}}{n-i+2}\) 。
应该就做完了?记得判断这个 \(q_i\) 的前缀是否合法,这里可以使用链表来判断每次塞入的数是否是最小的,这样总复杂度就是 \(O(n)\) 的。
#include<bits/stdc++.h>
using namespace std;
const int N=6e5+5;
const int MOD=998244353;
int ADD(int x,int y){return x+y>=MOD?x+y-MOD:x+y;}
int SUB(int x,int y){return x-y<0?x-y+MOD:x-y;}
int TIME(int x,int y){return (int)(1ll*x*y%MOD);}
int ksm(int x,int k=MOD-2){int res=1;for(;k;k>>=1,x=TIME(x,x))if(k&1)res=TIME(res,x);return res;}
int fact[N<<1],ifact[N<<1];
int C(int n,int m){
if(n<0||m<0||n-m<0) return 0;
return TIME(fact[n],TIME(ifact[m],ifact[n-m]));
}
int n,q[N],p[N],L[N],R[N],res;
void DEL(int x){
R[L[x]]=R[x],L[R[x]]=L[x];
}
int solve(){
cin>>n,R[0]=1,L[n+1]=n,res=0;
for(int i=1;i<=n;++i) L[i]=i-1,R[i]=i+1;
for(int i=1;i<=n;++i) scanf("%d",&q[i]);
for(int i=1;i<=n;++i) p[i]=max(p[i-1],q[i]);
for(int i=1;i<=n;++i){
res=ADD(res,SUB(C(2*n-i-p[i],n-i+1),C(2*n-i-p[i],n-i+2)));
if(q[i]<p[i]&&R[0]!=q[i]) break;else DEL(q[i]);
}
return printf("%d\n",res),0;
}
int main(){
fact[0]=1;
for(int i=1;i<(N<<1);++i) fact[i]=TIME(fact[i-1],i);
ifact[(N<<1)-1]=ksm(fact[(N<<1)-1]);
for(int i=(N<<1)-1;i>=1;--i) ifact[i-1]=TIME(ifact[i],i);
int T;cin>>T;while(T--) solve();
return 0;
}