この「単純な」シャッフルアルゴリズムの何が問題になっていますか?


23

これは、配列をランダムにシャッフルすることに関する Stackoverflowの質問のフォローアップです。

「単純な」アドホックな実装に依存するのではなく、配列をシャッフルするために使用する確立されたアルゴリズム(Knuth-Fisher-Yates Shuffleなど)があります。

私は今、私の素朴なアルゴリズムが壊れていることを証明(または反証)することに興味があります(すべての可能な順列を等しい確率で生成するわけではありません)。

アルゴリズムは次のとおりです。

ループを数回繰り返し(配列の長さで行う必要があります)、繰り返しごとに2つのランダム配列インデックスを取得し、2つの要素を交換します。

明らかに、これにはKFY(2倍)よりも多くの乱数が必要ですが、それ以外は適切に動作しますか?そして、適切な反復回数は何ですか(「配列の長さ」で十分ですか)。


4
このスワッピングがFYよりも「シンプル」または「ナイーブ」であると人々が考える理由を理解できません...この問題を初めて解決したとき、FYを実装しました(名前さえ知らない) 、それが私にとってそれを行う最も簡単な方法のように思えたからです。

1
@mbq:個人的には、FYは私にとってより「自然」に見えることに同意するものの、それらも同様に簡単だと思います。
ニコ

3
シャッフルアルゴリズムを自分で書いた後(私が放棄して以来実践していた)調査したとき、私はすべて「神聖ながらくた、行われ、名前を持っている !!」でした。
JMは統計学者ではありません

回答:


12

これは壊れていますが、十分なシャッフルを実行すると、優れた近似になる可能性があります(前の回答が示しているように)。

何が起こっているかを把握するために、最初の要素が固定されている要素配列シャッフルをアルゴリズムが生成する頻度を検討してください。順列が同じ確率で生成される場合、これはの時間で発生するはずです。してみましょう後、この出現の相対頻度もあなたのアルゴリズムでシャッフル。寛大になりましょう。実際にシャッフルのためにインデックスの異なるペアをランダムに均一にランダムに選択し、各ペアが確率 =K 2 1 / K P N N 1 / Kkk21/kpnn 2/kk11/(k2)2/(k(k1))。(つまり、「些細な」シャッフルは無駄にならないことを意味します。一方、2要素の修正と交換を交互に行うため、2要素配列のアルゴリズムを完全に破壊します。手順、結果にランダム性はまったくありません!)

が2つのばらばらの方法でシャッフルされた後、最初の要素が元の場所で見つかるため、この頻度は単純な繰り返しを満たします。1つは、シャッフル後に修正され、次のシャッフルは最初の要素を移動しないことです。もう1つは、シャッフル後に移動されたが、シャッフルがそれを元に戻すことです。最初の要素を移動しない可能性は =に等しいが、最初の要素を戻す可能性は等しい =。いつから:n n n + 1 s t k 1n+1nnn+1stk2/k1/ k(k12)/(k2)(k2)/k 2/kk11/(k2)2/(k(k1))

p0=1
は、最初の要素が正しい場所から始まるためです。

pn+1=k2kpn+2k(k1)(1pn).

解決策は

pn=1/k+(k3k1)nk1k.

減算すると、周波数がによって間違っていることがわかります。と大きい場合、適切な近似はです。これは、この特定の頻度のエラーが、配列のサイズ()に関連するスワップの数とともに指数関数的に減少することを示しています。これは、比較的多数のスワップを行った場合、 -しかし、エラーは常にそこにあります。k 31/k knk1(k3k1)nk1kknn/kk1kexp(2nk1)n/k

すべての周波数におけるエラーの包括的な分析を提供することは困難です。ただし、これらはこのように動作する可能性が高いため、エラーを許容できるほど小さくするには、少なくとも(スワップの数)が必要です。近似解はn

n>12(1(k1)log(ϵ))

どこに比べて非常に小さくなければなら。これが意味、数回であるべきであるも粗近似するため(すなわち、程度である倍程度です。)1 / k n k ϵ 0.01 1 / kϵ1/knkϵ0.011/k

これはすべて疑問を投げかけます:なぜ完全ではない(しかしほぼ正確な)アルゴリズムを使用することを選択するのでしょうか?

編集

