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

后端 未结 4 1914
慢半拍i
慢半拍i 2021-01-02 03:57

Lets assume:

List which element is:

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

What I want to a

相关标签:
4条回答
  • 2021-01-02 04:32
    // 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));
    
    0 讨论(0)
  • 2021-01-02 04:33

    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");
    }
    
    0 讨论(0)
  • 2021-01-02 04:39

    This is a fast solution with precomputation. The precomputation takes O(n), the search 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:

    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;
    }
    
    0 讨论(0)
  • 2021-01-02 04:42

    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...

    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...)

    0 讨论(0)
提交回复
热议问题