E - Cigar Box
これの噛み砕き
]のケースを具体例にしつつ説明する
0-indexed で考える
最終的に「移動を経験する数の集合」は必ず「移動を経験しない集合」を左右から挟む形になる(集合は空かもしれないけど)
そこで、その「移動を経験する左右の集合」が左個、右個の時の場合を数えることにする
つまり、ソート列からのとは移動を経験、は移動を経験せずに配置される
でが昇順に並んでない場合はソート列からを作るのが明らかに不可能なので、その時の場合の数は
この時はは動かさず、だけを動かす
こっちだとを動かさない この時ソート済みの数列からの並びは作れないので数えない
次に移動個に役割を振り分ける
まず横一列に並んだ個のボールで移動を表すことにし(左から番目のボールが回目の移動に対応)、ちょうど個の空でないグループに分ける
※元記事では色に塗り分けているけど実際には赤青赤と青赤青とかを区別しないので注意
同じグループのボールは同じ数を移動することに対応する
]で個のグループに分ける通り数を表すことにすると、
と言う式で表すことが出来る
これは
- 個のボールを色で塗り分ける場合の数を数える
- 色しか使ってないケースがだけ存在するので引く
- (赤青赤と青赤青のように)同じグループを表す色ぬりが存在するのでで割る
と言う操作である
次に種類のうち(その色の最後のボールが)左移動をする個を決める
こうすると自動的に各ボールがどの数を担当するかが決定する
例えば青と黄色は青の方が最後の一手が後ろにあるので、青がの右端のを担当
各色について、最後の一手の左右は決まるがそれ以外については左右どちらを選んでも良い
最後の一手で無いボールは個あるので、ここの向きの決め方が通りである
よって答えは
計算量はとすると
- を求めるのに
- 階乗やべきを前計算で求めておくのに
- が個(各場合について移動しない区間が昇順かはで求められる,下の実装参照)
よって全体で
以下の実装ではべきの前計算をサボってるので
#include<bits/stdc++.h> #include<atcoder/modint> using namespace std; using mint=atcoder::modint998244353; mint group[3001],Fact[3001],Finv[3001];//Fact[i]=i!,Finv[i]=1/(i!) mint nCk(int n,int k){ return Fact[n]*Finv[k]*Finv[n-k]; } int main(){ Fact[0]=Finv[0]=1; for(int i=1;i<=3000;i++){ Fact[i]=Fact[i-1]*i; Finv[i]=Fact[i].inv(); } int n,m;cin>>n>>m; vector<int> v(n); for(int &p:v)cin>>p; for(int k=1;k<=m;k++){ group[k]=mint(k).pow(m); for(int i=1;i<k;i++)group[k]-=group[i]*Fact[k]*Finv[k-i]; group[k]*=Finv[k]; } mint ans=0; for(int l=0;l<=n;l++)//左にl個移動させる for(int j=l;j<=n;j++){//aの[j,n)は右移動によって持ってくる if(j-2>=l&&v[j-1]<v[j-2])break;//移動させない部分が昇順じゃない int r=n-j; if(m-l-r<0)continue; ans+=group[l+r]*nCk(l+r,l)*mint(2).pow(m-l-r); } cout<<ans.val()<<endl; }