2012-02-04 19 views
11

Asumamos:¿Cuál es la forma más concisa de elegir un elemento aleatorio por peso en C#?

List<element> qué elemento es:

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

Lo que quiero lograr es, seleccione un elemento al azar por el peso. Por ejemplo:

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

Así

  • la oportunidad Element_1 fue seleccionado es 100/(100 + 50 + 200) = 28,57%
  • la oportunidad Element_2 fue seleccionado es 50/(100 + 50 + 200) = 14,29%
  • la ocasión fue seleccionado Element_3 es de 200/(100 + 50 + 200) = 57,14%

Sé que puedo crear un bucle, calcular el total, etc ...

Lo que quiero aprender es cuál es la mejor manera de hacer esto con Linq en UNA línea (o lo más corto posible), gracias.

ACTUALIZACIÓN

encontré mi respuesta a continuación. Lo primero que aprendo es: Linq NO es mágico, es más lento que el ciclo bien diseñado.

Así que mi pregunta es encontrar un elemento aleatorio en peso, (sin lo más corto posible cosas :)

+2

¿Quiere código corto, pero no le importa que sea lento? – CodesInChaos

+0

no no, sé que el ciclo es lento, así que quiero usar Linq, debería ser más rápido. No es muy corto, simplemente no es tan complejo como usar loop y loop nuevamente. Puedo obtener imágenes de lo que puedo hacer es: 1: obtener total, 2: aleatorio del total 3: obtener un elemento en el rango ... bastante complejo y lento, supongo que –

+2

Linq será más lento que el código basado en bucle. Si desea un código rápido, necesita calcular previamente en 'O (n)' para que pueda realizar una búsqueda 'O (1)'. Pero el código para eso será relativamente complejo. – CodesInChaos

Respuesta

4

Si quieres un versión genérica (útil para usar con un (Singleton) aleatorizar ayudante, considerar si usted necesita una semilla constante o no)

uso:

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

código:

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

Me encanta esta solución, se ve muy, muy decente. Además, editaré y permitiré 'totalWeight = 0', luego simplemente seleccionaré uno al azar (con la misma posibilidad ya que los pesos son los mismos) sin lanzar una excepción. –

+0

Creo que encontré un problema (un pequeño problema), debes usar 'randomWeightedIndex = _random.Next (totalWeight) +1;', luego compara 'if (randomWeightedIndex <= itemWeightedIndex)'. De lo contrario, la posibilidad de ser seleccionado es ligeramente diferente de mi requisito en mi pregunta. –

+2

Estoy bastante seguro de que no hace la diferencia. – doblak

4
// 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)); 
+2

Factorizar 'list.Max (...)'. Tu código actual es 'O (n^2)'. Si lo descompone, solo es 'O (n * log (n))' – CodesInChaos

+0

Hola @Tobias, gracias por el código. ¿Es aleatorio un número del peso máximo de todos los elementos, luego ordena por azar? Si ordena por azar, ¿por qué necesita 'where'? Y también, ¿por qué generar un número aleatorio a partir del peso máximo de todos los elementos? –

+1

Si necesita First(), y ya usa Random, ¿por qué ordenar por Random? Además, tendría una lista en caché. Max, no hay razón para calcular el máximo en cada iteración. – Abel

2

Ésta es una solución rápida con precomputation. La precomputación toma O(n), la búsqueda O(log(n)).

precomputamos:

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

Generar:

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

Pero si la lista cambia entre cada búsqueda, en su lugar puede utilizar un simple O(n) búsqueda lineal:

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

¿Dónde está el LINQ de la pregunta? ;) – Abel

+1

'int total = elements.Sum (e => e.Weight)': P Ese es el único lugar donde encontré linq útil en este problema. No pude encontrar una manera rápida y limpia de realizar la búsqueda en sí con linq. – CodesInChaos

2

Esto podría funcionar:

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

B asically, para cada elemento se crea una partición con un peso final. En su ejemplo, Element1 estaría asociado a (1 -> 100), Element2 asociado a (101 -> 151) y así sucesivamente ...

Luego se calcula una suma de peso aleatoria y buscamos el rango que está asociado a él.

También puede calcular la suma en el grupo método, sino que introduciría otro efecto secundario ...

Tenga en cuenta que no estoy diciendo que esto sea elegante o rápido. Pero sí utiliza linq (no en una línea ...)

+0

gracias. Creo que dejaré linq excepto la parte de suma, ya que crea demasiados objetos nuevos en el camino –

Cuestiones relacionadas