【问题标题】:Select x random elements from a weighted list in C# (without replacement)从 C# 中的加权列表中选择 x 个随机元素(无需替换)
【发布时间】:2012-07-31 08:02:12
【问题描述】:

更新:我的问题已经解决,我更新了问题中的代码源以匹配 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; }
}

【问题讨论】:

  • 嗬,我会用 linq、order by Guid.NewGuid() 和双/三/...根据速率的实例数量来做到这一点。更容易实现和更容易阅读——不过没有关于性能的消息。
  • System.Random 不是一个好的随机数生成器(Guids 根本不是随机生成器)。如果您需要真正的随机分布,则必须使用其他东西。别无选择。
  • 注意:即使是“完美”的 RNG,两张卡的命中数也不会相同(即使它们具有相同的权重)...
  • System.Random 是一个非常好的用于此目的的随机数生成器。当然它只是一个伪随机数生成器,但在这种情况下这不是问题。
  • @Adriano 你读过我之前的评论吗?使用另一种算法,我能够在选择一张卡片 10 000 次时获得预期的分布。 .NET 的伪随机生成器不是这里的问题。

标签: c# statistics probability


【解决方案1】:

你可以这样做:

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

理论上这应该可行。

【讨论】:

  • 我没有检查他的代码,但我想最大的问题不是你如何“提取”卡片,而是你生成伪随机数的方式。内置生成器远非最佳。
  • 这是我使用您的解决方案得到的结果:卡 1:0.00 %(预期为 10.00 %),卡 2:0.00 %(预期为 30.00 %),卡 3:0.00 %(预期为 50.00 %) ,卡 4:100.00 %(预期为 10.00 %)。这个问题并不像看起来那么琐碎,请参考链接问题(stackoverflow.com/a/2149533/57369)以获得更多见解。
【解决方案2】:

正如某些人在 cmets 中提到的,按照您想要的确切比例创建一张卡片列表:

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 到 heap[1].TotalWeight 的 4 个不同的随机数,并从那里获取 4 个相应的卡,尽管在这种情况下随机数的生成可能变得不可预测(重新滚动),因此效率低下。

【讨论】:

  • 您的代码正在运行,所以我正在考虑接受这是一个答案。但它比我发布的代码慢 6 倍,而且我敢肯定,一旦我开始使用真实数据,差异会更大。
  • 我不知道性能是一个考虑因素。 Guid.NewGuid() 部分可能是罪魁祸首,而您可能在此处生成随机小数会得到更好的结果。不过,我不是 100% 确定这一点。
  • 是的,它确实将计算时间减少了 40% 以上,但仍然比原始解决方案慢得多。我复制我的代码的答案被投票了 28 次,所以我想它的工作方式和宣传的一样。我不明白我的代码怎么会这么错误。
  • 我试图看看你的代码哪里出错了,但是按位运算把我的大脑炸得一塌糊涂;)
  • 恐怕我的问题措辞不佳:一旦选择了一张卡片,您就无法再次选择它(这就是我所说的无需更换的样本)。尽管您的原始答案完全尊重重量,但它也得到了与我的要求不符的重复卡片。
【解决方案3】:

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

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

其次,将gas &gt; 的两个位置都改为gas &gt;=

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

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

【讨论】:

  • 事实上我说得太快了,当我只选择一张卡时它工作得完美无缺。一旦我选择多张卡片(例如给定集合的 3 张卡片),我就会得到以下结果:卡片 1:18.30 %(预期为 10.00 %),卡片 2:30.20 %(预期为 30.00 %),卡片 3:32.25 %( 50.00 % 预期),卡 4:19.25 %(10.00 % 预期)
  • @Gabriel 我不认为你对选择多张卡片的期望是正确的。在每次试验中,您都会选择 3 张没有替换的卡片,对吗?所以卡3不可能占到50%的选秀权!
  • 当您选择多张卡片而不更换时,概率会随着您的移动而变化。取出第一张牌后,再次抽到那张牌的概率变为 0,抽到其余牌的概率上升。如果您从这 4 张卡片中选择 3 张,无需替换,我预计您将在大约 96.6% 的时间内获得卡片 3。但由于它只是你选择的三张牌中的一张,它只占你总选择的 32.2%。请注意,这与您观察到的非常接近!
  • 谢谢,这完全有道理。我尝试了几个不同的样本并选择计数,结果似乎令人满意:)
【解决方案4】:

如果您想从加权集合中选择 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

注意:这只适用于最大重量

【讨论】:

    猜你喜欢
    • 2011-05-29
    • 2014-02-07
    • 2013-10-19
    • 2023-01-11
    • 2011-01-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-03-26
    相关资源
    最近更新 更多