Thiloのコメントは適切です(そして、誰もこれを指摘しないことを望んでいたので、この余分な作業をspareしまないでください!)。ロジックを説明しましょう。

  • 毎回実際のスワップを生成することを確認する場合、あなたは完全にめちゃくちゃです。の場合に指摘した問題は、すべての配列に及びます。偶数個のスワップを適用すると、可能なすべての順列の半分しか取得できません。残りの半分は、奇数のスワップを適用することにより取得されます。したがって、この状況では、順列の一様分布に近い場所を生成することはできません(ただし、サイズが大きいシミュレーションスタディでは問題を検出できない可能性があるため)。それは本当に悪いです。kk=2k

  • したがって、2つの位置をランダムに独立して生成することにより、ランダムにスワップを生成するのが賢明です。これは、要素をそれ自体と交換するたびにチャンスがあることを意味します。つまり、何もしません。このプロセスにより、アルゴリズムが少し遅くなりますステップ後、真のスワップが発生したと予想されます。n k 11/knk1kN<N

  • エラーのサイズは、個別のスワップの数とともに単調に減少することに注意してください。したがって、平均して実行するスワップを少なくすると、平均てエラーが増加します。ただし、これは、最初の箇条書きで説明した問題を克服するために支払う必要がある価格です。その結果、私の誤差推定値は控えめに低く、おおよそ係数です。(k1)/k

また、興味深い明らかな例外を指摘したかったのです。エラーの式をよく見ると、場合にはエラーがないことがわかります。これは間違いではありません。正しいです。ただし、ここでは、順列の一様分布に関連する統計を1つだけ調べました。ときにアルゴリズムがこの1つの統計を再現できるという事実(つまり、特定の位置を修正する順列の正しい周波数を取得する)は、順列が実際に均一に分布していることを保証しません。実際、実際のスワップの後、生成できる置換は、k = 3 2 n 123 321k=3k=32n(123)(321)、およびアイデンティティ。後者のみが特定の位置を修正するため、実際、順列の正確に3分の1が位置を修正します。しかし、順列の半分が欠落しています!他の場合、実際のスワップの後、可能な置換は、、およびです。繰り返しますが、これらのいずれか1つが任意の位置を修正するため、その位置を修正する置換の正しい頻度を取得しますが、可能な置換の半分のみを取得します。2n+1(12)(23)(13)

この小さな例は、議論の主要部分を明らかにするのに役立ちます。「寛大」であることにより、特定の統計のエラー率を控えめに過小評価します。そのエラー率はすべてのでゼロではないため、アルゴリズムが壊れていることがわかります。さらに、この統計のエラー率の減衰を分析することにより、順列の均一な分布を近似するという希望を得るために必要なアルゴリズムの反復回数の下限を確立します。k4


1
「寛大になりましょう。実際にシャッフルのために、インデックスの異なるペアをランダムにランダムに選択しているとします」なぜそのような仮定を立てることができるのか、それがどのように寛大であるのかがわかりません。考えられる順列を破棄しているように見えるため、ランダム分布はさらに少なくなります。
ティロ

1
@ティロ:ありがとう。あなたのコメントは拡張回答に値するので、私はそれを応答自体に入れました。ここで、「寛大」であることは実際には順列を破棄するものではないことを指摘しておきます。それは、そうでなければ何もしないアルゴリズムのステップを削除するだけです。
whuber

2
この問題は、置換グループのケイリーグラフ上のマルコフ連鎖として完全に分析できます。k = 1から7(5040 x 5040マトリックス!)の数値計算により、サイズの最大固有値(1および-1の後)は正確に。これはあなたが順列の符号を交互の問題に対処した後のエラー、(-1の固有値に対応する)ことを意味するすべての確率がレートで減衰またはもっと早く。これは、すべての大きなについて保持され続けると思われます。1 2 /(k3)/(k1)=12/(k1)(12/(k1))nk
whuber

1
確率は共役クラスで不変であるため、よりもはるかに良い結果が得られます。また、パーティションは個しかなく、代わりに行列を分析できます。15 7 15 × 155040×504015715×15
ダグラスザーレ

8

数をシャッフルすると無限になりがちなので、単純なアルゴリズムでカードを正しくシャッフルすると思います。

3枚のカードがあるとします:{A、B、C}。カードが次の順序で始まると仮定します:A、B、C。その後、1回シャッフルした後、次の組み合わせがあります。

{A,B,C}, {A,B,C}, {A,B,C} #You get this if choose the same RN twice.
{A,C,B}, {A,C,B}
{C,B,A}, {C,B,A}
{B,A,C}, {B,A,C}

したがって、カードAが位置{1,2,3}にある確率は{5/9、2/9、2/9}です。

カードを2回シャッフルする場合:

