[洛谷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;
}