Выберите K случайных элементов из списка, элементы которого имеют веса
выбор без каких-либо Весов (равные вероятности) прекрасно описывается здесь.
Мне было интересно, есть ли способ преобразовать этот подход в взвешенный.
меня также интересуют и другие подходы.
Обновление: Отбор Проб без замена
13 ответов:
Я знаю, что это очень старый вопрос, но я думаю, что есть аккуратный трюк, чтобы сделать это в O(n) времени, если вы примените немного математики!
на экспоненциальное распределение имеет два очень полезных свойства.
учитывая n выборок из различных экспоненциальных распределений с различными параметрами скорости, вероятность того, что данная выборка является минимальной, равна ее параметру скорости, деленному на сумму всех скоростей параметры.
это "без памяти". Поэтому, если вы уже знаете минимум, то вероятность того, что любой из оставшихся элементов является 2-м по мин, равна вероятности того, что если бы истинная мин была удалена (и никогда не создавалась), этот элемент был бы новым min. Это кажется очевидным, но я думаю, что из-за некоторых проблем с условной вероятностью это может быть неверно для других распределений.
, используя факт 1, мы знаем, что выбор одного элемента может быть сделан путем генерации этих образцов экспоненциального распределения с параметром скорости, равным весу, а затем выбора одного с минимальным значением.
используя факт 2, мы знаем, что нам не нужно повторно генерировать экспоненциальные выборки. Вместо этого просто создайте один для каждого элемента и возьмите k элементов с самыми низкими выборками.
найти самый низкий k можно сделать в O (n). Используйте Quickselect алгоритм поиска k-го элемента, затем просто сделайте еще один проход через все элементы и выведите все ниже k-го.
полезное примечание: если у вас нет непосредственного доступа к библиотеке для создания образцов экспоненциального распределения, это можно легко сделать:
-ln(rand())/weight
если выборка с заменой, вы можете использовать этот алгоритм (реализованный здесь в Python):
import random items = [(10, "low"), (100, "mid"), (890, "large")] def weighted_sample(items, n): total = float(sum(w for w, v in items)) i = 0 w, v = items[0] while n: x = total * (1 - random.random() ** (1.0 / n)) total -= x while x > w: x -= w i += 1 w, v = items[i] w -= x yield v n -= 1
Это O (n + m), где m - это количество элементов.
почему это работает? он основан на следующем алгоритме:
def n_random_numbers_decreasing(v, n): """Like reversed(sorted(v * random() for i in range(n))), but faster because we avoid sorting.""" while n: v *= random.random() ** (1.0 / n) yield v n -= 1
функции
weighted_sample
просто этот алгоритм слился с прогулкойitems
список, чтобы выбрать элементы, выбранные этими случайные цифры.это в свою очередь работает, потому что вероятность того, что n случайные числа 0..v все будет меньше, чем z и P = (z / v)n. Решите для z, а вы z = vP1/n. Подставляя случайное число для P выбирает наибольшее число с правильным распределение; и мы можем просто повторить процесс, чтобы выбрать все остальные числа.
если выборка без замены, вы можете поместить все элементы в двоичную кучу, где каждый узел кэширует общее количество весов всех элементов в этом подзаголовке. Построение кучи - Это O (m). Выбрать случайный элемент из кучи, соблюдая гири, является o(зарегистрируйте m). Удаление этого элемента и обновление кэшированных итогов также O (log m). Так что вы можете выбрать n элементы в O (m + n log m) времени.
(Примечание: "вес" здесь означает, что каждый раз, когда элемент выбран, остальные возможности выбираются с вероятностью, пропорциональной их весу. Это не значит, что элементы появляются на выходе с вероятностью, пропорциональной их весу.)
вот реализация этого обильно прокомментировал:
import random class Node: # Each node in the heap has a weight, value, and total weight. # The total weight, self.tw, is self.w plus the weight of any children. __slots__ = ['w', 'v', 'tw'] def __init__(self, w, v, tw): self.w, self.v, self.tw = w, v, tw def rws_heap(items): # h is the heap. It's like a binary tree that lives in an array. # It has a Node for each pair in `items`. h[1] is the root. Each # other Node h[i] has a parent at h[i>>1]. Each node has up to 2 # children, h[i<<1] and h[(i<<1)+1]. To get this nice simple # arithmetic, we have to leave h[0] vacant. h = [None] # leave h[0] vacant for w, v in items: h.append(Node(w, v, w)) for i in range(len(h) - 1, 1, -1): # total up the tws h[i>>1].tw += h[i].tw # add h[i]'s total to its parent return h def rws_heap_pop(h): gas = h[1].tw * random.random() # start with a random amount of gas i = 1 # start driving at the root while gas >= h[i].w: # while we have enough gas to get past node i: gas -= h[i].w # drive past node i i <<= 1 # move to first child if gas >= h[i].tw: # if we have enough gas: gas -= h[i].tw # drive past first child and descendants i += 1 # move to second child w = h[i].w # out of gas! h[i] is the selected node. v = h[i].v h[i].w = 0 # make sure this node isn't chosen again while i: # fix up total weights h[i].tw -= w i >>= 1 return v def random_weighted_sample_no_replacement(items, n): heap = rws_heap(items) # just make a heap... for i in range(n): yield rws_heap_pop(heap) # and pop n items off it.
если выборка с заменой, используйте выбор колеса рулетки техника (часто используется в генетических алгоритмах):
- сортировать Весов
- вычислить суммарный вес
- выберите случайное число в
[0,1]*totalWeight
- найти интервал, в котором это число попадает в
- выберите элементы с соответствующим интервалом
- повторить
k
разесли выборка без замены, вы можете адаптировать вышеуказанный метод, удалив выбранный элемент из списка после каждой итерации, а затем повторно нормализовать веса так, чтобы их сумма равнялась 1 (действительная функция распределения вероятности)
Я сделал это в Ruby
https://github.com/fl00r/pickup
require 'pickup' pond = { "selmon" => 1, "carp" => 4, "crucian" => 3, "herring" => 6, "sturgeon" => 8, "gudgeon" => 10, "minnow" => 20 } pickup = Pickup.new(pond, uniq: true) pickup.pick(3) #=> [ "gudgeon", "herring", "minnow" ] pickup.pick #=> "herring" pickup.pick #=> "gudgeon" pickup.pick #=> "sturgeon"
Если вы хотите генерировать большие массивы случайных чисел С заменой, вы можете использовать кусочно-линейную интерполяцию. Например, используя NumPy / SciPy:
import numpy import scipy.interpolate def weighted_randint(weights, size=None): """Given an n-element vector of weights, randomly sample integers up to n with probabilities proportional to weights""" n = weights.size # normalize so that the weights sum to unity weights = weights / numpy.linalg.norm(weights, 1) # cumulative sum of weights cumulative_weights = weights.cumsum() # piecewise-linear interpolating function whose domain is # the unit interval and whose range is the integers up to n f = scipy.interpolate.interp1d( numpy.hstack((0.0, weights)), numpy.arange(n + 1), kind='linear') return f(numpy.random.random(size=size)).astype(int)
Это не эффективно, если вы хотите попробовать без замены.
вот реализация Go от geodns:
package foo import ( "log" "math/rand" ) type server struct { Weight int data interface{} } func foo(servers []server) { // servers list is already sorted by the Weight attribute // number of items to pick max := 4 result := make([]server, max) sum := 0 for _, r := range servers { sum += r.Weight } for si := 0; si < max; si++ { n := rand.Intn(sum + 1) s := 0 for i := range servers { s += int(servers[i].Weight) if s >= n { log.Println("Picked record", i, servers[i]) sum -= servers[i].Weight result[si] = servers[i] // remove the server from the list servers = append(servers[:i], servers[i+1:]...) break } } } return result }
Если вы хотите выбрать x элементов из взвешенного набора без замены таким образом, что элементы выбираются с вероятностью, пропорциональной их Весам:
import random def weighted_choose_subset(weighted_set, count): """Return a random sample of count elements from a weighted set. weighted_set should be a sequence of tuples of the form (item, weight), for example: [('a', 1), ('b', 2), ('c', 3)] Each element from weighted_set shows up at most once in the result, and the relative likelihood of two particular elements showing up is equal to the ratio of their weights. This works as follows: 1.) Line up the items along the number line from [0, the sum of all weights) such that each item occupies a segment of length equal to its weight. 2.) Randomly pick a number "start" in the range [0, total weight / count). 3.) Find all the points "start + n/count" (for all integers n such that the point is within our segments) and yield the set containing the items marked by those points. Note that this implementation may not return each possible subset. For example, with the input ([('a': 1), ('b': 1), ('c': 1), ('d': 1)], 2), it may only produce the sets ['a', 'c'] and ['b', 'd'], but it will do so such that the weights are respected. This implementation only works for nonnegative integral weights. The highest weight in the input set must be less than the total weight divided by the count; otherwise it would be impossible to respect the weights while never returning that element more than once per invocation. """ if count == 0: return [] total_weight = 0 max_weight = 0 borders = [] for item, weight in weighted_set: if weight < 0: raise RuntimeError("All weights must be positive integers") # Scale up weights so dividing total_weight / count doesn't truncate: weight *= count total_weight += weight borders.append(total_weight) max_weight = max(max_weight, weight) step = int(total_weight / count) if max_weight > step: raise RuntimeError( "Each weight must be less than total weight / count") next_stop = random.randint(0, step - 1) results = [] current = 0 for i in range(count): while borders[current] <= next_stop: current += 1 results.append(weighted_set[current][0]) next_stop += step return results
в вопросе, который вы связали, решение Кайла будет работать с тривиальным обобщением. Сканируйте список и суммируйте общие веса. Тогда вероятность выбрать элемент должна быть:
1 - (1 - (#требуется/(вес слева)))/(вес при n). После посещения узла, вычтите его вес из общего. Кроме того, если вам нужно n и осталось n, вы должны остановиться явно.
вы можете проверить, что все, что имеет вес 1, это упрощает для Кайла решение.
отредактировано: (пришлось переосмыслить то, что в два раза вероятнее)
это делает именно это с O(n) и без избыточного использования памяти. Я считаю, что это умное и эффективное решение на любом языке. Первые две строки предназначены только для заполнения выборочных данных в Drupal.
function getNrandomGuysWithWeight($numitems){ $q = db_query('SELECT id, weight FROM theTableWithTheData'); $q = $q->fetchAll(); $accum = 0; foreach($q as $r){ $accum += $r->weight; $r->weight = $accum; } $out = array(); while(count($out) < $numitems && count($q)){ $n = rand(0,$accum); $lessaccum = NULL; $prevaccum = 0; $idxrm = 0; foreach($q as $i=>$r){ if(($lessaccum == NULL) && ($n <= $r->weight)){ $out[] = $r->id; $lessaccum = $r->weight- $prevaccum; $accum -= $lessaccum; $idxrm = $i; }else if($lessaccum){ $r->weight -= $lessaccum; } $prevaccum = $r->weight; } unset($q[$idxrm]); } return $out; }
я помещаю здесь простое решение для выбора 1 элемента, вы можете легко развернуть его для k элементов (стиль Java):
double random = Math.random(); double sum = 0; for (int i = 0; i < items.length; i++) { val = items[i]; sum += val.getValue(); if (sum > random) { selected = val; break; } }
я реализовал тот же алгоритм, что идея Джейсон Orendorff в Руст здесь. Моя версия дополнительно поддерживает массовые операции: вставка и удаление (когда вы хотите удалить кучу элементов, заданных их идентификаторами, а не через взвешенный путь выбора) из структуры данных в
O(m + log n)
время, где m-количество элементов для удаления и n-количество элементов в хранении.
выборка wihout замена с рекурсией-элегантное и очень короткое решение в c#
//сколькими способами можно выбрать 4 из 60 студентов, так что каждый раз мы выбираем разные 4
class Program { static void Main(string[] args) { int group = 60; int studentsToChoose = 4; Console.WriteLine(FindNumberOfStudents(studentsToChoose, group)); } private static int FindNumberOfStudents(int studentsToChoose, int group) { if (studentsToChoose == group || studentsToChoose == 0) return 1; return FindNumberOfStudents(studentsToChoose, group - 1) + FindNumberOfStudents(studentsToChoose - 1, group - 1); } }