Pr(A in position 1 after 2 shuffles) = 5/9*Pr(A in position 1 after 1 shuffle) 
                                     + 2/9*Pr(A in position 2 after 1 shuffle) 
                                     + 2/9*Pr(A in position 3 after 1 shuffle) 

これにより、0.407が得られます。

同じ考えを使用して、繰り返しの関係を形成できます。

Pr(A in position 1 after n shuffles) = 5/9*Pr(A in position 1 after (n-1) shuffles) 
                                     + 2/9*Pr(A in position 2 after (n-1) shuffles) 
                                     + 2/9*Pr(A in position 3 after (n-1) shuffles).

これをRでコーディングすると(以下のコードを参照)、10回シャッフルした後、カードAが{0.3,334、0.33333、0.33333}の位置にある{1,2,3}の確率を与えます。

Rコード

## m is the probability matrix of card position
## Row is position
## Col is card A, B, C
m = matrix(0, nrow=3, ncol=3)
m[1,1] = 1; m[2,2] = 1; m[3,3] = 1

## Transition matrix
m_trans = matrix(2/9, nrow=3, ncol=3)
m_trans[1,1] = 5/9; m_trans[2,2] = 5/9; m_trans[3,3] = 5/9

for(i in 1:10){
  old_m = m
  m[1,1] = sum(m_trans[,1]*old_m[,1])
  m[2,1] = sum(m_trans[,2]*old_m[,1])
  m[3,1] = sum(m_trans[,3]*old_m[,1])

  m[1,2] = sum(m_trans[,1]*old_m[,2])
  m[2,2] = sum(m_trans[,2]*old_m[,2])
  m[3,2] = sum(m_trans[,3]*old_m[,2])

  m[1,3] = sum(m_trans[,1]*old_m[,3])
  m[2,3] = sum(m_trans[,2]*old_m[,3])
  m[3,3] = sum(m_trans[,3]*old_m[,3])
}  
m

1
+1。これは、特定のカードが特定の位置に到達する確率が、シャッフルの回数が増えるにつれて予想される比率に近づくことを示しています。ただし、配列をランダムに1回だけ回転させるアルゴリズムについても同じことが言えます。すべてのカードはすべての位置で終わる確率が等しくなりますが、ランダム性はまったくありません(配列はソートされたままです)。
ティロ

@Thilo:ごめんなさい、あなたのコメントをフォローしていません。「アルゴリズムはランダムな量だけ回転します」が、それでも「ランダム性なし」ですか?さらに説明してもらえますか?
-csgillespie

N要素の配列を0からN-1の位置の間で(ランダムに)回転させて「シャッフル」した場合、すべてのカードはNのいずれかの位置で終わる可能性がまったく同じですが、2は常に1の間にありますおよび3.
ティロ

1
@Thio:ああ、私はあなたのポイントを得る。さて、Pr(Aの位置2)およびPr(Aの位置3)の確率(上記とまったく同じ考え方を使用)を計算できます-カードBおよびCのditoです。すべての確率が傾向があることがわかります。 1/3。注:私の答えは特定のケースのみを示しますが、@ whuberの良い答えは一般的なケースを示します。
csgillespie

4

完全に均一な分布が得られないことを確認する1つの方法は、割り切れることです。一様分布では、各順列の確率はt個のランダムな転置のシーケンスを生成し、その積によってシーケンスを収集すると、得られる確率は整数Aに対してA / n 2 tの形式になります。1 / nの場合= A / n 2 t、次にn 2 t / n = A1/n!tA/n2tA1/n!=A/n2tn2t/n!=A。用ベルトランの仮説(定理)により分裂しない分母に生じ、素数が存在するnがので、N 2 T / N は整数ではなく、転置をnに均等に分割する方法はありません順列。たとえば、n = 52の場合、1 / 52の分母割り切れる3 5 7 47ながら1 /の分母n3nn2t/n!n!n=521/52!3,5,7,...,47ので、ではありません A / 52 2 tはに減らすことはできません 1 / 52 1/522tA/522t1/52!

ランダム置換をうまく近似するには何個必要ですか?ランダムな転置によるランダムな順列の生成は、DiaconisとShahshahaniによって、対称グループの表現理論を使用して分析されました。

Diaconis、P.、Shahshahani、M.(1981):「ランダムな転置によるランダムな順列の生成」。Z.ワルシュ。うわー ゲブ。57、159–179。

