"蔚来杯"2022牛客暑期多校训练营3_ACM/NOIP/NOI/CCPC/ICPC算法编程基础集训_牛客竞赛OJ
注(検索用):Multi-Universities Campとか、TwitterではNowCoderとか呼ばれている中国の合宿の問題です。
問題概要
長さの順列
に対し、
を
なる
(
)の個数として定めます。
をランダムに決めたときの
の期待値を
で求めてください。
解法
準備
まずの場合は自明なので除きます。また素因数分解すると
なので、素数
について
で求めることができれば、中国剰余定理より答えが求まります。以降はこれらのことを前提にして解きます。
を求める
解説スライドによれば、を固定して完全順列の式を使い式変形を頑張る方法もあるようですが、ここでは組み合わせ的解釈から導くことにします。
を
だと思うことにします。これはつまり、
であるような
を一つ選ぶ場合の数です。今
を求めているのですから、一つ取り出す操作を
回繰り返す方法を数え上げればよいです。このとき取り出す順番を考慮していることに注意します。
さて、個取り出した
たちは当然大量に重複しますが、この重複を除いたものが
種類(ただし
)だったとします。この
を固定してみます。
まず、種類がどのように分布するかを考えます。
回の操作をそれぞれ区別される玉、
種類の
を区別しない箱だと思えば、求める分布の数はそれぞれの箱に1個以上の玉を入れる場合の数ですから、写像12相によれば
となります。ここで
は第2種スターリング数です。
次に、それを達成できる順列がどれくらいあるか考えます。
から
のうち
種類の箱として使うものを決めるのが、どの箱にどの数字を対応させるかまで含めて
通りになります。先ほど箱を区別しなかった分、ここで区別する必要がありました。選んだ
個の値は位置まで決定しており、残りは自由ですから
通りで、掛け合わせると
になります。
順列としての重複を考える必要はありません。なぜなら今の考察は、最初「すべての順列について」「種類の取り方すべてについて」の順で考えていたものを、逆順で表現しなおしただけだからです。
よってであることがわかりました。
ベル数による書きなおし
今は期待値を求めているので、先ほど求めた式の両辺をで割ります。また
のとき
であるため、
の部分を
に書き換えることができます。これにより、求める値を
と表せました。
ところで、を
で足し合わせたものはベル数
です。よって
と書けます。特に
の場合は、
さえ求まればよいことになります。
Touchard's congruence
巨大なに対して
、正確には
を求める必要があります。ここで素数
が十分小さいことを利用します。
Touchard's congruenceと呼ばれる合同式が存在します。非負整数、素数
について
という式ですが、これについても組み合わせ的な解釈を説明します。
まず、ベル数とは、
個の区別できる玉をいくつかの箱に分ける方法でした。ここで
を、
において得られた分け方にさらに
個の玉を追加したものとして捉えます。追加の際に箱を増やしても構いません。
いったん個の分け方を固定しましょう。ここに
個の玉、説明のため
と番号を付けますが、これを追加し、得られた
個の玉の分け方を集めた集合を
とします。最初に定めた
個の分け方が異なれば当然そこから得られる
の要素もすべて異なりますから、
を考えてうまく足し合わせることで
が得られます。
任意のについて、
における玉
を一斉に
に置き換えたものもまた
の要素になっています。これを
とします。つまり、
は
の写像を与えます。また明らかに
です。
を、
を複数回適用することによって互いに移り変わる要素たちに分割します。巡回群
による
への作用の軌道を考えているとも言えますが、以降特に群の性質は使っていません。分割後の各部分集合のサイズを見ましょう。
であって
であるようなものを取ります。
と、
が素数で
が成り立つということから、
がすべて異なると言えます。よって分割後の部分集合で
が含まれるもののサイズはちょうど
だとわかりました。今は
を考えているので、この部分集合は
の要素としては無視してよいです。
結局、なる
を数える問題に帰着されました。ではこの
はどんな構造をしているのでしょう。以下、
を
を満たすものとして固定します。
玉が入っていた箱に同時に入っていた玉
の数を数え、
とします。例えば玉
に注目すると、
個の玉が
によって置き換えられ、
のほうで数えると
個になることから、
が成り立ちます。ここで
より
です。同様の議論で
がすべて等しいことが言えました。つまり
個の玉が同じ数ずつに分けられてそれぞれ箱に入っていることになりますが、
は素数ですから、その分け方は「
個ずつ」または「
個ずつ」です。
個ずつ分ける場合
当然ながら、すでに分けられていた個のどれかと一緒に入ってしまうと
が成り立たなくなるので、それぞれについて新しい箱を用意する
通りしかありません。これを
個の分け方である
倍するのですから、
に対する寄与も
となります。
個ずつ分ける場合
どの箱に入れても常にとなります。ここで
個をまとめて1個の玉だと思うことで、
個の玉を分ける方法と合わせ、
には
だけ寄与します。
以上よりが言えました。ここまでの議論は参考文献[1]のLemma 3.1の証明に着想を得たものです。
また、より一般化した合同式として、非負整数について
が成り立ちますが、これについては帰納法で簡単に示すことができます。以下
を法とします。
ただし最後の行への変形で、に対して
であること、フェルマーの小定理より
であることを用いました。
この等式を使うことで、巨大なに対しても十分高速に
を求めることができます。
を求める
さて、残るはです。ここに
という制約が効いてきます。つまり、改めて
と置きなおして、
に対して
をそれぞれ高速に求められれば良いのです。
は自明なので取り除いておきます。
ところで、第2種オイラー数というものが存在します。これに関する日本語の文献はほとんどなく、英語で「Eulerian numbers of the second kind」などと検索する必要があります。三角かっこで2重にくくる表記が一般的ですが、ここでは第2種スターリング数(と参考文献[2])に合わせてで表記します。
この値の組み合わせ的な解釈を述べます。まず、を並べ替えた数列であって、「任意の
について
と
の間に出現する数はすべて
より大きい」という条件を満たすものを、
次のStirling permutationと呼びます。
とは、
次のStirling permutation
であって、「
を満たす
(
)がちょうど
個ある」という条件を満たすものの数です。
具体例など詳しくはWikipediaを参照してください。だと思う定義も存在し、文献によっては
が少しずれていました。また左右が逆のものもあります。
このような数を突如導入した理由は2点、まず単純な漸化式が存在し小さい範囲の値を高速に求められるから、次にに対し
が成り立つからです。
前者によってを前計算しておけば、後者によって
を高速に求めることができます。
が小さい今回であればリュカの定理により
が達成でき、
が大きければ二項係数を徐々に更新することで
が達成できます。
参考までに、が小さいときは一般のスターリング数を高速に求めることができるそうです。大きな
について
を高速に求めたくなる機会は今後訪れるのでしょうか。
第2種オイラー数の漸化式
というものです。ただし
または
の場合
としておきます。
の場合に
となることは、以下の漸化式の導出により確かめることができます。
この漸化式はdpだと思うことで簡単に導けます。最初にの場合、条件を満たすただ一つの数列
を見て
と定めることができます。以降
とします。
次のStirling permutationに
を二つ挿入する方法を考えます。条件より
と
の間に出現する数はすべて
より大きい必要があるので、特に今、二つの
は隣接している必要があります。挿入できる位置は両端を含めて
箇所あります。
まず、でカウントされる並べ方について考えます。
を挿入することで
なる
を一つ増やす必要がありますから、すでにそうなっている
の後ろに挿入してはいけません。そのような位置は
箇所あります。さらに左端に入れるとその左の項が存在しないため、これもまた意図に沿いません。以上より挿入できる位置は
箇所なので、場合の数として
に対し
だけ寄与します。
次にについて考えます。今度は逆で、先ほど挿入できなかった箇所にしか挿入できません。
なる
は今回
個ありますから、その
箇所と左端を合わせて
だけ寄与します。
二つを合わせることで先の漸化式が得られました。
について
探したらこれにも組み合わせ的な解釈がありました。参考文献[2]のTheorem 2.3を用いるものです。ちなみにこれが、先ほど言及した左右が逆になっているものです。以下、文献と左右が逆になった状態のまま進めます。
まず、を、
次のStirling permutationに対し、その項の間または両端に合計
本仕切りを入れたものの集合とします。ただしこの時、数列の「左端」「
なる
の直後」には必ず1本以上仕切りが入っているものとします。
例えば、
とし、仕切りを
で表せば、
のようになります。
実はそのような仕切りの入った数列と個の玉を
個の区別できない箱に入れる方法が一対一対応しており、したがって
が成り立ちます。
先に、ここから上の式を導いておきましょう。となるので、
次のStirling permutationに対し、条件を満たしつつ
本の仕切りを入れる方法を数え上げればよいです。
「なる
」が
(ただし
かつ
)個あったとすると、そのような数列は
通り存在し、左端と合わせすでに
本の仕切りの位置が確定しています。
残り本の仕切りを入れる位置を
箇所から選びます。これは重複組み合わせによって
と書け、二項係数に直すと
となります。ここまで変形すると、先ほどの
の制限を外してよくなります。仕切りが足りない場合を考慮していましたが、そのような場合
から
となるからです。
まとめるととなり、見事求める式が得られました。
について
参考文献[2]では一方をもう一方に変換する方法を事細かく説明していました。これを引き写してくるのはさすがに面倒なので、具体例を見た後大まかな説明を試みます。
基本的には仕切り一つが箱一つ+玉一つに対応します。玉が二つ以上入っている箱については数列の同じ値を持つ要素二つを対応させます。先ほど挙げた例を変換してみましょう。これは
、
より、ラベル
を
個の集合に分割する方法の一つと対応しているはずです。
まず、を、
と分解していきます。同じ値を持つ要素二つとその間の仕切りすべてを削除するのを繰り返しますが、このとき削除した要素のすぐ左には必ず別の仕切りが存在します。このことは、
の定義で設けた仕切りの位置の制約から言えます。
削除されたのと逆順にラベルを振っていきます。まず最初の仕切りが二つでとなります。右から左の順でラベルが付いているのは、参考文献と第2種オイラー数の定義が逆だからです。次に、
に対応した仕切りのすぐ右の要素を削除しましたから、それをラベル
として
に追加します。
です。
以下同様に、
と更新されていき、無事
を
個の集合に分割できました。どう表示したとしても見にくいので勘弁してください。
さて、分割を数列に戻しましょう。それぞれの集合のうち最小のラベルを仕切りに変換し、残りを倍にコピーして降順に並べると、となります。これを適切にマージします。具体的には、仕切りがもともと対応していたラベルを昇順に見て、それより
小さいラベルが出現する最も右の位置のすぐ左に塊ごと挿入します。
となりました。番号を順序を保って振りなおせば
が得られます。
これはいったい何だったんでしょうか。数列の要素との入れ子関係によってラベルの番号を管理しているのは明らかです。このとき、すでに決まった集合の構造が失われないように要素を二つずつ使っていたのでしょう。
つまり、すべての仕切りについて、一つ右にある要素を見るのと同じ要素の次の出現位置に飛ぶのを交互に繰り返せば、スタート地点の仕切りと同じ集合に入っていたラベルが列挙できるのです。特に要素のラベルは単調減少に並ぶようにしましたが、これは仕切りの位置の制約から、となる場合その間に別の仕切りが必要であることに対応しています。これによって一意性が担保されています。
今説明した両方向の変換が、それぞれ互いの逆変換になっていることも、この理由付けから納得できそうです。僕が納得した気になったので終わります。
実装例
ACLからmodint
を、手持ちのライブラリからの解を復元するのにgarnerのアルゴリズムを使っています。
#include<iostream> #include<atcoder/modint> using namespace std; #include"math/garner.cpp" template<int p> struct A{ using mint=atcoder::static_modint<p>; mint C[p][p],Bell[p],B[5000][5000]; A() { for(int i=0;i<p;i++)for(int j=0;j<p;j++)C[i][j]=0; C[0][0]=1; for(int i=1;i<p;i++) { C[i][0]=1; for(int j=1;j<=i;j++)C[i][j]=C[i-1][j-1]+C[i-1][j]; } Bell[0]=1; for(int i=1;i<p;i++) { Bell[i]=0; for(int j=1;j<=i;j++)Bell[i]+=C[i-1][j-1]*Bell[j-1]; } for(int i=0;i<5000;i++)for(int j=0;j<5000;j++)B[i][j]=0; B[0][0]=1; for(int i=1;i<5000;i++) { B[i][0]=B[i-1][0]; for(int j=1;j<i;j++) { B[i][j]=(2*i-j-1)*B[i-1][j-1]+(j+1)*B[i-1][j]; } } } mint bell(long long k) { int m=0; long long q=1; while(q<=k/p)m++,q*=p; vector<mint>now; now.push_back(1); for(;m>=1;m--,q/=p) { int t=k/q; k%=q; vector<mint>F(t+1); mint coef=1; for(int i=t;i>=0;i--) { F[i]=coef*C[t][i]; coef*=m; } vector<mint>nxt(now.size()+F.size()-1); for(int i=0;i<now.size();i++)for(int j=0;j<F.size();j++)nxt[i+j]+=now[i]*F[j]; while(nxt.size()>p) { mint tmp=nxt.back(); nxt.pop_back(); nxt[nxt.size()-p]+=tmp; nxt[nxt.size()-p+1]+=tmp; } now=nxt; } mint ret=0; for(int i=0;i<now.size();i++) { int id=i+k; if(id<p)ret+=Bell[id]*now[i]; else ret+=(Bell[id-p]+Bell[id-p+1])*now[i]; } return ret; } mint combination(long long n,long long k) { if(n<k)return 0; mint ret=1; while(k>0) { ret*=C[n%p][k%p]; n/=p; k/=p; } return ret; } mint S2(long long k,long long m) { m=k-m; if(m==0)return 1; //S2(k,k-m) mint ret=0; for(int i=0;i<m;i++)ret+=B[m][i]*combination(k+m-i-1,2*m); return ret; } int solve(long long n,long long k) { if(k==0)return 1; mint ans=bell(k); for(long long m=n+1;m<=k;m++)ans-=S2(k,m); return ans.val(); } }; int main() { long long n,k; cin>>n>>k; vector<long long>x(3),m(3); x[0]=A<857>().solve(n,k);m[0]=857; x[1]=A<997>().solve(n,k);m[1]=997; x[2]=A<1009>().solve(n,k);m[2]=1009; cout<<garner(x,m)<<endl; }
参考文献
[1]:Greg Hurst, Andrew Schultz、2009、[0906.0696] An elementary (number theory) proof of Touchard's congruence
[2]:Ira Gessel, Richard P Stanley、1976、Stirling polynomials - ScienceDirect