【问题标题】:Whats the most concise way to pick a random element by weight in c#?在 c# 中按重量选择随机元素的最简洁方法是什么?
【发布时间】:2012-02-26 20:02:17
【问题描述】:

假设:

List<element>是哪个元素:

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

我想要实现的是,通过权重随机选择一个元素。 例如:

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

所以

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

我知道我可以创建一个循环、计算总数等...

我想了解的是,Linq 在 ONE 行中执行此操作的最佳方法是什么(或尽可能短),谢谢。

更新

我在下面找到了我的答案。我学到的第一件事是:Linq 不是魔法,它比精心设计的循环要慢

所以我的问题变成了按重量找到一个随机元素,(没有尽可能短的东西:)

【问题讨论】:

  • 所以你想要短代码,但你不在乎它很慢?
  • Linq 会比基于循环的代码慢。如果你想要快速的代码,你需要在O(n) 中预先计算,这样你就可以在O(1) 中查找。但其代码会相对复杂。
  • 你认为 Linq(to objects) 是如何工作的?魔法?它只是封装了循环,通常比手写循环慢 2-3 倍。 linq 的主要优点是代码更短更清晰。
  • LINQ 确实比自己编写有效的迭代算法要慢。使用 linq 的原因是它更易于阅读/使用,并且不太可能导致错误。
  • @CodeInChaos - 慢 2 到 3 倍真是夸张。

标签: c# linq weighted weighted-average


【解决方案1】:
// 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));

【讨论】:

  • 分解list.Max(...)。您当前的代码是O(n^2)。如果你把它排除在外,它只是O(n*log(n))
  • 如果你需要First(),而且你已经使用了Random,为什么还要随机订购呢?另外,我会缓存 list.Max,没有理由计算每次迭代的最大值。
  • 无论如何,对于@allguys,我想我只是使用循环:(
  • @EricYin:他在最大权重上使用 Random,因为您说 200 应该比 50 更有可能被选中。但是,我不确定它是否与 sum(weight)-weighting 匹配你提出你的问题。排序似乎确实是多余的(请参阅我之前的评论)。
  • minWeight 是为了确保随着重量的增加而更频繁地选择这些物品。随机顺序 (@Abel) 用于从结果集中选择 一个随机项,因为仅选择第一个项将永远不会产生示例中的 Weight=50 项,即使 minWeight 将是< 50
【解决方案2】:

这可行:

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

基本上,为每个元素创建一个带有最终权重的分区。 在您的示例中,Element1 将关联到 (1-->100),Element2 将关联到 (101-->151),依此类推...

然后计算一个随机的权重总和,并寻找与之关联的范围。

您也可以计算方法组中的总和,但这会带来另一个副作用...

请注意,我并不是说这是优雅或快速的。但它确实使用了 linq(不是一行...)

【讨论】:

  • 谢谢。我想我会留下 linq,除了 sum 部分,因为它会在途中创建太多新对象
【解决方案3】:

如果您想要一个通用版本(对于与(单例)随机化帮助器一起使用时很有用,请考虑您是否需要一个常量种子)

用法:

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

代码:

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

【讨论】:

  • 我喜欢这个解决方案,看起来非常非常体面。此外,我将编辑并允许totalWeight=0,然后随机选择一个(机会均等,因为它们的权重相同)而不抛出异常。
  • 我想在这里我发现了一个问题(一个小问题),你应该使用randomWeightedIndex = _random.Next(totalWeight)+1;,然后比较if(randomWeightedIndex &lt;= itemWeightedIndex)。否则被选中的机会与我在问题中的要求略有不同。
  • 我很确定这没有什么不同。
  • 它使。假设A=0, B=2,A 被选中的概率为 0%,B 为 100%。 R=rand.Next(2) 将返回 0-1。当R=1时,将选择A,A和B变为50%。如果使用+1,R=1到2,都不会
  • 基于零的索引会有所不同,但我认为权重值 0 为“0% 机会”,所以......这一切都取决于定义。不过很高兴看到您发现它很有用。
【解决方案4】:

这是一个带有预计算的快速解决方案。预计算采用O(n),搜索采用O(log(n))

预计算:

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

生成:

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];

但如果列表在每次搜索之间发生变化,您可以改为使用简单的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;
}

【讨论】:

  • 问题中的 LINQ 在哪里? ;)
  • int total=elements.Sum(e=&gt;e.Weight) :P 这是我发现 linq 在这个问题中唯一有用的地方。我找不到使用 linq 执行搜索本身的快速而干净的方法。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-11-12
  • 1970-01-01
  • 1970-01-01
  • 2014-06-01
  • 1970-01-01
相关资源
最近更新 更多