加重シャッフルの実装方法


22

私は最近、非常に非効率的だと思ったコードをいくつか書きましたが、含まれている値が少ないため、受け入れました。しかし、私はまだ次のより良いアルゴリズムに興味があります:

  1. Xオブジェクトのリスト。各オブジェクトには「重み」が割り当てられます
  2. 重みを合計する
  3. 0から合計までの乱数を生成します
  4. オブジェクトを反復処理し、合計が非正になるまで合計から重みを減算します
  5. リストからオブジェクトを削除してから、新しいリストの最後に追加します

項目2、4、および5はすべてn時間がかかるため、O(n^2)アルゴリズムです。

これは改善できますか?

重み付きシャッフルの例として、要素はより高い重みで前面にいる可能性が高くなります。

例(実際に乱数を生成します):

重みが6,5,4,3,2,1の6つのオブジェクト。合計は21

私は19を選びました:19-6-5-4-3-2 = -1したがって、2が最初の位置に移動し、重みは6,5,4,3,1になりました。合計は19

16:を選んだ16-6-5-4-3 = -2ので、3番目が2番目の位置になり、重みは6,5,4,1になりました。合計は16

3を選択しました。3-6 = -3したがって、6は3番目の位置になり、重みは5,4,1になりました。合計は10

8:を選んだ8-5-4 = -1ので、4は4番目の位置になり、重みは5,1になりました。合計は6

5:を選んだ5-5=0ので、5は5番目の位置になり、重みは1になりました。合計は1

私は1を選んだ1-1=0ので、1が最後の位置に移動し、重みがなくなり、終了します


6
加重シャッフルとは正確には何ですか?重量が大きいほど、オブジェクトがデッキの上部にある可能性が高いということですか?
ドーバル14年

好奇心から、ステップ(5)の目的は何ですか。リストが静的な場合、これを改善する方法があります。
ロボットをゲット14

はい、ドーバル。リストからアイテムを削除して、シャッフルされたリストに複数回表示されないようにします。
ネイサンメリル14年

リスト内のアイテムの重量は一定ですか?

1つのアイテムの重量は他のアイテムよりも大きくなりますが、アイテムXの重量は常に同じです。(明らかに、アイテムを削除すると、より大きな重量が比例して大きくなります)
ネイサンメリル14年

回答:


14

これはO(n log(n))、ツリーを使用して実装できます。

最初に、各ノードの右側と左側のすべての子孫ノードの累積合計を保持しながら、ツリーを作成します。

アイテムをサンプリングするには、ルートノードから再帰的にサンプリングし、累積合計を使用して、現在のノード、左のノード、または右のノードのいずれを返すかを決定します。ノードをサンプリングするたびに、その重みをゼロに設定し、親ノードも更新します。

これはPythonでの私の実装です:

import random

def weigthed_shuffle(items, weights):
    if len(items) != len(weights):
        raise ValueError("Unequal lengths")

    n = len(items)
    nodes = [None for _ in range(n)]

    def left_index(i):
        return 2 * i + 1

    def right_index(i):
        return 2 * i + 2

    def total_weight(i=0):
        if i >= n:
            return 0
        this_weigth = weights[i]
        if this_weigth <= 0:
            raise ValueError("Weigth can't be zero or negative")
        left_weigth = total_weight(left_index(i))
        right_weigth = total_weight(right_index(i))
        nodes[i] = [this_weigth, left_weigth, right_weigth]
        return this_weigth + left_weigth + right_weigth

    def sample(i=0):
        this_w, left_w, right_w = nodes[i]
        total = this_w + left_w + right_w
        r = total * random.random()
        if r < this_w:
            nodes[i][0] = 0
            return i
        elif r < this_w + left_w:
            chosen = sample(left_index(i))
            nodes[i][1] -= weights[chosen]
            return chosen
        else:
            chosen = sample(right_index(i))
            nodes[i][2] -= weights[chosen]
            return chosen

    total_weight() # build nodes tree

    return (items[sample()] for _ in range(n - 1))

使用法:

In [2]: items = list(range(10))
   ...: weights = list(range(10, 0, -1))
   ...:

In [3]: for _ in range(10):
   ...:     print(list(weigthed_shuffle(items, weights)))
   ...:
