1. 程式人生 > 其它 >P4769 [NOI2018] 氣泡排序

P4769 [NOI2018] 氣泡排序

閒話:之前在做這道題的時候猜了一個結論,最長上升子序列的個數不能超過二,當時證著證著覺得假了。。。


首先一個序列如果能滿足下界,那麼其中的每一個元素必然是隻向其目標方向移動。又由於,一個數的移動情況與其左邊比其大的數的個數和右邊比其小的數的個數決定,而且必然是先向左再向右,所以對於任意一個數,不能同時存在左邊有數比其大,右邊有數比其小。

上面這個性質,再轉換一下,等價於不存在長度大於等於三的下降子序列。於是啊,題目中奇怪的需要滿足氣泡排序下界的這個性質,就被我們轉化為了比較優美的一個性質了。

我們暫且先不考慮題目要求的字典序嚴格大於,即單純的求好排列的個數,這個我們顯然可以直接 \(\text{dp}\)

。我們先來考慮一波長度大於等於三的下降子序列的性質,不妨假設此時存在一個長度大於等於三的下降子序列。

  1. 如果這個子序列的第一個數已經被我們填入了,那麼其剩下部分不能出現長度大於等於二的下降子序列。
  2. 如果這個子序列的前兩個數已經被我們填入了,那麼其剩下部分不能存在比第二個數小的數。

所以說,我們便需要設計一種狀態,使得其滿足我們列出的兩個性質。不妨令 \(f_{i,j}\) 表示已經填入 \(i\) 個數,且其中最大的數是 \(j\) ,且滿足不存在沒填入的數比已經填入的數的長度為 \(2\) 的下降子序列的後一個數小。我們假設當前填入的數為 \(k\) ,那麼此時分兩種情況:

  1. \(k<j\)
    ,那麼我們便不會更新 \(j\) ,即 \(f_{i+1,j}\leftarrow f_{i,j}\) 。但是此時需要滿足剩下的數不存在比 \(k\) 小的,即 \(k\) 是沒填入的數中最小的。
  2. \(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;
}