【问题标题】:Random weighted choice随机加权选择
【发布时间】:2010-09-08 13:45:37
【问题描述】:

考虑下面代表代理的类:

public class Broker
{
    public string Name = string.Empty;
    public int Weight = 0;

    public Broker(string n, int w)
    {
        this.Name = n;
        this.Weight = w;
    }
}

我想从数组中随机选择一个经纪人,考虑到他们的权重。

你觉得下面的代码怎么样?

class Program
    {
        private static Random _rnd = new Random();

        public static Broker GetBroker(List<Broker> brokers, int totalWeight)
        {
            // totalWeight is the sum of all brokers' weight

            int randomNumber = _rnd.Next(0, totalWeight);

            Broker selectedBroker = null;
            foreach (Broker broker in brokers)
            {
                if (randomNumber <= broker.Weight)
                {
                    selectedBroker = broker;
                    break;
                }

                randomNumber = randomNumber - broker.Weight;
            }

            return selectedBroker;
        }


        static void Main(string[] args)
        {
            List<Broker> brokers = new List<Broker>();
            brokers.Add(new Broker("A", 10));
            brokers.Add(new Broker("B", 20));
            brokers.Add(new Broker("C", 20));
            brokers.Add(new Broker("D", 10));

            // total the weigth
            int totalWeight = 0;
            foreach (Broker broker in brokers)
            {
                totalWeight += broker.Weight;
            }

            while (true)
            {
                Dictionary<string, int> result = new Dictionary<string, int>();

                Broker selectedBroker = null;

                for (int i = 0; i < 1000; i++)
                {
                    selectedBroker = GetBroker(brokers, totalWeight);
                    if (selectedBroker != null)
                    {
                        if (result.ContainsKey(selectedBroker.Name))
                        {
                            result[selectedBroker.Name] = result[selectedBroker.Name] + 1;
                        }
                        else
                        {
                            result.Add(selectedBroker.Name, 1);
                        }
                    }
                }


                Console.WriteLine("A\t\t" + result["A"]);
                Console.WriteLine("B\t\t" + result["B"]);
                Console.WriteLine("C\t\t" + result["C"]);
                Console.WriteLine("D\t\t" + result["D"]);

                result.Clear();
                Console.WriteLine();
                Console.ReadLine();
            }
        }
    }

我没那么自信。当我运行这个时,Broker A 总是比 Broker D 获得更多的命中,而且它们的权重相同。

有没有更准确的算法?

谢谢!

【问题讨论】:

  • 您好,先生,我看到了您的问题并受到启发,使用您的算法在 Java 中创建了我自己的 adrotator 类。我恳请您解释一下,如果数据库中有一百万个经纪人存储在一个宽行中,您将如何从数据库中选择经纪人。我会选择第一个 n 并应用您的算法来选择一个随机代理,然后在下一个请求中选择从 n+1 开始的接下来的 n 个代理,依此类推吗?
  • 我写了一个非常相似的库...它有一些附加功能,并且针对大型数据集进行了优化:github.com/kinetiq/Ether.WeightedSelector
  • 你的broker需要按权重升序排序吗?

标签: c# algorithm random


【解决方案1】:

您的算法几乎是正确的。但是,测试应该是&lt; 而不是&lt;=

if (randomNumber < broker.Weight)

这是因为 0 包含在随机数中,而 totalWeight 是排除的。换句话说,权重为 0 的经纪人仍然有很小的机会被选中——根本不是你想要的。这说明代理 A 的点击次数比代理 D 多。

除此之外,您的算法很好,实际上是解决此问题的规范方法。

【讨论】:

  • 这也适用于双精度值的权重吗?
  • @Jordan 会达到double 的精度。但是,上面的代码使用了_rnd.Next,它只适用于整数范围。要使用双精度范围,您需要使用适当的方法从double 范围生成数字。
  • 我知道。 Random 有一个 NextDouble 方法,它返回一个介于 0.0 和 1.0 之间的双精度值。我可以将这个值乘以总重量。 :) 谢谢。
【解决方案2】:

更通用一点的东西怎么样?可以用于任何数据类型?

using System;
using System.Linq;
using System.Collections;
using System.Collections.Generic;

public static class IEnumerableExtensions {
    