[5, 0, 8, 6, 7, 2, 3, 1, 4]
[1, 2, 5, 7, 3, 6, 9, 0, 4]
[1, 0, 2, 6, 8, 3, 7, 5, 4]
[4, 6, 8, 1, 2, 0, 3, 9, 7]
[3, 5, 1, 0, 4, 7, 2, 6, 8]
[3, 7, 1, 2, 0, 5, 6, 4, 8]
[1, 4, 8, 2, 6, 3, 0, 9, 5]
[3, 5, 0, 4, 2, 6, 1, 8, 9]
[6, 3, 5, 0, 1, 2, 4, 8, 7]
[4, 1, 2, 0, 3, 8, 6, 5, 7]

weigthed_shuffleジェネレータであるため、上位のkアイテムを効率的にサンプリングできます。配列全体をシャッフルする場合は、(list関数を使用して)枯渇するまでジェネレーターを反復処理します。

更新:

重み付けランダムサンプリング(2005; Efraimidis、Spirakis)は、このための非常にエレガントなアルゴリズムを提供します。実装は非常にシンプルで、以下で実行されO(n log(n))ます:

def weigthed_shuffle(items, weights):
    order = sorted(range(len(items)), key=lambda i: -random.random() ** (1.0 / weights[i]))
    return [items[i] for i in order]

最後の更新は、間違ったワンライナーソリューションに不気味に似ています。それは正しいですか?
ジャコモアルゼッタ

19

編集:この答えは、予想される方法で重みを解釈しません。つまり、重量が2のアイテムは、重量が1のアイテムの1倍になる可能性は高くありません。

リストをシャッフルする1つの方法は、リスト内の各要素に乱数を割り当て、それらの番号でソートすることです。この考えを拡張することができます。重み付き乱数を選択するだけです。たとえば、を使用できますrandom() * weight。異なる選択により、異なる分布が生成されます。

Pythonのようなものでは、これは次のように単純でなければなりません。

items.sort(key = lambda item: random.random() * item.weight)

キーが異なる値になってしまうため、キーを複数回評価しないように注意してください。


2
これは、その単純さのために正直に天才です。nlognソートアルゴリズムを使用していると仮定すると、これはうまく機能するはずです。
ネイサンメリル14年

重りの重さはどれくらいですか?高い場合、オブジェクトは単純に重量でソートされます。それらが低い場合、オブジェクトは重量に応じてわずかな摂動のみでほぼランダムです。いずれにせよ、私はいつもこの方法を使用しましたが、ソート位置の計算にはおそらく微調整が必​​要になるでしょう。
david.pfx 14年

@ david.pfx重みの範囲は、乱数の範囲でなければなりません。そのようmax*min = min*maxにして、したがってあらゆる順列が可能ですが、いくつかはより可能性が高いです(特に重みが均一に分散されていない場合)
ネイサンメリル14年

2
実際、このアプローチは間違っています!ウェイト75と25を想像してください。75の場合、2/3の時間で25より大きい数字を選択します。残りの1/3の時間では、25の50%を「ビート」します。75は最初の2/3 +(1/3 * 1/2)の83%です。まだ修正されていません。
アダムラブン14年

1
このソリューションは、ランダムサンプリングの一様分布を指数分布に置き換えることで機能します。
P-Gn 14

5

最初に、ソートされるリスト内の指定された要素の重みが一定であることから作業してみましょう。反復間で変化することはありません。もしそうなら、それで...まあ、それはより大きな問題です。

例として、前面のカードに重みを付けたいカードのデッキを使用します。 weight(card) = card.rank。これらを合計すると、重みの分布がわからない場合、実際には一度O(n)になります。

これらの要素は、インデックス可能なオブジェクトの変更などのソートされた構造に格納されます 、特定のノードからレベルのすべてのインデックスにアクセスできるようスキップリストのます。

   1 10
 o ---> o -------------------------------------------- -------------> oトップレベル
   1 3 2 5
 o ---> o ---------------> o ---------> o ---------------- -----------> oレベル3
   1 2 1 2 5
 o ---> o ---------> o ---> o ---------> o ----------------- ----------> oレベル2
   1 1 1 1 1 1 1 1 1 1 1 1 
 o ---> o ---> o ---> o ---> o ---> o ---> o ---> o ---> o ---> o ---> o ---> o最下位レベル