1つの結論は、それがかかるということでした1ϵ1の後という意味での 2 nlognの転置12nlogn順列はランダムとは程遠いが、1+ϵ1の後(1ϵ)12nlogn、合計変動とL2距離の両方の意味で結果はランダムに近くなります。このタイプのカットオフ現象は、グループでのランダムウォークで一般的であり、デッキがランダムに近づく前に7回のリフルシャッフルが必要であるという有名な結果に関連しています。(1+ϵ)12nlognL27


2

私は統計学者ではないことを心に留めておいてください、しかし、私は私の2セントを入れます。

私はRで少しテストを行いました(注意してください、それはhighのために非常に遅いですnumTrials、コードはおそらく最適化できます):

numElements <- 1000
numTrials <- 5000

swapVec <- function()
    {
    vec.swp <- vec

    for (i in 1:numElements)
        {
        i <- sample(1:numElements)
        j <- sample(1:numElements)

        tmp <- vec.swp[i]
        vec.swp[i] <- vec.swp[j]
        vec.swp[j] <- tmp
        }

    return (vec.swp)
    }

# Create a normally distributed array of numElements length
vec <- rnorm(numElements)

# Do several "swapping trials" so we can make some stats on them
swaps <- vec
prog <- txtProgressBar(0, numTrials, style=3)

for (t in 1:numTrials)
    {
    swaps <- rbind(swaps, swapVec())
    setTxtProgressBar(prog, t)
    }

これによりswapsnumTrials+1行(試行ごとに1つ+元の行)numElementsと列(ベクトル要素ごとに1つ)の行列が生成されます。メソッドが正しい場合、各列の分布(つまり、試行全体の各要素の値)は、元のデータの分布と異なるべきではありません。

元のデータは正規分布であるため、すべての列がそこから逸脱しないことが期待されます。

走れば

par(mfrow= c(2,2))
# Our original data
hist(swaps[1,], 100, col="black", freq=FALSE, main="Original")
# Three "randomly" chosen columns
hist(swaps[,1], 100, col="black", freq=FALSE, main="Trial # 1") 
hist(swaps[,257], 100, col="black", freq=FALSE, main="Trial # 257")
hist(swaps[,844], 100, col="black", freq=FALSE, main="Trial # 844")

我々が得る:

Histograms of random trials

とても有望に見えます。さて、分布が元の分布から逸脱していないことを統計的に確認したい場合、コルモゴロフ-スミルノフ検定を使用できると思います(統計学者はこれが正しいことを確認できますか?)

ks.test(swaps[1, ], swaps[, 234])

これはp = 0.9926を与えます

すべての列をチェックする場合:

ks.results <- apply(swaps, 2, function(col){ks.test(swaps[1,], col)})
p.values <- unlist(lapply(ks.results, function(x){x$p.value})

そして、私たちは走ります

hist(p.values, 100, col="black")

我々が得る:

Histogram of Kolmogorov-Smirnov test p values

したがって、配列の要素の大部分について、四分位数も見ることができるように、スワップメソッドは良い結果をもたらしました。

1> quantile(p.values)
       0%       25%       50%       75%      100% 
0.6819832 0.9963731 0.9999188 0.9999996 1.0000000

明らかに、試行回数が少ない場合、状況はそれほど良くないことに注意してください。

50回の試行

1> quantile(p.values)
          0%          25%          50%          75%         100% 
0.0003399635 0.2920976389 0.5583204486 0.8103852744 0.9999165730

100回の試行

          0%         25%         50%         75%        100% 
 0.001434198 0.327553996 0.596603804 0.828037097 0.999999591 

500回の試行

         0%         25%         50%         75%        100% 
0.007834701 0.504698404 0.764231550 0.934223503 0.999995887 

0

擬似コードで、私はあなたのアルゴリズムをどのように解釈しているのですか?

void shuffle(array, length, num_passes)
  for (pass = 0; pass < num_passes; ++pass) 
    for (n = 0; n < length; ++)
      i = random_in(0, length-1)
      j = random_in(0, lenght-1)
      swap(array[i], array[j]

2×length×num_passes[0,length1]length

length2×length×num_passes

length!length!<length2×length×num_passes

length!|length2×length×num_passes

pp<lengthplengthlength>2p|length!length2×length×num_passeslength!length2×length×num_passeslength>2

lengthp<lengthlength1length1length

これをFisher-Yatesと比較してください。最初の反復では、次の中から選択しますlengthオプション。2番目の反復にはlength1オプションなど。言い換えれば、あなたが持っているlength トレース、および length|length。各トレースが異なる順列をもたらすことを示すことは難しくなく、そこからフィッシャー・イェイツが各順列を等しい確率で生成することは簡単にわかります。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.