在c#中,按权重选取随机元素的最简洁方法是什么?
让我们假设:在c#中,按权重选取随机元素的最简洁方法是什么?,c#,linq,weighted,weighted-average,C#,Linq,Weighted,Weighted Average,让我们假设: 列表哪个元素是: public class Element(){ int Weight {get;set;} } 我想要实现的是,根据权重随机选择一个元素。 例如: Element_1.Weight = 100; Element_2.Weight = 50; Element_3.Weight = 200; 所以 被选中的机会元素_1为100/(100+50+200)=28.57% 被选中的机会元素_2为50/(100+50+200)=14.29% 被选中的机会元素_3为
列表
哪个元素是:
public class Element(){
int Weight {get;set;}
}
我想要实现的是,根据权重随机选择一个元素。
例如:
Element_1.Weight = 100;
Element_2.Weight = 50;
Element_3.Weight = 200;
所以
- 被选中的机会
为100/(100+50+200)=28.57%元素_1
- 被选中的机会
为50/(100+50+200)=14.29%元素_2
- 被选中的机会
为200/(100+50+200)=57.14%元素_3
所以我的问题变成了按权重查找一个随机元素(没有尽可能短的东西:)这是一个预计算的快速解决方案。预计算采用
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;
}
如果你想要一个<强>泛型版本>(对使用(单体)随机化助手有用的话,考虑是否需要一个常数种子)
用法:randomizer.GetRandomItem(items, x => x.Weight)
代码:
公共T GetRandomItem(IEnumerable itemsEnumerable,Func weightKey)
{
var items=itemsEnumerable.ToList();
var totalWeight=items.Sum(x=>weightKey(x));
var randomWeightedIndex=_random.Next(总重量);
var itemWeightedIndex=0;
foreach(项目中的var项目)
{
itemWeightedIndex+=权重键(项);
if(随机加权指数<项目加权指数)
退货项目;
}
抛出新ArgumentException(“集合计数和权重必须大于0”);
}
这可能会起作用:
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会比基于循环的代码慢。如果你想要快速的代码,你需要在
O(n)
中预计算,这样你就可以进行O(1)
查找。但是这方面的代码将相对复杂。您认为Linq(到对象)是如何工作的?魔术它只是封装循环,通常比手写循环慢2-3倍。linq的主要优点是代码更短更清晰。linq确实比自己编写高效的迭代算法慢。linq的原因是它更易于阅读/使用,并且不太可能导致错误。@CodeInChaos-2到3倍的速度是一种严重的夸张。Factor outlist.Max(…)
。您当前的代码是O(n^2)
。如果你把它算出来,只有O(n*log(n))
如果你需要First(),而且你已经使用了Random,为什么要按Random排序呢?另外,我会缓存list.Max,没有理由在每次迭代中计算Max。无论如何,对于@allguys,我想我只是使用loop:(@EricYin:他在Max-weight上使用Random,因为你说200应该比50更有可能被选中。但是,我不确定它是否匹配总和(weight)-权衡你提出的问题。排序似乎确实是多余的(见我之前的评论)。minWeight
是为了确保随着物品重量的增加,选择物品的频率会增加。而随机顺序(@Abel)用于从结果集中选择一个随机项,因为仅从示例中选择第一个不会产生Weight=50
项,即使minWeight
是<50
问题的LINQ在哪里?;)int total=elements.Sum(e=>e.Weight)
:P这是我发现linq在这个问题上唯一有用的地方。我找不到一个快速、干净的方法来使用linq执行搜索。我喜欢这个解决方案,看起来非常不错。此外,我将编辑并允许totalWeight=0
,然后随机选择一个(由于它们的权重相同,所以机会均等),而不抛出异常。我想我在这里发现了一个问题(一个小问题),您应该使用randomWeightedIndex=\u random。下一步(totalWeight)+1
,然后比较if(randomWeightedIndex)我很确定它不会有什么不同。它会产生影响。让我们假设a=0,B=2
,a被选中的几率为0%,B为100%。R=rand.Next(2)当R=1时,A将被选择,A将变成A和B的50%。如果使用+1,R=1到2,在基于零的索引中也不会有区别,但是我认为权重值0是“0%机会”。,所以…这一切都取决于定义。不过,很高兴看到你发现它很有用。谢谢。我想除了求和部分,我将离开linq,因为它在途中创建了太多新对象
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;
}
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");
}
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);