Fisher-Yates法
配列をシャッフルする際に用いられるアルゴリズムとして,Fisher-Yates法は非常に有名なのではないでしょうか.
Fisher-Yates法によるシャッフルアルゴリズムは,以下のようなコードになります.
def shuffle(array):
for i in range(len(array)):
r = random.randint(i, len(array) - 1)
tmp = array[i]
array[i] = array[r]
array[r] = tmp
return array
ときたま,Fisher-Yates法のアルゴリズムを以下のように実装しているコードを見かけます.
def shuffle(array):
for i in range(len(array)):
r = random.randint(0, len(array) - 1)
tmp = array[i]
array[i] = array[r]
array[r] = tmp
return array
しかし,このコードは厳密なシャッフルを行えないため,間違いがあるコードです.
今回は,なぜこのコードが間違っているのか,ということを,配列の長さが33の場合を例にして証明してみたいと思います.
証明すること
まず,配列の長さNをN=3とします.
シャッフルコードが正しいアルゴリズムになっているなら,配列のi番目の要素がシャッフル実行後にj番目となっている確率は,偏りなく均等になっているはずです.
つまり,配列のi番目の要素が,j番目となっている確率をp(N)i,jとすると,p(N)i,j=1Nとなっているはずです.
しかし,間違ったコードの書き方では,この確率に偏りが生じてしまいます.
以降では,間違ったコードを実行した場合の確率p(N)i,jを計算し,実際に偏りが生じてしまっていることを示します.
p(N)i,jの導出
p(N)i,jを計算するために,コードのループ部分をn∈{1,2,3}回実行した後に,i番目の要素がj番目となっている確率をp(n)i,jとし,この確率を順々に計算していきます.
p(n)i,jの漸化式
まず,順々に計算を行っていくために,p(n)i,jの漸化式を導出します.
p(n)i,jは,p(n−1)i,jを用いると,以下のように表すことができます.
p(n)i,j={1N(j=n)p(n−1)i,jN−1N+p(n−1)i,n1N(j≠n)上の式では,j=nの場合と,j≠nの場合で場合分けをしています.
これは,n番目に入れ替える対象がまさにj番目であるというケースと,それ以外とで分けて考えている,ということです.
j=nの場合,それまでに配列のどのインデックスにいようとも,入れ替えられる対象として選ばれた場合,n回目のループ実行後にj番目の要素となることができます.
したがって,j=nの場合,入れ替えられる対象として選ばれる確率1Nがp(n)i,jになります.
一方でj≠nの場合,n回目のループ実行前に既にj番目にいるか,n回目のループ実行前にn番目にいる,という2つの条件のいずれかを満たしていなければなりません.
前者の条件を満たしている場合,j番目の要素が入れ替えられる対象として選ばれることがなければ,n回目のループ実行後にj番目の要素となることができます.
前者の条件を満たす確率はp(n−1)i,jであり,j番目の要素が入れ替えられる対象として選ばれる確率は1Nなので,前者の条件を満たしn回目のループ実行後にj番目の要素となる確率はp(n−1)i,jN−1Nとなります.
後者の条件を満たしている場合,j番目の要素が入れ替えられる対象として選ばれれば,n回目のループ実行後にj番目の要素となることができます.
後者の条件を満たす確率はp(n−1)i,nであり,j番目の要素が入れ替えられる対象として選ばれる確率は1Nなので,後者の条件を満たしn回目のループ実行後にj番目の要素となる確率はp(n−1)i,j1Nとなります.
結局,j≠nの場合,これら2つの確率を足し合わせて,p(n)i,jはp(n)i,j=p(n−1)i,jN−1N+p(n−1)i,j1Nと計算されます.
漸化式が導出できたので,あとはn=1から順々に計算を行っていきましょう.
n=1の場合の計算
(1) i=1の時
p(1)1,j=1N(2) i>1の時
p(1)i,j={1N(j=1)p(0)i,jN−1N+p(0)i,11N(j≠1)={1N(j=1)0(j>1∧j≠i)N−1N(j>1∧j=i)n=2の場合の計算
(1) i=1の時
p(2)1,j=1N(2) i=2の時
p(2)2,j={1N(j=2)p(1)2,jN−1N+p(1)2,21N(j≠2)={1N(j=2)1NN−1N+N−1N1N(j=1)N−1N1N(j>2)(3) i>2の時
p(2)i,j={1N(j=2)p(1)i,jN−1N+p(1)i,21N(j≠2)={1N(j=2)1NN−1N(j=1)0(j>2∧j≠i)N−1NN−1N(j>2∧j=i)n=3の場合の計算
(1) i=1の時
p(3)1,j=1N(2) i=2の時
p(3)2,j={1N(j=3)p(2)2,jN−1N+p(2)2,31N(j≠3)={1N(j=3)1NN−1N+N−1N1N1N(j=2)(1NN−1N+N−1N1N)N−1N+N−1N1N1N(j=1)N−1N1NN−1N+N−1N1N1N(j>3)(3) i=3の時
p(3)3,j={1N(j=3)p(2)3,jN−1N+p(2)3,31N(j≠3)={1N(j=3)p(2)3,2N−1N+N−1NN−1N1N(j=2)p(2)3,1N−1N+N−1NN−1N1N(j=1)p(2)3,jN−1N+N−1NN−1N1N(j>3)={1N(j=3)1NN−1N+N−1NN−1N1N(j=2)1NN−1NN−1N+N−1NN−1N1N(j=1)N−1NN−1N1N(j>3)(4) i>3の時
p(3)i,j={1N(j=3)p(2)i,jN−1N+p(2)i,31N(j≠3)={1N(j=3)p(2)i,2N−1N(j=2)p(2)i,3N−1N(j=1)p(2)i,jN−1N(j>3)={1N(j=3)1NN−1N(j=2)1NN−1NN−1N(j=1)0(j>3∧j≠i)N−1NN−1NN−1N(j>3∧j=i)N=3の場合におけるp(N)i,jの導出
n=3までの確率を計算することができたので,これを使ってN=3とした場合の確率p(N)i,jの式を計算してみましょう.
主に3.4節の式にN=3を代入すればよいのですが,N=3の場合はi>3やj>3になることがありえないため,式をもうすこし簡潔に書けるようになります.
(1) i=1の時
p(3)1,j=13(2) i=2の時
p(3)2,j={13(j=3)827(j=2)1027(j=1)(3) i=3の時
p(3)3,j={13(j=3)1027(j=2)827(j=1)i=1に関しては確率に偏りがないことが分かりますが,i=2,3に関しては確率に偏りが生じていることが分かります.
これでは配列の要素の初期順によって結果に偏りが生じるため,偏りなく均等にシャッフルを行いたいという意図に反してしまっています.
配列のシャッフルコードを書く場合には,確率に偏りが生じていないか実験してみてデバッグをしてみるのが良いでしょう.
最後に
本当はNが任意の値の場合の確率の式を導出したかったのですが,イマイチ規則性が分からなかったのでN=4の計算をしている途中で断念してしまいました.
そのうち任意のNに対する式を導出するつもりです,そのうち...