ヘッド1番目2番目3番目4番目5番目6番目7番目8番目9番目10番目NIL
      ノードノードノードノードノードノードノードノードノードノードノード

ただし、この場合、各ノードはその重量と同じだけのスペースを「占有」します。

このリストでカードを検索すると、O(log n)時間でリスト内のカードの位置にアクセスし、O(1)時間で関連リストから削除できます。OK、O(1)ではなく、O(log log n)の時間かもしれません(これについてもっと考えなければなりません)。上記の例で6番目のノードを削除するには、4つのレベルすべてを更新する必要があります。これらの4つのレベルは、リストにある要素の数に依存しません(レベルの実装方法によって異なります)。

要素の重みは一定であるsum -= weight(removed)ため、構造を再度トラバースすることなく簡単に実行できます。

したがって、O(n)の1回限りのコストとO(log n)のルックアップ値、およびO(1)のリストからの削除コストがあります。これはO(n)+ n * O(log n)+ n * O(1)になり、O(n log n)の全体的なパフォーマンスが得られます。


カードでこれを見てみましょう。これは上記で使用したものです。

      10
トップ3 -----------------------> 4d
                                。
       3 7。
    2 ---------> 2d ---------> 4d
                  。。
       1 2。3 4。
ボット1->広告-> 2d-> 3d-> 4d

これは非常に小さなデッキで、カードは4枚しかありませ。これをどのように拡張できるかは簡単にわかるはずです。52枚のカードで理想的な構造には6つのレベルがあります(log 2(52)〜= 6)。ただし、スキップリストを掘り下げると、さらに小さい数に減らすことができます。

すべての重みの合計は10です。したがって、[1 .. 10)とその4から乱数を取得します。 スキップリストを調べて、ceiling(4)にあるアイテムを見つけます。4は10未満なので、トップレベルから2番目のレベルに移動します。4は3より大きいので、2つのダイヤモンドになりました。4は3 + 7未満であるため、下のレベルに移動し、4は3 + 3未満であるため、3つのダイヤモンドがあります。

構造から3つのダイヤモンドを削除すると、構造は次のようになります。

       7
トップ3 ----------------> 4d
                         。
       3 4。
    2 ---------> 2d-> 4d
                  。。
       1 2。4。
ボット1->広告-> 2d-> 4d

ノードは、構造内の重みに比例した量の「スペース」を占めることに注意してください。これにより、加重選択が可能になります。

これはバランスの取れたバイナリツリーに近いため、このルックアップでは最下層(O(n))を歩く必要はなく、代わりに最上部から構造をすばやくスキップして、探しているものを見つけることができますために。

これの多くは、代わりに何らかのバランスの取れたツリーで実行できます。問題は、ノードが削除されたときに構造が再調整されることです。これは古典的なツリー構造ではなく、4つのダイヤモンドが[6 7 8 9]から[3 4 5 6]は、ツリー構造の利点よりも費用がかかる場合があります。

ただし、スキップリストはO(log n)の時間でリストをスキップできるという点でバイナリツリーに似ていますが、代わりにリンクリストで作業するという単純さがあります。

これは、すべてを簡単に実行できると言うことではありません(要素を削除するときに変更する必要があるすべてのリンクにタブを維持する必要があります)が、所有している多くのレベルとそのリンクを更新するだけです適切なツリー構造の右側のすべてよりも。


私は必ず一致にスキップリストを記述しているか何じゃない(が、その後、私はなかっただけで、スキップリストを調べます)。ウィキペディアで私が理解していることからすると、重みが高いほど、重みが低いよりも右になります。ただし、スキップの幅は重量にする必要があると説明しています。もう1つの質問...この構造を使用して、ランダムな要素をどのように選択しますか?
ネイサンメリル14年

1
したがって、@ MrTi は、インデックス付け可能なスキップリストの概念の変更です。重要なのは、前の要素の重みがO(n)時間ではなくO(log n)時間で<23になるまでに要素にアクセスできるようにすることです。まだ説明した方法でランダム要素を選択し、[0、sum(weights)]から乱数を選択してから、リストから対応する要素を取得します。スキップリスト内のノード/カードの順序は関係ありません-より重い重み付きアイテムが占める大きな「スペース」がキーになるためです。

ああ、わかりました。私はそれが好きです。
ネイサンメリル14年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.