Какой самый краткий способ выбрать случайный элемент по весу в c#?
допустим:
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 в одной строке (или как можно короче), спасибо.
обновление
Я нашел свой ответ ниже. Первое, что я узнаю, это:Linq не магия, это медленнее, чем хорошо продуманная петля.
таким образом, мой вопрос становится найти случайный элемент по весу, (без как можно более короткого материала:)
4 ответов
Если вы хотите универсальный вариант (полезно для использования с помощником (singleton) randomize, подумайте, нужно ли вам постоянное семя или нет)
использование:
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");
}
// 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));
это быстрое решение с предварительным вычислением. Предварительное вычисление принимает 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;
}
создать:
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;
}
Это может сработать:
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);
в принципе, для каждого элемента создается раздел с конечным весом. В вашем примере Элемент1 будет связан с (1-->100), Элемент2 связан с (101-->151) и так далее...
затем вычисляется сумма случайного веса, и мы ищем диапазон, который связан с ним.
вы также можете вычислить сумму в группе методов, но это приведет к другому побочному эффекту...
обратите внимание, что я не говорю это элегантно или быстро. Но он использует linq (не в одной строке...)