6

更新:我的问题已经解决,我更新了问题中的代码源以匹配 Jason 的答案。请注意,rikitikitik 的答案是解决从带有替换的样本中挑选卡片的问题。

我想从加权列表中选择 x 个随机元素。采样不更换。我找到了这个答案:https ://stackoverflow.com/a/2149533/57369 ,在 Python 中有一个实现。我用 C# 实现了它并对其进行了测试。但结果(如下所述)与我的预期不符。我对 Python 一无所知,所以我很确定我在将代码移植到 C# 时犯了一个错误,但我看不到 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 %
};

这是我在 C# 中的实现

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; }
}
4

4 回答 4

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)

当然,这只适用AttributionRateint. 否则,您将不得不稍微修改套牌生成。

对于 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% 

编辑:

我勇敢地进行了按位运算,并查看了您的代码。在我的油炸大脑上加入大量烧烤酱后,我注意到了一些事情:

首先,Random.Next(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 张牌。该算法实际上并不完全适用于您的情况。

我怀疑每次抽牌时都必须重新创建堆,但我怀疑它仍然会表现良好。但是,如果我要处理这个问题,我只会从 1 到 4 生成不同的随机数,heap[1].TotalWeight并从那里得到 4 张相应的卡片,尽管在这种情况下随机数生成可能变得不可预测(重新滚动),因此效率低下。

于 2012-08-06T03:58:26.530 回答
2

程序中有两个小错误。首先,随机数的范围应该正好等于所有物品的总重量:

int gas = random.Next(heap[1].TotalWeight);

其次,改变它所说的两个地方gas >gas >=

(原始的 Python 代码是可以的,因为gas它是一个浮点数,所以 和 之间的差异>可以>=忽略不计。该代码被编写为接受整数或浮点权重。)

更新:好的,您在代码中进行了建议的更改。我认为该代码现在是正确的!

于 2012-08-06T23:16:02.543 回答
1

如果您想从加权集合中选择 x 个元素而不进行替换,以便以与其权重成比例的概率选择元素,那么您的算法是错误的。

考虑以下加权列表:
'a':权重 1
'b':权重 2
'c':权重 3
和 x = 2

在此示例中,您的函数应始终在结果集中返回“c”。这是“c”被选择为“a”的 3 倍和“b”的 1.5 倍的唯一方法。但是很容易看到您的算法并不总是在结果中产生“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,那么这个问题是不可能的:您必须多次选择一个元素才能尊重权重。

于 2014-11-20T01:03:09.917 回答
0

你可以这样做:

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);
}

理论上这应该有效。

于 2012-08-02T11:13:56.887 回答