ちょうど背景として、私は Fisher-Yates 完璧なシャッフルを知っています。 O(n)複雑さと保証された均一性を備えた素晴らしいシャッフルであり、配列のインプレース更新を許可する環境では、私はそれを使用しないでください... (すべてではないにしても、ほとんどの場合、命令型プログラミング環境)。
残念ながら、関数型プログラミングの世界では、変更可能な状態にアクセスできません。
ただし、Fisher-Yatesがあるため、シャッフルアルゴリズムの設計方法については、あまり多くの文献を見つけることができません。実際にそれを扱っているいくつかの場所は、実際には「ここでフィッシャーイェイツです。これはあなたが知る必要があるすべてのシャッフルです」と言う前に簡単にそうします。結局、私は自分の解決策を考え出す必要がありました。
私が思いついた解決策は、データの任意のリストをシャッフルするこのような作品です:
Erlangコードでは、次のようになります。
_shuffle([]) -> [];
shuffle([L]) -> [L];
shuffle(L) ->
{Left, Right} = lists:partition(fun(_) ->
random:uniform() < 0.5
end, L),
shuffle(Left) ++ shuffle(Right).
_
(これがあなたにとって乱れたクイックソートのように見える場合、まあ、それは基本的にはそれです。)
だからここに私の問題があります:フィッシャーイエーツではないシャッフルアルゴリズムを見つけることが困難になる同じ状況により、シャッフルアルゴリズムを分析するツールを見つけることも同様に困難になります。 PRNGの均一性、周期性などを分析するために私が見つけることができる多くの文献がありますが、シャッフルを分析する方法に関する情報はあまりありません。 (実際、私がシャッフルの分析で見つけた情報のいくつかは、明らかに間違っていました-簡単なテクニックで簡単に騙されました。)
だから私の質問はこれです:私のシャッフルアルゴリズムをどのように分析しますか(そこに呼び出されるrandom:uniform()
呼び出しは、適切な特性を持つ適切な乱数を生成するタスクまでであると仮定します)?たとえば、1..100の範囲の整数のリストに対してシャフラーを100,000回実行した結果、かなり良い結果が得られたかどうかを判断するための数学ツールはありますか?私は自分でいくつかのテストを行いました(たとえば、シャッフルの増分と減分を比較しています)が、さらにいくつか知りたいのですが。
そして、そのシャッフルアルゴリズム自体に何らかの洞察があれば、それも評価されます。
確率を使用するアルゴリズムの正確性に関する私の個人的なアプローチ:証明する方法が正しいことを知っている場合、それはおそらく正しいでしょう。そうしないと、間違いです。
別の言い方をすると、思いつく可能性のあるすべてのアルゴリズムを分析しようとすることは一般に絶望的です。できるアルゴリズムが見つかるまでアルゴリズムを探し続ける必要があります(== --- ==)正しいことを証明します。
私は、単純な「多数のテストをスローして均一性をチェックする」よりも強力なシャッフル(またはより一般的にはランダム使用アルゴリズム)を「自動的に」分析する1つの方法を知っています。アルゴリズムの各入力に関連付けられた分布を機械的に計算できます。
一般的な考え方は、ランダムに使用するアルゴリズムが可能性の世界の一部を探索するということです。アルゴリズムがセット内のランダムな要素を要求するたびに(コインを弾くときに{true
、false
})、アルゴリズムには2つの可能な結果があり、そのうちの1つが選択されます。アルゴリズムを変更して、考えられる結果の1つを返すのではなく、all解を並列に探索し、関連する分布とともにすべての考えられる結果を返すようにすることができます。 。
一般に、そのためにはアルゴリズムを詳細に書き直す必要があります。言語が区切り付き継続をサポートしている場合は、その必要はありません。ランダムな要素を要求する関数内に「考えられるすべての結果の探索」を実装できます(アイデアは、ランダムジェネレーターが結果を返すのではなく、プログラムに関連付けられた継続をキャプチャし、すべての異なる結果で実行するというものです)。このアプローチの例については、olegの [〜#〜] hansei [〜#〜] を参照してください。
中間であり、おそらくそれほど難解ではない解決策は、この「起こり得る結果の世界」をモナドとして表し、モナディックプログラミング用の機能を備えたHaskellなどの言語を使用することです。 probability パッケージの確率モナドを使用した、Haskellでのアルゴリズムのバリアントの実装例を以下に示します。
_import Numeric.Probability.Distribution
shuffleM :: (Num prob, Fractional prob) => [a] -> T prob [a]
shuffleM [] = return []
shuffleM [x] = return [x]
shuffleM (pivot:li) = do
(left, right) <- partition li
sleft <- shuffleM left
sright <- shuffleM right
return (sleft ++ [pivot] ++ sright)
where partition [] = return ([], [])
partition (x:xs) = do
(left, right) <- partition xs
uniform [(x:left, right), (left, x:right)]
_
与えられた入力に対してそれを実行し、出力分布を得ることができます:
_*Main> shuffleM [1,2]
fromFreqs [([1,2],0.5),([2,1],0.5)]
*Main> shuffleM [1,2,3]
fromFreqs
[([2,1,3],0.25),([3,1,2],0.25),([1,2,3],0.125),
([1,3,2],0.125),([2,3,1],0.125),([3,2,1],0.125)]
_
このアルゴリズムは、サイズ2の入力では均一ですが、サイズ3の入力では不均一であることがわかります。
テストベースのアプローチとの違いは、有限のステップ数で絶対的な確実性を得ることができるということです。これは、可能性の世界の徹底的な調査に相当するため、非常に大きくなる可能性があります(ただし、一般に2 ^ Nよりも小さい)。同様の結果の因数分解があります)が、それが不均一な分布を返す場合、アルゴリズムが間違っていることは確かです。もちろん、_[1..N]
_と_1 <= N <= 100
_の均一な分布を返す場合、アルゴリズムがサイズ100のリストまで均一であることだけがわかります。まだ間違っているかもしれません。
注意:このアルゴリズムは、特定のピボット処理のため、Erlangの実装のバリアントです。あなたの場合のようにピボットを使用しない場合、入力サイズは各ステップでもう減少しません:アルゴリズムは、すべての入力が左側のリスト(または右側のリスト)にある場合も考慮し、無限ループで失われます。これは確率モナドの実装の弱点であり(アルゴリズムが非終了の確率0の場合、分布計算はまだ発散する可能性があります)、修正方法がまだわかりません。
以下は、正しいことが証明できると確信している簡単なアルゴリズムです。
衝突の確率(選択した2つの乱数が等しい)が十分に低いことがわかっている場合は、ステップ2を省略できますが、それがないとシャッフルは完全に均一ではありません。
[1..N]でキーを選択すると、Nはコレクションの長さであり、衝突が多数発生します( 誕生日の問題 )。キーを32ビット整数として選択すると、実際には競合の可能性は低くなりますが、誕生日の問題が発生しやすくなります。
有限長のキーではなく、無限の(遅延評価された)ビット文字列をキーとして使用する場合、衝突の確率は0になり、区別の確認は不要になります。
以下は、OCamlでのシャッフル実装であり、遅延ビット列として遅延実数を使用しています。
_type 'a stream = Cons of 'a * 'a stream lazy_t
let rec real_number () =
Cons (Random.bool (), lazy (real_number ()))
let rec compare_real a b = match a, b with
| Cons (true, _), Cons (false, _) -> 1
| Cons (false, _), Cons (true, _) -> -1
| Cons (_, lazy a'), Cons (_, lazy b') ->
compare_real a' b'
let shuffle list =
List.map snd
(List.sort (fun (ra, _) (rb, _) -> compare_real ra rb)
(List.map (fun x -> real_number (), x) list))
_
「純粋なシャッフル」には他のアプローチがあります。いいのは、apfelmusの mergesortベースのソリューション です。
アルゴリズム上の考慮事項:以前のアルゴリズムの複雑さは、すべてのキーが異なる確率に依存します。それらを32ビット整数として選択すると、特定のキーが別のキーと衝突する可能性が約40億分の1になります。これらのキーによるソートは、乱数の選択がO(1)であると想定して、O(n log n)です。
ビット文字列が無限の場合、ピッキングをやり直す必要はありませんが、複雑さは「ストリームの要素が平均でいくつ評価されるか」に関連しています。私はそれが平均でO(log n)であると推測します(したがって、まだ合計でO(n log n)です)が、証明はありません。
もっと考え直した後、私は(duplepのように)あなたの実装は正しいと思います。これは非公式の説明です。
リストの各要素は、いくつかのrandom:uniform() < 0.5
テストによってtestedされます。要素に、これらのテストの結果のリストをブール値のリストまたは{_0
_、_1
_}として関連付けることができます。アルゴリズムの最初は、これらの番号のいずれかに関連付けられているリストがわかりません。最初のpartition
呼び出しの後、各リストの最初の要素などがわかります。アルゴリズムが戻ると、テストのリストは完全に既知であり、要素はsortedそれらのリストに従って(辞書式順序でソートされ、または実数のバイナリ表現と見なされます)。
したがって、このアルゴリズムは、無限のビット文字列キーによるソートと同等です。ピボット要素に対するクイックソートのパーティションを連想させるリストのパーティション化のアクションは、実際には、ビット文字列の特定の位置で、評価_0
_の要素を評価_1
_の要素から分離する方法です。
ビットストリングがすべて異なるため、ソートは均一です。実際、n
番目までのビットの実数を持つ2つの要素は、深さshuffle
の再帰的なn
呼び出し中に発生するパーティションの同じ側にあります。アルゴリズムは、パーティションの結果として得られるすべてのリストが空またはシングルトンである場合にのみ終了します。すべての要素が少なくとも1つのテストによって分離されているため、1つの異なる2進10進数があります。
アルゴリズム(または同等の並べ替えベースの方法)についての微妙な点は、終了条件がprobabilisticであることです。 Fisher-Yatesは常に、既知の数のステップ(配列内の要素の数)の後に終了します。アルゴリズムでは、終了は乱数ジェネレーターの出力に依存します。
アルゴリズムを終了しない可能性のある出力divergeがあります。たとえば、乱数ジェネレーターが常に_0
_を出力する場合、partition
を呼び出すたびに入力リストが変更されずに返され、そこでシャッフルを再帰的に呼び出します。無限にループします。
ただし、乱数ジェネレーターが公平であると確信している場合、これは問題ではありません。不正を行わず、常に独立して均一に分散された結果を返します。その場合、テストrandom:uniform() < 0.5
が常にtrue
(またはfalse
)を返す確率は正確に0です。
true
を返す確率は2 ^ {-N}ですtrue
を返す確率は、最初のN個の呼び出しが_0
_を返すイベントのすべてのNに対する無限交差の確率です。 2 ^ {-N}の極限値isであり、0です¹:数学的な詳細については、 http://en.wikipedia.org/wiki/Measure_(mathematics)#Measures_of_infinite_intersections_of_measurable_sets を参照してください
より一般的には、一部の要素が同じブールストリームに関連付けられた場合にのみ、アルゴリズムは終了しません。これは、少なくとも2つの要素が同じブールストリームを持つことを意味します。しかし、2つのランダムなブールストリームが等しい確率は再び0です。位置Kの数字が等しい確率は1/2であるため、最初のN桁が等しい確率は2 ^ {-N}であり、同じです。分析が適用されます。
したがって、アルゴリズムが確率1で終了することがわかります。これは、Fisher-Yatesアルゴリズムが常に終了することを保証する少し弱い保証です。特に、乱数ジェネレータを制御する悪質な攻撃者の攻撃に対して脆弱です。
さらに確率論を使用すると、特定の入力長に対するアルゴリズムの実行時間の分布を計算することもできます。これは私の技術的能力を超えていますが、私はそれが良いと思います:私はあなたが平均してO(log N)の最初の数字を見るだけですべてのNレイジーストリームが異なっていることと、はるかに高い実行時間の確率をチェックする必要があると思います指数関数的に減少します。
ウィキペディアの記事で説明されているように、アルゴリズムはソートベースのシャッフルです。
一般的に言えば、並べ替えベースのシャッフルの計算の複雑さは、基になる並べ替えアルゴリズムと同じです(例:O(nlogn)平均、O(n²)クイックソートベースのシャッフルの最悪のケース)、およびwhile分布は完全に均一ではありません均一です。ほとんどの実用的な目的では、均一に近づく必要があります。
Oleg Kiselyovが次の記事/ディスカッションを提供します。
これは、ソートベースのシャッフルの制限をより詳細にカバーし、Fischer–Yates戦略の2つの適応も提供します:ナイーブO(n²) 1つ、およびバイナリツリーベースのO(nlogn ) 1。
残念ながら、関数型プログラミングの世界では、変更可能な状態にアクセスできません。
これは真実ではありません:純粋に関数型プログラミングは副作用を回避しますが、副作用を必要とせずに、ファーストクラスの効果で可変状態へのアクセスをサポートします。
この場合、Haskellの可変配列を使用して、このチュートリアルで説明されているように、Fischer–Yatesアルゴリズムを実装できます。
シャッフルソートの具体的な基盤は、実際には無限キー 基数ソート です。gascheが指摘するように、各パーティションは数字のグループに対応しています。
これの主な欠点は、他の無限キーソートシャッフルと同じです。終了の保証はありません。比較が進むにつれて終了の可能性が高くなりますが、上限はありません。最悪の場合の複雑さはO(∞)です。
私は少し前にこれと似たようなことをしていました。特に、機能的で不変であるがO(1)ランダムアクセス/更新特性を備えた)Clojureのベクトルに興味があるかもしれません。これら2つの要点には、「このMサイズのリストからランダムにN個の要素を取得する」の実装がいくつかあります。N= Mとすると、少なくとも1つは、Fisher-Yatesの機能的な実装になります。
ランダム性をテストする方法(インケース-シャッフル) に基づいて、以下を提案します。
ゼロと1の等しい数で構成されるシャッフル(中サイズ)配列。退屈するまで繰り返して連結します。これらをダイハードテストへの入力として使用します。適切なシャッフルがある場合は、ゼロと1のランダムシーケンスを生成する必要があります(ゼロ(または1)の累積超過は、中サイズの配列の境界でゼロになることに注意してください。テストで検出されることを期待します。ただし、「中」が大きいほど、そうする可能性は低くなります)。
テストでは、次の3つの理由でシャッフルを拒否できることに注意してください。
テストが拒否された場合は、解決する必要があります。
diehardテスト のさまざまな適応(特定の数値を解決するために、 diehardページ の source を使用しました)。適応の主なメカニズムは、シャッフルアルゴリズムを均一に分散されたランダムビットのソースとして機能させることです。
等々...