1. 程式人生 > 實用技巧 >[洛谷P4769] NOI2018 氣泡排序

[洛谷P4769] NOI2018 氣泡排序

問題描述

最近,小 S 對氣泡排序產生了濃厚的興趣。為了問題簡單,小 S 只研究對 1 到 n 的排列的氣泡排序。

下面是對氣泡排序的演算法描述。

輸入:一個長度為 n 的排列 p[1...n]
輸出:p 排序後的結果。
for i = 1 to n do
	for j = 1 to n - 1 do
		if(p[j] > p[j + 1])
			交換 p[j] 與 p[j + 1] 的值

氣泡排序的交換次數被定義為交換過程的執行次數。可以證明交換次數的一個下界是 \(\frac{1}{2}\sum_{i=1}^n|i-p_i|\),其中 \(p_i\) 是排列 \(p\) 中第 \(i\)

個位置的數字。如果你對證明感興趣,可以看提示。

小 S 開始專注於研究長度為 \(n\) 的排列中,滿足交換次數 = \(\frac{1}{2}\sum_{i=1}^n|i-p_i|\) 的排列(在後文中,為了方便,我們把所有這樣的排列叫「好」的排列)。他進一步想,這樣的排列到底多不多?它們分佈的密不密集?

小 S 想要對於一個給定的長度為 \(n\) 的排列 \(q\),計算字典序嚴格大於 \(q\) 的“好”的排列個數。但是他不會做,於是求助於你,希望你幫他解決這個問題,考慮到答案可能會很大,因此只需輸出答案對 998244353 取模的結果。

輸入格式

輸入第一行包含一個正整數 \(T\)

,表示資料組數。

對於每組資料,第一行有一個正整數 \(n\),保證 \(n \leq 6 \times 10^5\)

接下來一行會輸入 \(n\) 個正整數,對應於題目描述中的 \(q_i\),保證輸入的是一個 \(1\)\(n\) 的排列。

輸出格式

輸出共 \(T\) 行,每行一個整數。

對於每組資料,輸出一個整數,表示字典序嚴格大於 \(q\) 的「好」的排列個數對 998244353 取模的結果。

樣例輸入

1
3
1 3 2

樣例輸出

3

解析

我們先考慮什麼樣的排列滿足交換次數等於下界。由下界的證明過程,滿足要求的排列在交換時是不會走重複的路程的(具體可以體會一下1 4 3 2,3需要先和左邊比他大的元素交換,再和右邊比他小的元素交換)。所以,如果排列中不存在長度為3及以上的下降子序列時,該排列滿足要求。

假設沒有字典序的要求,我們考慮計算滿足要求的排列個數。設 \(f_{i,j}\) 表示長度為 \(i\) 、第一個元素為 \(j\) 的滿足條件的排列個數。轉移分三種情況:

  • 第二個元素 \(k\)\(j\) 大。如果 \(k\) 不能和後面組成長度為 3 以上的下降序列,那麼 \(j\) 肯定也不能。因此,我們把從第二個開始的每一個比 \(j\) 大的元素都減一,把後面當做一個排列,滿足要求的方案數就是 \(f_{i-1,k}\)
  • 第二個元素 \(k\)\(j\) 小且不等於 1 。那麼排列中一定存在 \(k,j,1\) 的下降子序列。方案數為0。
  • 第二個元素等於 1 。那麼 \([1,j-1]\) 的元素必須遞增排列(位置上不一定是連續的)。但方案數顯然不是 \(f_{i-1,1}\) 。我們需要轉化一下,不妨將 1 移動到 2 , 2 移動到 3 ,……,j-1 移動到 1,那麼序列就變成了 j-1 開頭。可以證明,\(f_{i-1,j}\)等價於我們要求的方案數。

綜上所述,我們有如下轉移方程:

\[f_{i,j}=\sum_{k=j-1}^{i-1} f_{i-1,k} \]

