Выберите x случайных элементов из взвешенного списка в С# (без замены)
Обновление: моя проблема решена, я обновил исходный код в своем вопросе, чтобы ответить на вопрос Джейсона. Обратите внимание, что ответ rikitikitik решает проблему сбора карточек из образца с заменой.
Я хочу выбрать x случайных элементов из взвешенного списка. Отбор проб без замены. Я нашел этот ответ: qaru.site/info/45201/... с реализацией в Python. Я реализовал его на С# и протестировал его. Но результаты (как описано ниже) не соответствовали ожидаемым. Я не знаю Python, поэтому я уверен, что допустил ошибку при переносе кода на С#, но я не вижу, где, поскольку код в Pythong был действительно хорошо документирован.
Я выбрал одну карту 10000 раз, и это результаты, которые я получил (результат согласован по сравнению с исполнением):
Card 1: 18.25 % (10.00 % expected)
Card 2: 26.85 % (30.00 % expected)
Card 3: 46.22 % (50.00 % expected)
Card 4: 8.68 % (10.00 % expected)
Как вы можете видеть, карты 1 и 4 имеют как вес 1, так и карту 1, которая выбрана чаще, чем карта 4 (даже если я выбираю 2 или 3 карты).
Данные теста:
var cards = new List<Card>
{
new Card { Id = 1, AttributionRate = 1 }, // 10 %
new Card { Id = 2, AttributionRate = 3 }, // 30 %
new Card { Id = 3, AttributionRate = 5 }, // 50 %
new Card { Id = 4, AttributionRate = 1 }, // 10 %
};
Вот моя реализация в С#
public class CardAttributor : ICardsAttributor
{
private static Random random = new Random();
private List<Node> GenerateHeap(List<Card> cards)
{
List<Node> nodes = new List<Node>();
nodes.Add(null);
foreach (Card card in cards)
{
nodes.Add(new Node(card.AttributionRate, card, card.AttributionRate));
}
for (int i = nodes.Count - 1; i > 1; i--)
{
nodes[i>>1].TotalWeight += nodes[i].TotalWeight;
}
return nodes;
}
private Card PopFromHeap(List<Node> heap)
{
Card card = null;
int gas = random.Next(heap[1].TotalWeight);
int i = 1;
while (gas >= heap[i].Weight)
{
gas -= heap[i].Weight;
i <<= 1;
if (gas >= heap[i].TotalWeight)
{
gas -= heap[i].TotalWeight;
i += 1;
}
}
int weight = heap[i].Weight;
card = heap[i].Value;
heap[i].Weight = 0;
while (i > 0)
{
heap[i].TotalWeight -= weight;
i >>= 1;
}
return card;
}
public List<Card> PickMultipleCards(List<Card> cards, int cardsToPickCount)
{
List<Card> pickedCards = new List<Card>();
List<Node> heap = GenerateHeap(cards);
for (int i = 0; i < cardsToPickCount; i++)
{
pickedCards.Add(PopFromHeap(heap));
}
return pickedCards;
}
}
class Node
{
public int Weight { get; set; }
public Card Value { get; set; }
public int TotalWeight { get; set; }
public Node(int weight, Card value, int totalWeight)
{
Weight = weight;
Value = value;
TotalWeight = totalWeight;
}
}
public class Card
{
public int Id { get; set; }
public int AttributionRate { get; set; }
}
Ответы
Ответ 1
В программе есть две незначительные ошибки. Во-первых, диапазон случайного числа должен быть в точности равен общему весу всех предметов:
int gas = random.Next(heap[1].TotalWeight);
Во-вторых, измените оба места, где говорится gas >
, чтобы сказать gas >=
.
(Исходный код Python в порядке, потому что gas
- число с плавающей запятой, поэтому разница между >
и >=
незначительна. Этот код был написан для принятия либо целых, либо с плавающей запятой.)
Обновление: ОК, вы внесли рекомендуемые изменения в свой код. Я думаю, что теперь код правильный!
Ответ 2
Как некоторые люди упомянули в комментариях, создайте список карт в той точной пропорции, которую вы хотите:
var deck = new List<Card>();
cards.ForEach(c =>
{
for(int i = 0; i < c.AttributionRate; i++)
{
deck.Add(c);
}
}
В случайном порядке:
deck = deck.OrderBy(c => Guid.NewGuid()).ToList();
И выберите x карты:
var hand = deck.Take(x)
Конечно, это работает только в том случае, если AttributionRate
является int
. В противном случае вам придется немного поработать с колодой.
Я получаю следующие результаты для 10 000 прогонов, принимающих 5 за раз:
Card 1: 9.932%
Card 2: 30.15%
Card 3: 49.854%
Card 4: 10.064%
Другой результат:
Card 1: 10.024%
Card 2: 30.034%
Card 3: 50.034%
Card 4: 9.908%
EDIT:
Я отважился на побитовые операции, и я взглянул на ваш код. После добавления на мой жареный мозг щедрый соус для барбекю, я заметил несколько вещей:
Во-первых, Random.Next(min,max)
будет включать min в случайном пуле, но не max. Это является причиной более высокой, чем ожидалось, вероятности для карты 1.
После выполнения этого изменения я внедрил ваш код и, похоже, работает, когда вы рисуете 1 карту.
Card 1: 10.4%
Card 2: 32.2%
Card 3: 48.4%
Card 4: 9.0%
Card 1: 7.5%
Card 2: 28.1%
Card 3: 50.0%
Card 4: 14.4%
ОДНАКО, ваш код не будет работать, если вы рисуете более 1 карты из-за этого утверждения:
heap[i].Weight = 0;
Эта строка и цикл пересчета после этого существенно удаляют все экземпляры извлеченной карты из кучи. Если вам выпадет четыре карты, тогда процент будет составлять 25% для всех карт, так как вы в основном рисуете все 4 карты. Алгоритм, как есть, не полностью применим к вашему делу.
Я подозреваю, что вам придется воссоздавать кучу каждый раз, когда вы рисуете карту, но я сомневаюсь, что она все равно будет работать. Если бы я работал над этим, я бы просто сгенерировал 4 разных случайных числа от 1 до heap[1].TotalWeight
и получил от них 4 соответствующие карты, хотя генерация случайных чисел в этом случае могла бы стать непредсказуемой (перерисовкой) и, следовательно, неэффективной.
Ответ 3
Если вы хотите выбрать х элементов из взвешенного набора без замены, чтобы элементы выбирались с вероятностью, пропорциональной их весам, то ваш алгоритм ошибочен.
Рассмотрим следующий взвешенный список:
'a': вес 1
'b': вес 2
'c': вес 3
и x = 2
В этом примере ваша функция всегда должна возвращать 'c' в наборе результатов. Это единственный способ, чтобы "c" выбирался в 3 раза чаще, чем "a", и в 1,5 раза чаще, чем "b". Но тривиально видеть, что ваш алгоритм не всегда дает результат "c".
Один алгоритм, который выполняет это, состоит в том, чтобы выровнять элементы вдоль числовой строки от 0 до 1 так, чтобы они занимали сегмент, пропорциональный их весу, затем случайным образом выбирали число "начало" между 0 и 1/x, затем найти все точки "start + n/x" (для всех целых чисел n, таких, что точка находится между 0 и 1) и дать множество, содержащее элементы, отмеченные этими точками.
Другими словами, что-то вроде:
a.) optionally shuffle the list of elements (if you need random combinations of elements in addition to respecting the weights)
b.) create a list of cumulative weights, if you will, called borders, such that borders[0] = items[0].weight and borders[i] = borders[i - 1] + items[i].weight
c.) calculate the sum of all the weights => total_weight
d.) step_size = total_weight / x
e.) next_stop = pick a random number between [0, step_size)
f.) current_item = 0
g.) while next_stop < total_weight:
h.) while borders[current_item] < next_stop:
i.) current_item += 1
j.) append items[current_item] to the output
k.) next_stop += step_size
Примечание: это работает только там, где наибольший вес <= step_size. Если один из элементов имеет вес больше общего веса /x, то эта проблема невозможна: вам нужно будет выбрать элемент более одного раза, чтобы уважать веса.
Ответ 4
Вы можете сделать это:
Card GetCard(List<Card> cards)
{
int total = 0;
foreach (Card c in cards)
{
total += AttributionRate;
}
int index = Random.Next(0, total - 1);
foreach(Card c in cards)
{
index -= c.AttributionRate;
if (index < 0)
{
return c;
}
}
}
Card PopCard(List<Card> cards)
{
Card c = GetCard(cards);
cards.Remove(c);
}
В теории это должно сработать.