Выберите K случайных элементов из списка, элементы которого имеют веса


выбор без каких-либо Весов (равные вероятности) прекрасно описывается здесь.

Мне было интересно, есть ли способ преобразовать этот подход в взвешенный.

меня также интересуют и другие подходы.

Обновление: Отбор Проб без замена

13 65

13 ответов:

Я знаю, что это очень старый вопрос, но я думаю, что есть аккуратный трюк, чтобы сделать это в O(n) времени, если вы примените немного математики!

на экспоненциальное распределение имеет два очень полезных свойства.

  1. учитывая n выборок из различных экспоненциальных распределений с различными параметрами скорости, вероятность того, что данная выборка является минимальной, равна ее параметру скорости, деленному на сумму всех скоростей параметры.

  2. это "без памяти". Поэтому, если вы уже знаете минимум, то вероятность того, что любой из оставшихся элементов является 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.

если выборка с заменой, используйте выбор колеса рулетки техника (часто используется в генетических алгоритмах):

  1. сортировать Весов
  2. вычислить суммарный вес
  3. выберите случайное число в [0,1]*totalWeight
  4. найти интервал, в котором это число попадает в
  5. выберите элементы с соответствующим интервалом
  6. повторить k раз

alt text

если выборка без замены, вы можете адаптировать вышеуказанный метод, удалив выбранный элемент из списка после каждой итерации, а затем повторно нормализовать веса так, чтобы их сумма равнялась 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);

    }
}

я использовал ассоциативную карту (вес,объекта). например:

{
(10,"low"),
(100,"mid"),
(10000,"large")
}

total=10110

подсмотрите случайное число между 0 и "total" и повторите по клавишам, пока это число не поместится в заданный диапазон.