Выберите случайный элемент массива, удовлетворяющий определенному свойству
Предположим, у меня есть список, называемый elements
, каждый из которых выполняет или не удовлетворяет некоторому логическому свойству p
. Я хочу выбрать один из элементов, который удовлетворяет p
случайным с равномерным распределением. Я не знаю заранее, сколько элементов удовлетворяют этому свойству p
.
Будет ли следующий код:
pickRandElement(elements, p)
randElement = null
count = 0
foreach element in elements
if (p(element))
count = count + 1
if (randInt(count) == 0)
randElement = element
return randElement
(randInt(n)
возвращает случайный int k
с 0 <= k < n
.)
Ответы
Ответ 1
Это работает математически. Может быть доказано индукцией.
Ясно работает для элемента n = 1, удовлетворяющего p.
Для n + 1 элементов мы выберем элемент n + 1 с вероятностью 1/(n + 1), поэтому его вероятность ОК. Но как это влияет на конечную вероятность выбора одного из предыдущих n элементов?
Каждый из предыдущих n имел шанс быть выбранным с вероятностью 1/n, пока мы не найдем элемент n + 1. Теперь, после нахождения n + 1, существует вероятность 1/(n + 1), что элемент n + 1 выбран, поэтому существует вероятность n/(n + 1), что выбранный ранее элемент остается выбранным. Это означает, что его конечная вероятность быть выбранным после n + 1 найденных значений равна 1/n * (n/n + 1) = 1/n + 1 - это вероятность, которую мы хотим для всех n + 1 элементов для равномерного распределения.
Если он работает для n = 1, и он работает для n + 1, заданного n, то он работает для всех n.
Ответ 2
Да, я так считаю.
В первый раз, когда вы сталкиваетесь с соответствующим элементом, вы определенно выбираете его. В следующий раз вы выберете новое значение с вероятностью 1/2, чтобы каждый из двух элементов имел равные шансы. В следующий раз вы выбираете новое значение с вероятностью 1/3, оставляя каждый из других элементов с вероятностью 1/2 * 2/3 = 1/3.
Я пытаюсь найти статью в Википедии об этой стратегии, но пока не удается...
Обратите внимание, что в более общем плане вы просто выбираете случайную выборку из последовательности неизвестной длины. Ваша последовательность генерируется, беря начальную последовательность и фильтруя ее, но алгоритм не требует этой части вообще.
Я думал, что для этого в LINK-операторе в MoreLINQ мне понадобится оператор LINQ, но я не могу найти его в репозитории... EDIT: К счастью, он все еще существует из this ответить:
public static T RandomElement<T>(this IEnumerable<T> source,
Random rng)
{
T current = default(T);
int count = 0;
foreach (T element in source)
{
count++;
if (rng.Next(count) == 0)
{
current = element;
}
}
if (count == 0)
{
throw new InvalidOperationException("Sequence was empty");
}
return current;
}
Ответ 3
В Практике программирования, стр. 70, (Алгоритм цепи Маркова) существует аналогичный алгоритм для этого:
[...]
nmatch = 0;
for ( /* iterate list */ )
if (rand() % ++nmatch == 0) /* prob = 1/nmatch */
w = suf->word;
[...]
"Обратите внимание на алгоритм выбора одного случайным образом, когда мы не знаем, как многие предметы есть. Переменная nmatch подсчитывает количество совпадений как список проверяется. Выражение
rand() % ++nmatch == 0
увеличивает nmatch и в этом случае true с вероятностью 1/nmatch. "
Ответ 4
У decowboy есть хорошее доказательство того, что это работает на TopCoder
Ответ 5
Для большей ясности я бы постарался:
pickRandElement(elements, p)
OrderedCollection coll = new OrderedCollection
foreach element in elements
if (p(element))
coll.add(element)
if (coll.size == 0) return null
else return coll.get(randInt(coll.size))
Для меня это намного облегчает то, что вы пытаетесь сделать, и самодокументируетесь. Кроме того, он проще и элегантнее, и теперь очевидно, что каждый будет выбран с четным распределением.