ランダムに配列をシャッフルする「Fisher-Yates shuffleアルゴリズム」を理解した
2020/05/10 17:57 追記:shuffleがshufflになっていたのを訂正
シャッフルアルゴリズムとしてはかなり有名な部類だそう。Fisher-Yatesってなんて読むんだろう。
設定、目標、制約等
長さの順列をランダムに並び変えたい。効率の良い方法を見つける。
ただし乱数生成器の偏りとかは考えてもしょうがないので無視する。
以降、rand(a, b)
で以上以下の整数値をランダムに取得できると仮定する。0-indexedとして説明する。
まずは考えてみる
rand(0, n-1)
で整数値を2つ取得してswap(p[i], p[j])
する。この操作を十分な回数行うとシャッフルされるように思う。
でも少し考えてみると、例えば回数値をswapしたとき、2回以上swapされる数値や、逆に1回もswapされない数値が出てくることがわかる。試行回数を無限回に増やせば偏りは無くなるが、実行時間も無限にかかることになるため現実的ではない。かといって、有限回試行した程度では偏りが無くなったとは言えない。
ところで、長さの順列は通り存在する。
順列を作る作業は、「最初の要素はからまでの個の中から一つを選び、次の要素はからのうち先ほど選んだ一つを除いた個の中から一つを選び、…と繰り返していく」作業と言える。そしてこの作業の各ステップは互いに独立なので、となる。この、「回目は個の中から選び、回目は個の中から選び、…」という操作のイメージをだいたいそのまま適用するのが今回紹介するFisher-Yates shuffleアルゴリズムだ。
Fisher-Yates shuffleアルゴリズムの手順
長さの配列に対して以下の操作を実行する。
- 変数にを代入して、以下の~の操作を回繰り返す。
- 変数に
rand(0, i)
を代入する。 swap(p[i], p[j])
する。- の値を減らす。
操作が終了した時点で、もとの順列の並びは完全にシャッフルされている。
アルゴリズムを理解する
上記の説明中の変数で、は確定させていない要素のうち一番末尾の要素を表す。また、は確定させていない要素のうちの一つをランダムに選ぶ。
配列を確定していない部分(前半)と既に確定している部分(後半)に分ける。確定していない部分のうち一番最後の要素を、まだ選ばれていない要素の中からどれにするか選ぶというイメージ。選ばれた要素は末尾に固定され、その後選ばれることはない。つまり、同じ要素が回以上選択されることは無いということである。回の操作が終了した時点で、通りの要素の並び方のうちどれかになる(先ほどの説明通り)。計算量はとなる。
実は配列の要素を比較したりしていないので、順列に限らず、どの要素も異なるような配列ならば適用可能である。
C++での実装
アルゴリズムが単純なので、実装も数行程度とそこまで重くない。
C++での乱数生成はstd::mt19973
を使うとよい。さらにある範囲から乱数を得たいときはstd::uniform_int_distribution
を使うと偏り無く数値を得られる。
#include <iostream> #include <vector> #include <random> std::mt19937 mt{std::random_device{}()}; void FisherYates(std::vector<int>& p){ int n = p.size(); for(int i = n-1; i >= 0; i --){ std::uniform_int_distribution<int> dist(0, i); int j = dist(mt); std::swap(p[i], p[j]); } } // 以下、動作確認用コード int main(){ std::vector<int> p; int n = 10; for(int i = 0; i < n; i ++){ p.push_back(i); } FisherYates(p); for (int i = 0; i < n; i ++){ std::cout << p[i] << std::endl; } return 0; }
ちなみに
「ふぃっしゃーいぇーつ」と読むらしい。勉強になりました。
参考文献
Fisher–Yates shuffle - Wikipedia
Wikipediaです。こら!Wikipediaを参考文献にしてはいけません!
ランダムシャッフル | Programming Place Plus アルゴリズムとデータ構造編【その他のアルゴリズム】 第2章
単純なswapによるシャッフルが偏ってしまう説明があり、わかりやすいです。
C++の乱数ライブラリが使いにくい話 - Qiita
C++で乱数生成するための方法が色々まとめてあります。