简体   繁体   English

在 C# 中按权重选择随机元素的最简洁方法是什么?

[英]Whats the most concise way to pick a random element by weight in c#?

Lets assume:让我们假设:

List<element> which element is: List<element>哪个元素是:

public class Element {
   int Weight { get; set; }
}

What I want to achieve is, select an element randomly by the weight.我想要实现的是,按权重随机选择一个元素。 For example:例如:

Element_1.Weight = 100;
Element_2.Weight = 50;
Element_3.Weight = 200;

So所以

  • the chance Element_1 got selected is 100/(100+50+200)=28.57% Element_1被选中的几率是 100/(100+50+200)=28.57%
  • the chance Element_2 got selected is 50/(100+50+200)=14.29% Element_2被选中的几率是 50/(100+50+200)=14.29%
  • the chance Element_3 got selected is 200/(100+50+200)=57.14% Element_3被选中的几率是 200/(100+50+200)=57.14%

I know I can create a loop, calculate total, etc...我知道我可以创建一个循环,计算总数,等等......

What I want to learn is, whats the best way to do this by Linq in ONE line (or as short as possible), thanks.我想学习的是,Linq 在一行(或尽可能短)中执行此操作的最佳方法是什么,谢谢。

UPDATE更新

I found my answer below.我在下面找到了答案。 First thing I learn is: Linq is NOT magic, it's slower then well-designed loop .我学到的第一件事是: Linq 不是魔法,它比精心设计的循环慢

So my question becomes find a random element by weight, (without as short as possible stuff:)所以我的问题变成了按重量找到一个随机元素,(没有尽可能短的东西:)

// assuming rnd is an already instantiated instance of the Random class
var max = list.Sum(y => y.Weight);
var rand = rnd.Next(max);
var res = list
    .FirstOrDefault(x => rand >= (max -= x.Weight));

If you want a generic version (useful for using with a (singleton) randomize helper, consider whether you need a constant seed or not) 如果要使用通用版本 (与(单个)随机化助手一起使用时有用,请考虑是否需要恒定种子)

usage: 用法:

randomizer.GetRandomItem(items, x => x.Weight)

code: 码:

public T GetRandomItem<T>(IEnumerable<T> itemsEnumerable, Func<T, int> weightKey)
{
    var items = itemsEnumerable.ToList();

    var totalWeight = items.Sum(x => weightKey(x));
    var randomWeightedIndex = _random.Next(totalWeight);
    var itemWeightedIndex = 0;
    foreach(var item in items)
    {
        itemWeightedIndex += weightKey(item);
        if(randomWeightedIndex < itemWeightedIndex)
            return item;
    }
    throw new ArgumentException("Collection count and weights must be greater than 0");
}

This is a fast solution with precomputation. 这是带有预计算的快速解决方案。 The precomputation takes O(n) , the search O(log(n)) . 预计算采用O(n) ,搜索O(log(n))

Precompute: 预计算:

int[] lookup=new int[elements.Length];
lookup[0]=elements[0].Weight-1;
for(int i=1;i<lookup.Length;i++)
{
  lookup[i]=lookup[i-1]+elements[i].Weight;
}

To generate: 产生:

int total=lookup[lookup.Length-1];
int chosen=random.GetNext(total);
int index=Array.BinarySearch(lookup,chosen);
if(index<0)
  index=~index;
return elements[index];

But if the list changes between each search, you can instead use a simple O(n) linear search: 但是,如果列表在每次搜索之间都发生变化,则可以改用简单的O(n)线性搜索:

int total=elements.Sum(e=>e.Weight);
int chosen=random.GetNext(total);
int runningSum=0;
foreach(var element in elements)
{
  runningSum+=element.Weight;
  if(chosen<runningSum)
    return element;
}

This could work: 这可以工作:

int weightsSum = list.Sum(element => element.Weight);
int start = 1;
var partitions = list.Select(element => 
                 { 
                   var oldStart = start;
                   start += element.Weight;
                   return new { Element = element, End = oldStart + element.Weight - 1};
                 });

var randomWeight = random.Next(weightsSum);
var randomElement = partitions.First(partition => (partition.End > randomWeight)).
                               Select(partition => partition.Element);

Basically, for each element a partition is created with an end weight. 基本上,为每个元素创建一个带有最终权重的分区。 In your example, Element1 would associated to (1-->100), Element2 associated to (101-->151) and so on... 在您的示例中,Element1将与(1-> 100)关联,Element2将与(101-> 151)关联,依此类推...

Then a random weight sum is calculated and we look for the range which is associated to it. 然后计算出一个随机权重之和,并寻找与其相关的范围。

You could also compute the sum in the method group but that would introduce another side effect... 您也可以在方法组中计算总和,但这会带来另一种副作用。

Note that I'm not saying this is elegant or fast. 请注意,我并不是说这很优雅或很快。 But it does use linq (not in one line...) 但是它确实使用linq(不在一行中...)

.Net 6 introduced.MaxBy making this much easier. .Net 6 introduced.MaxBy 使这更容易。
This could now be simplified to the following one-liner:现在可以将其简化为以下单行代码:

list.MaxBy(x => rng.GetNext(x.weight));

This works best if the weights are large or floating point numbers, otherwise there will be collisions, which can be prevented by multiplying the weight by some factor.如果权重很大或浮点数,这种方法效果最好,否则会发生冲突,可以通过将权重乘以某个因子来防止冲突。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM