1. 程式人生 > >[NOI2018]冒泡排序

[NOI2018]冒泡排序

ans efi 記憶 計算 ret include str pla 記憶化

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]冒泡排序