[NOI2018]冒泡排序
https://www.zybuluo.com/ysner/note/1261482
題面
戳我
- \(8pts\ n\leq9\)
- \(44pts\ n\leq18\)
- \(ex12pts\ q_i=i\)
- \(80pts\ n\leq1000\)
\(100pts\ n\leq6*10^5\)
解析
\(8pts\)算法
\(O(n!n^2)\)模擬即可。
當然如果忘了答案清\(0\)的話。。。。\(44pts\)算法
手玩下樣例,可以感受到:當交換次數達到下限時,每個數到自己位置的過程中不需要折返。
這就需要保證,一個數被交換過一遍後,不能被反方向交換。
這種情況只有在前面有數比它大,後面有數比它小的的情況下才能實現。
想想冒泡排序就是交換逆序對。。。
可得結論:當交換次數達到下限時,序列中不存在長度超過\(2\)的下降子序列。
註意到\(n\leq18\),可以狀壓\(DP\)。
設\(f[S]\)表示在某種符合條件的排列中,已經填完的數(包括當前這一次)的集合為\(S\)。
然後顯然可得出這一次填數之前的集合\(g\)。
(模擬序列從前往後填數的過程)
於是要從\(f[g]\)轉移到\(f[S]\)。
填了前幾位數可從\(g\)中得出,設此為\(t\)。
肯定要枚舉當前這一位填了哪個數\(i\)。
如果\(i>t\),就說明後面一定有比它小的數,此時判掉前面填了比它大的數的情況。
如果\(i<t\)
如果\(i=t\),若前面有比它大的數,後面就一定有比它小的數,隨便選一種情況判掉即可。
答案有嚴格大於的限制?
加一維看當前填出的序列是否比原來序列的字典序大,限制下轉移即可。
代碼整合在下一檔部分分的代碼裏。
\(56pts\)算法
這種限制,相當於使題目只給出一個\(n\)。
大概率可以打表。
\(0,1,1,4,13,41,131,428...\)
發現答案是第\(n\)個卡特蘭數\(-1\)。
卡特蘭數公式是\(\frac{C_{2n}^n}{n+1}\)。
#include<iostream> #include<cstdio> #include<cstdlib> #include<cmath> #include<cstring> #include<algorithm> #define ll long long #define re register #define il inline #define pf(a) ((ll)(a)*(a)) #define max(a,b) (((a)>(b))?(a):(b)) #define min(a,b) (((a)<(b))?(a):(b)) #define fp(i,a,b) for(re int i=a;i<=b;i++) #define fq(i,a,b) for(re int i=a;i>=b;i--) using namespace std; const int mod=998244353,N=2e6+100; int n,a[N],f[2][1<<20],tag,jc[N]; il ll gi() { re ll x=0,t=1; re char ch=getchar(); while(ch!=‘-‘&&(ch<‘0‘||ch>‘9‘)) ch=getchar(); if(ch==‘-‘) t=-1,ch=getchar(); while(ch>=‘0‘&&ch<=‘9‘) x=x*10+ch-48,ch=getchar(); return x*t; } il ll ksm(re ll S,re ll n) { re ll T=S;S=1; while(n) { if(n&1) S=S*T%mod; T=T*T%mod; n>>=1; } return S; } il void solve() { jc[0]=1;fp(i,1,n*2) jc[i]=1ll*jc[i-1]*i%mod; re ll ans=1ll*jc[n*2]*ksm(1ll*jc[n]*jc[n]%mod,mod-2)%mod*ksm(n+1,mod-2)%mod-1; if(ans<0) ans+=mod; printf("%lld\n",ans); } int main() { re int T=gi(); while(T--) { tag=1; n=gi(); fp(i,1,n) a[i]=gi(),tag&=(a[i]==i); if(tag) {solve();continue;} memset(f,0,sizeof(f)); f[1][0]=1; fp(S,1,(1<<n)-1) { re int t=0,p=S; while(p) p-=p&-p,++t; fp(i,1,n) { if(!(S&(1<<i-1))) continue; re int g=S^(1<<i-1); if(i>t&&g>((1<<i-1)-1)) continue; if(i<=t&&(g&((1<<i-1)-1))!=(1<<i-1)-1) continue; if(a[t]==i) (f[0][S]+=f[0][g])%=mod,(f[1][S]+=f[1][g])%=mod; else { (f[0][S]+=f[0][g])%=mod; if(a[t]<i) (f[0][S]+=f[1][g])%=mod; } } } printf("%d\n",f[0][(1<<n)-1]); } return 0; }
\(84pts\)算法
考慮二維狀態的\(DP\)。
發現存下每個數的使用狀態是沒有必要的。
實際上,我們每次能夠填入的數只有兩種:
- 比前面所有數都大。
- 是當前未使用的數的最小值。
易證其它情況一定不合法。
根據條件,\(DP\)狀態應該設為\(f[i][j]\)表示填了前\(i\)個數,沒填的數中有\(j\)個比前面的數大。
邊界當然是\(f[1][1]=1\)
則轉移就是
\[f[i][j]=\sum_{k=1}^{j-1} f[i-1][k]\]
復雜度似乎是\(O(n^3)\)的,前綴和優化一下:
\[f[i][j]=f[i-1][j]+f[i][j-1]\]
然後卡著字典序下界,從前往後統計每一位貢獻的答案就可以啦。
註意一下若第\(i\)個數(即\(q_i\))不符合條件需直接退出,停止統計答案。
代碼整合在下一檔部分分的代碼裏。
\(100pts\)算法
復雜度瓶頸在計算\(f[i][j]\)上,想辦法優化。
看著那個很勻稱的遞推式,是不是能想到什麽東西?
有點像在網格圖中,從\((1,1)\)出發,只能往右或往上走,中間坐標\((i,j)\)不能出現\(i<j\)的情況,到\((n,m)\)的方案數?
該限制條件相當於路徑不能越過(觸碰)\(y=x-1\)這條線。
這是個組合數學中的經典模型,
答案是,在只能往右或往上走的前提下,點\((1,1)\)到點\((n,m)\)的方案數\(-\)點\((0,2)\)到點\((n,m)\)的方案數(即用合法方案減去所有不合法方案)
則\[f[n][m]=C_{m+n-2}^{n-1}-C_{m+n-2}^{n}\]
最後可以通過記憶化和預處理一部分逆元來卡常。
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cmath>
#include<cstring>
#include<algorithm>
#define ll long long
#define re register
#define il inline
#define pf(a) ((ll)(a)*(a))
#define max(a,b) (((a)>(b))?(a):(b))
#define min(a,b) (((a)<(b))?(a):(b))
#define fp(i,a,b) for(re int i=a;i<=b;i++)
#define fq(i,a,b) for(re int i=a;i>=b;i--)
using namespace std;
const int mod=998244353,N=1e6+2e5+100,M=N;
int n,a[N],jc[N],inv[M],gu[N],tag,F[1010][1010];
bool vis[N];
il ll gi()
{
re ll x=0,t=1;
re char ch=getchar();
while(ch!=‘-‘&&(ch<‘0‘||ch>‘9‘)) ch=getchar();
if(ch==‘-‘) t=-1,ch=getchar();
while(ch>=‘0‘&&ch<=‘9‘) x=x*10+ch-48,ch=getchar();
return x*t;
}
il ll ksm(re int p)
{
if(jc[p]<=M-100) return inv[jc[p]];
if(~gu[p]) return gu[p];
re ll T=jc[p],S=1,n=mod-2;
while(n)
{
if(n&1) S=S*T%mod;
T=T*T%mod;
n>>=1;
}
return gu[p]=S;
}
il ll getF(re ll n,re ll m)
{
re int c1=jc[n+m-2];
re ll ans=(1ll*c1*ksm(n-1)%mod*ksm(m-1)%mod+mod-1ll*c1*ksm(n)%mod*ksm(m-2)%mod)%mod;
if(ans<0) ans+=mod;
return ans;
}
il void solve1()
{
fp(i,1,n) vis[i]=0;
if(!F[1][1])
{
F[1][1]=1;
fp(i,2,1001)
fp(j,1,i)
F[i][j]=(F[i][j-1]+F[i-1][j])%mod;
}
re int flag=1,mx=0,ans=0,p=1;
fq(o,n,1)
if(flag)
{
re int i=n-o+1;
(ans+=F[o+1][n-max(a[i],mx)])%=mod;
if(a[i]<mx&&a[i]>p) flag=0;
mx=max(mx,a[i]);
vis[a[i]]=1;
while(vis[p]) ++p;
}
printf("%d\n",ans);
}
il void solve2()
{
memset(vis,0,sizeof(vis));
re int flag=1,mx=0,ans=0,p=1;
fq(o,n,1)
if(flag)
{
re int i=n-o+1;
(ans+=getF(o+1,n-max(a[i],mx)))%=mod;
if(a[i]<mx&&a[i]>p) flag=0;
mx=max(mx,a[i]);
vis[a[i]]=1;
while(vis[p]) ++p;
}
printf("%d\n",ans);
}
il void Pre()
{
memset(gu,-1,sizeof(gu));
tag=1;
jc[0]=1;fp(i,1,N-100) jc[i]=1ll*jc[i-1]*i%mod;
inv[1]=1;fp(i,2,M-100) inv[i]=1ll*(mod-mod/i)*inv[mod%i]%mod;
}
int main()
{
re int T=gi();
while(T--)
{
n=gi();if(n>1000&&!tag) Pre();
fp(i,1,n) a[i]=gi();
if(n<=1000) solve1();
else
solve2();
}
return 0;
}
[NOI2018]冒泡排序