    public static T RandomElementByWeight<T>(this IEnumerable<T> sequence, Func<T, float> weightSelector) {
        float totalWeight = sequence.Sum(weightSelector);
        // The weight we are after...
        float itemWeightIndex =  (float)new Random().NextDouble() * totalWeight;
        float currentWeightIndex = 0;

        foreach(var item in from weightedItem in sequence select new { Value = weightedItem, Weight = weightSelector(weightedItem) }) {
            currentWeightIndex += item.Weight;
            
            // If we've hit or passed the weight we are after for this item then it's the one we want....
            if(currentWeightIndex >= itemWeightIndex)
                return item.Value;
            
        }
        
        return default(T);
        
    }
    
}

只需调用

    Dictionary<string, float> foo = new Dictionary<string, float>();
    foo.Add("Item 25% 1", 0.5f);
    foo.Add("Item 25% 2", 0.5f);
    foo.Add("Item 50%", 1f);
    
    for(int i = 0; i < 10; i++)
        Console.WriteLine(this, "Item Chosen {0}", foo.RandomElementByWeight(e => e.Value));

【讨论】:

    【解决方案3】:
    class Program
    {
        static void Main(string[] args)
        {
            var books = new List<Book> {
            new Book{Isbn=1,Name="A",Weight=1},
            new Book{Isbn=2,Name="B",Weight=100},
            new Book{Isbn=3,Name="C",Weight=1000},
            new Book{Isbn=4,Name="D",Weight=10000},
            new Book{Isbn=5,Name="E",Weight=100000}};
    
            Book randomlySelectedBook = WeightedRandomization.Choose(books);
        }
    }
    
    public static class WeightedRandomization
    {
        public static T Choose<T>(List<T> list) where T : IWeighted
        {
            if (list.Count == 0)
            {
                return default(T);
            }
    
            int totalweight = list.Sum(c => c.Weight);
            Random rand = new Random();
            int choice = rand.Next(totalweight);
            int sum = 0;
    
            foreach (var obj in list)
            {
                for (int i = sum; i < obj.Weight + sum; i++)
                {
                    if (i >= choice)
                    {
                        return obj;
                    }
                }
                sum += obj.Weight;
            }
    
            return list.First();
        }
    }
    
    public interface IWeighted
    {
        int Weight { get; set; }
    }
    
    public class Book : IWeighted
    {
        public int Isbn { get; set; }
        public string Name { get; set; }
        public int Weight { get; set; }
    }
    

    【讨论】:

    • 每次执行 new Random() 都会使用时钟进行初始化。这意味着在一个紧密的循环中,您可以多次获得相同的值。您应该保留一个 Random 实例并继续在同一实例上使用 Next。因此,为 Random 实例做一个类级别的声明。
    • 我也想知道为什么需要内部 for 循环?不只是增加总和的权重并检查它是否 >= 选择也可以吗?
    【解决方案4】:

    因为这是 Google 上的最高搜索结果:

    我已经创建了a C# library for randomly selected weighted items

    • 它实现了树选择和 walker 别名方法算法,为所有用例提供最佳性能。
    • 它经过单元测试和优化。
    • 它支持 LINQ。
    • 它是免费和开源的,在 MIT 许可下获得许可。

    一些示例代码:

    IWeightedRandomizer<string> randomizer = new DynamicWeightedRandomizer<string>();
    randomizer["Joe"] = 1;
    randomizer["Ryan"] = 2;
    randomizer["Jason"] = 2;
    
    string name1 = randomizer.RandomWithReplacement();
    //name1 has a 20% chance of being "Joe", 40% of "Ryan", 40% of "Jason"
    
    string name2 = randomizer.RandomWithRemoval();
    //Same as above, except whichever one was chosen has been removed from the list.
    

    【讨论】:

      【解决方案5】:

      在选择代理而不是内存使用时,另一种方法有利于速度。基本上,我们创建的列表包含与指定权重相同数量的代理实例引用。

      List<Broker> brokers = new List<Broker>();
      for (int i=0; i<10; i++)
          brokers.Add(new Broker("A", 10));
      for (int i=0; i<20; i++)
          brokers.Add(new Broker("B", 20));
      for (int i=0; i<20; i++)
          brokers.Add(new Broker("C", 20));
      for (int i=0; i<10; i++)
          brokers.Add(new Broker("D", 10));
      

      然后,选择一个随机加权的实例是一个 O(1) 操作:

      int randomNumber = _rnd.Next(0, brokers.length);
      selectedBroker = brokers[randomNumber];
      

      【讨论】:

      • 另一种不会花费太多内存的替代方法是在 Broker 数组中使用索引。
      【解决方案6】:

      有点晚了,但这里是 C#7 示例。它非常小,而且分布正确。

      public static class RandomTools
      {
          public static T PickRandomItemWeighted<T>(IList<(T Item, int Weight)> items)
          {
              if ((items?.Count ?? 0) == 0)
              {
                  return default;
              }
      
              int offset = 0;
              (T Item, int RangeTo)[] rangedItems = items
                  .OrderBy(item => item.Weight)
                  .Select(entry => (entry.Item, RangeTo: offset += entry.Weight))
                  .ToArray();
      
              int randomNumber = new Random().Next(items.Sum(item => item.Weight)) + 1;
              return rangedItems.First(item => randomNumber <= item.RangeTo).Item;
          }
      }
      

      【讨论】:

        【解决方案7】:

        如果您想要更快的速度,您可以考虑加权水库采样,您不必提前找到总重量(但您可以更频繁地从随机数生成器中采样)。代码可能类似于

        Broker selected = null;
        int s = 0;
        foreach(Broker broker in brokers) {
            s += broker.Weight;
            if (broker.Weight <= _rnd.Next(0,s)) {
                selected = broker;
            }
        }
        

        这需要通过列表经纪人一次。但是,如果经纪人列表是固定的或不经常更改,您可以保留一个累积和数组,即 A[i] 是所有经纪人的权重总和 0,..,i-1。那么 A[n] 是总权重,如果你选择一个介于 1 和 A[n-1] 之间的数字,比如说 x,你会找到经纪人 j s.t. A[j-1]

        【讨论】:

        • 是我还是那个循环总是在第一次迭代时选择第一个项目
        • @Epirocks 它会暂时,但它有机会在后续迭代中覆盖它。
        • @Solomon 当然你是对的。我想我写的时候蒙上了眼睛。
        • @Epirocks 我主要是为了其他人感到困惑而指出的。 (一开始我也很困惑!)
        【解决方案8】:

        我想出了这个解决方案的通用版本:

        public static class WeightedEx
        {
            /// <summary>
            /// Select an item from the given sequence according to their respective weights.
            /// </summary>
            /// <typeparam name="TItem">Type of item item in the given sequence.</typeparam>
            /// <param name="a_source">Given sequence of weighted items.</param>
            /// <returns>Randomly picked item.</returns>
            public static TItem PickWeighted<TItem>(this IEnumerable<TItem> a_source)
                where TItem : IWeighted
            {
                if (!a_source.Any())
                    return default(TItem);
        
                var source= a_source.OrderBy(i => i.Weight);
        
                double dTotalWeight = source.Sum(i => i.Weight);
        
                Random rand = new Random();
        
                while (true)
                {
                    double dRandom = rand.NextDouble() * dTotalWeight;
        
                    foreach (var item in source)
                    {
                        if (dRandom < item.Weight)
                            return item;
        
                        dRandom -= item.Weight;
                    }
                }
            }
        }
        
        /// <summary>
        /// IWeighted: Implementation of an item that is weighted.
        /// </summary>
        public interface IWeighted
        {
            double Weight { get; }
        }
        

        【讨论】:

        • 我认为a_source在传入之前需要按重量排序?
        • 好眼光。我会解决的。
        • 在我的实现中,我只是确保它作为 SortedList 传入,这意味着我可以通过 collection.Keys.Sum() 来获得总权重。希望对您有所帮助。
        • 那行得通。在使用扩展方法时,我更喜欢尽可能地保持抽象。按重量排序确实需要一些大 O,但它允许我对任何可能的加权对象枚举进行排序。如果我要接受一个 SortedList,那么我的方法只会扩展 SortedList。如果用户给我一个已经排序的列表,那么 OrderBy 将只是 O(n),在我看来可以忽略不计。这是个人偏好,但在我的代码准备好测试之前我不会进行优化,而且这种做法从未给我带来任何真正的问题。
        【解决方案9】:

        只是为了分享我自己的实现。希望你会发现它有用。

            // Author: Giovanni Costagliola <giovanni.costagliola@gmail.com>
        
            using System;
            using System.Collections.Generic;
            using System.Linq;
        
            namespace Utils
            {
            /// <summary>
            /// Represent a Weighted Item.
            /// </summary>
            public interface IWeighted
            {
                /// <summary>
                /// A positive weight. It's up to the implementer ensure this requirement
                /// </summary>
                int Weight { get; }
            }
        
            /// <summary>
            /// Pick up an element reflecting its weight.
            /// </summary>
            /// <typeparam name="T"></typeparam>
            public class RandomWeightedPicker<T> where T:IWeighted
            {
                private readonly IEnumerable<T> items;
                private readonly int totalWeight;
                private Random random = new Random();
        
                /// <summary>
                /// Initiliaze the structure. O(1) or O(n) depending by the options, default O(n).
                /// </summary>
                /// <param name="items">The items</param>
                /// <param name="checkWeights">If <c>true</c> will check that the weights are positive. O(N)</param>
                /// <param name="shallowCopy">If <c>true</c> will copy the original collection structure (not the items). Keep in mind that items lifecycle is impacted.</param>
                public RandomWeightedPicker(IEnumerable<T> items, bool checkWeights = true, bool shallowCopy = true)
                {
                    if (items == null) throw new ArgumentNullException("items");
                    if (!items.Any()) throw new ArgumentException("items cannot be empty");
                    if (shallowCopy)
                        this.items = new List<T>(items);
                    else
                        this.items = items;
                    if (checkWeights && this.items.Any(i => i.Weight <= 0))
                    {
                        throw new ArgumentException("There exists some items with a non positive weight");
                    }
                    totalWeight = this.items.Sum(i => i.Weight);
                }
                /// <summary>
                /// Pick a random item based on its chance. O(n)
                /// </summary>
                /// <param name="defaultValue">The value returned in case the element has not been found</param>
                /// <returns></returns>
                public T PickAnItem()
                {
                    int rnd = random.Next(totalWeight);
                    return items.First(i => (rnd -= i.Weight) < 0);
                }
        
                /// <summary>
                /// Resets the internal random generator. O(1)
                /// </summary>
                /// <param name="seed"></param>
                public void ResetRandomGenerator(int? seed)
                {
                    random = seed.HasValue ? new Random(seed.Value) : new Random();
                }
            }
        }
        

        要点:https://gist.github.com/MrBogomips/ae6f6c9af8032392e4b93aaa393df447

        【讨论】:

        • 也许我读错了这段代码,但我认为如果你有 2 件重量相同的物品,这将无法正常工作。
        【解决方案10】:

        原始问题中的实现对我来说似乎有点奇怪;

        列表的总权重为 60,因此随机数为 0-59。 它总是根据权重检查随机数,然后递减它。 在我看来,它会根据它们的顺序来支持列表中的东西。

        这是我正在使用的通用实现 - 关键在于 Random 属性:

        using System;
        using System.Collections.Generic;
        using System.Linq;
        
        public class WeightedList<T>
        {
            private readonly Dictionary<T,int> _items = new Dictionary<T,int>();
        
            // Doesn't allow items with zero weight; to remove an item, set its weight to zero
            public void SetWeight(T item, int weight)
            {
                if (_items.ContainsKey(item))
                {
                    if (weight != _items[item])
                    {
                        if (weight > 0)
                        {
                            _items[item] = weight;
                        }
                        else
                        {
                            _items.Remove(item);
                        }
        
                        _totalWeight = null; // Will recalculate the total weight later
                    }
                }
                else if (weight > 0)
                {
                    _items.Add(item, weight);
        
                    _totalWeight = null; // Will recalculate the total weight later
                }
            }
        
            public int GetWeight(T item)
            {
                return _items.ContainsKey(item) ? _items[item] : 0;
            }
        
            private int? _totalWeight;
            public int totalWeight
            {
                get
                {
                    if (!_totalWeight.HasValue) _totalWeight = _items.Sum(x => x.Value);
        
                    return _totalWeight.Value;
                }
            }
        
            public T Random
            {
                get
                {
                    var temp = 0;
                    var random = new Random().Next(totalWeight);
        
                    foreach (var item in _items)
                    {
                        temp += item.Value;
        
                        if (random < temp) return item.Key;
                    }
        
                    throw new Exception($"unable to determine random {typeof(T)} at {random} in {totalWeight}");
                }
            }
        }
        

        【讨论】:

          猜你喜欢
          • 2015-07-05
          • 2017-12-26
          • 1970-01-01
          • 2023-03-31
          • 2020-01-22
          • 1970-01-01
          • 2022-01-08
          相关资源
          最近更新 更多