これは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]