接下來考慮字典序的限制。我們可以列舉公共字首的長度來解決這個問題,那麼我們可以把後面離散化後當做一個長度為 \(n-i+1\) 的排列,而第一位要大於原排列的對應位置。設當前在第 \(i\) 位,\([i,n]\) 中有 \(j\) 個數比 \([1,i]\) 中的最大值小(或等於)。第 \(i\) 位上不能放字尾最小值,這會使字典序達不到要求;同樣,也不能放比字首最大值小的值,這樣一定會有長度為3的下降子序列。如果原排列中 \(i\) 上就是字首最大值,構造的排列中第 \(i\) 位上由於字典序的限制也不能放字首最大值。所以,我們有:

\[Ans=\sum_{i=1}^{n-1}\sum_{k=j+1}^{n-i+1}f_{n-i+1,k} \]

這顯然就是一個關於 \(f_i\) 的字尾和。我們接下來考慮如何 \(O(1)\) 求出 \(f_I\) 的字尾和。記字尾和為 \(S_{i,j}\),由轉移方程,我們不難發現:

\[S_{i,j}=S_{i-1,j-1}+S_{i,j+1} \]

考慮組合意義,求 \(S_{n,m}\) 相當於從左上角 \((0,0)\) 出發,每次能夠往右下或左走,求到達右下角 $(n,m) $ 的方案數。先考慮到 \((n,0)\),每次向右下走之後都必須有一個向左走的操作與之對應,才能夠回到第 \(0\) 列。這相當於括號匹配,答案就是卡特蘭數。

\[S_{n,0}=C_n=C_{2u}^n-C_{2n}^{n-1} \]

推廣到所有,我們有:

\[S_{n,m}=C_{2n-m}^{n-m}-C_{2n-m}^{n-m-1} \]

證明的話可以利用不能走到 \(y=-1\) 這條直線上來的性質。注意列舉字首時還要判斷字首是否合法,記錄一下字首最大值之後的次大值即可。

題解比程式碼長

程式碼

#include <iostream>
#include <cstdio>
#include <cstring>
#define int long long
#define N 600002
#define M 1200000
using namespace std;
const int mod=998244353;
int t,n,i,p[N],maxx[N],minx[N],cnt[N],c[N],fac[2*N],inv[2*N];
int read()
{
	char c=getchar();
	int w=0;
	while(c<'0'||c>'9') c=getchar();
	while(c<='9'&&c>='0'){
		w=w*10+c-'0';
		c=getchar();
	}
	return w;
}
int poww(int a,int b)
{
	int ans=1,base=a;
	while(b){
		if(b&1) ans=ans*base%mod;
		base=base*base%mod;
		b>>=1;
	}
	return ans;
}
int lowbit(int x)
{
	return x&(-x);
}
void add(int x,int y)
{
	for(int i=x;i<=n;i+=lowbit(i)) c[i]+=y;
}
int ask(int x)
{
	int ans=0;
	for(int i=x;i>=1;i-=lowbit(i)) ans+=c[i];
	return ans;
}
int C(int n,int m)
{
	return fac[n]*inv[m]%mod*inv[n-m]%mod;
}
int S(int n,int m)
{
	return (C(2*n-m,n-m)-C(2*n-m,n-m-1)+mod)%mod;
}
signed main()
{
	t=read();
	for(i=fac[0]=1;i<=M;i++) fac[i]=fac[i-1]*i%mod;
	inv[M]=poww(fac[M],mod-2);
	for(i=M-1;i>=0;i--) inv[i]=inv[i+1]*(i+1)%mod;
	while(t--){
		n=read();
		memset(c,0,sizeof(c));
		for(i=1;i<=n;i++) p[i]=read();
		for(i=1;i<=n;i++) maxx[i]=max(maxx[i-1],p[i]);
		for(i=n,minx[n+1]=1<<30;i>=1;i--) minx[i]=min(minx[i+1],p[i]);
		for(i=n;i>=1;i--){
			add(p[i],1);
			cnt[i]=ask(maxx[i]);
		}
		int ans=0,max1=0;
		for(i=1;i<=n;i++){
			if(max1>minx[i]) break;
			if(cnt[i]+1<=n-i+1) ans=(ans+S(n-i+1,cnt[i]+1))%mod;
			if(p[i]<maxx[i]) max1=max(max1,p[i]);
		}
		printf("%lld\n",ans);
	}
	return 0;
}