计算掉落概率 在游戏中经常遇到按一定的掉落概率来随机掉落指定物品的情况,例如按照:钻石10%,金币20%,银币50%,饰品10%,装备10% 来计算掉落物品的类型。
通常的做法是将物品掉落概率(或者权重)变成一个离散的列表,随后产生一个随机数,再在列表中找到第一个大于该随机数的数,这个数对应的下标也就是对应的物品类型。
对应前面的例子,第一步会构建一个列表 [0.1, 0.3, 0.8, 0.9, 1.0] ;第二步生成一个随机数 0.56(假设);第三步在列表中查找到第一个大于 0.56 的数是 0.8,下标为 2,此时掉落物品应该为银币。
这种算法比较直观,理解和实现起来也比较容易。很多时候甚至都不需要预先构建列表,而是每次累加概率直到找打大于随机数的那个数的下标。但是运用这个算法的时候,第三步我们往往使用的是顺序查找,这在掉落类型较多的时候确实不怎么好。当然大多数情况类型的种类并不是一个很大的数,所以其实也没有影响。(后面实现时候采用二分查找)
在上周的技术交流会时,同事提到了一个掉落概率算法 Alias Method,这个算法比较有意思,实现的很巧妙。算法的论文在这里:《Darts, Dice, and Coins: Sampling from a Discrete Distribution》
(以下引用自 《抽奖概率-三种算法》 ) Alias Method 算法大概是这么做的:把 N 种可能性拼装成一个方形(整体),分成 N 列,每列高度为 1 且最多 2 种可能性。可能性抽象为某种颜色,即每列最多有 2 种颜色,且第 n 列中必有第 n 种可能性,这里将第 n 种可能性称为原色。 想象抛出一个硬币,会落在其中一列,并且是落在列上的一种颜色。这样就得到两个数组:一个记录落在原色的概率是多少,记为 Prob 数组,另一个记录列上非原色的颜色名称,记为 Alias 数组,若该列只有原色则记为 null。
为了直接用网上的图片,我把前面例子的掉落概率依次改为 1/4, 1/5, 1/10, 1/20, 2/5。
由上图方形可得到两个数组: Prob: [3/4, 1/4, 1/2, 1/4, 1] Alias: [4, 4, 0, 1, null] (记录非原色的下标)。之后就根据 Prob 和 Alias 获取其中一个物品,随机产生一列 C,再随机产生一个数 R,通过与Prob[C] 比较,R 较大则返回 C,反之返回 Alias[C]。
Alias Method 算法 算法论文中已经有了一个 java 的版本 ,这里我就按照作者 java 实现 “翻译” 了 python 和 C# 版本。
python 版本 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 from __future__ import divisionimport randomclass AliasMethod4Sample (object) : def __init__ (self, probabilities_or_weights) : assert probabilities_or_weights is not None assert isinstance(probabilities_or_weights, (list, tuple)) self._probabilities_or_weights = probabilities_or_weights self._alias = [None ] * len(probabilities_or_weights) total = sum(probabilities_or_weights) self._probabilities = map(lambda x: x/total, probabilities_or_weights) self.__preprocess() @property def probabilities_or_weights (self) : return tuple(self._probabilities_or_weights) @property def probabilities (self) : return tuple(self._probabilities) @property def alias (self) : return tuple(self._alias) def __preprocess (self) : average = 1.0 / len(self._probabilities) small = [] large = [] for index, val in enumerate(self._probabilities): if val >= average: large.append(index) else : small.append(index) while small and large: less = small.pop() more = large.pop() self._probabilities[more] = self._probabilities[more] + self._probabilities[less] - average self._probabilities[less] = self._probabilities[less] * len(self._probabilities) self._alias[less] = more if self._probabilities[more] >= average: large.append(more) else : small.append(more) while small: self._probabilities[small.pop()] = 1.0 while large: self._probabilities[large.pop()] = 1.0 def next (self) : current_index = random.randint(0 , len(self._probabilities) - 1 ) current_number = random.random() if current_number < self._probabilities[current_index]: return current_index else : return self._alias[current_index]
C# 版本 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 public class AliasMethod4Sample { private readonly Random _random = new Random(); public Double[] ProbabilitiesOrWeights { get ; private set ; } public Int32[] Alias { get ; private set ; } public Double[] Probabilities { get ; private set ; } public AliasMethod4Sample (ICollection<Double> probabilitiesOrWeights ) { if (probabilitiesOrWeights == null ) { throw new ArgumentNullException("probabilitiesOrWeights" ); } if (probabilitiesOrWeights.Count == 0 ) { throw new ArgumentException("probabilitiesOrWeights is empty." , "probabilitiesOrWeights" ); } ProbabilitiesOrWeights = probabilitiesOrWeights.ToArray(); Alias = new Int32[ProbabilitiesOrWeights.Length]; var sum = ProbabilitiesOrWeights.Sum(); Probabilities = ProbabilitiesOrWeights.Select(x => x/sum).ToArray(); Init(); } public Int32 Next ( ) { var index = _random.Next(0 , Probabilities.Length); var val = _random.NextDouble(); return val < Probabilities[index] ? index : Alias[index]; } private void Init ( ) { var average = 1.0 /Probabilities.Length; var small = new Queue<Int32>(); var large = new Queue<Int32>(); for (var i = 0 ; i < Probabilities.Length; i++) { if (Probabilities[i] >= average) { large.Enqueue(i); } else { small.Enqueue(i); } } while (small.Count > 0 && large.Count > 0 ) { var less = small.Dequeue(); var more = large.Dequeue(); Probabilities[more] = Probabilities[more] + Probabilities[less] - average; Probabilities[less] = Probabilities[less]*Probabilities.Length; Alias[less] = more; if (Probabilities[more] >= average) { large.Enqueue(more); } else { small.Enqueue(more); } } while (large.Count > 0 ) { Probabilities[large.Dequeue()] = 1.0 ; } while (small.Count> 0 ) { Probabilities[small.Dequeue()] = 1.0 ; } } }
常用的离散算法 常用的离散算法,第三步改为二分查找。
python 版本 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 from __future__ import divisionimport randomimport bisectclass DiscreteMethod4Sample (object) : def __init__ (self, probabilities_or_weights) : assert probabilities_or_weights is not None assert isinstance(probabilities_or_weights, (list, tuple)) total = sum(probabilities_or_weights) discrete_table = [] for pw in probabilities_or_weights: if discrete_table: discrete_table.append((discrete_table[-1 ] + pw)/total) else : discrete_table.append(pw/total) self._discrete_table = discrete_table self._probabilities_or_weights = probabilities_or_weights @property def discrete_table (self) : return tuple(self._discrete_table) @property def probabilities_or_weights (self) : return tuple(self._probabilities_or_weights) def next (self) : current_probability = random.random() return bisect.bisect_right(self._discrete_table, current_probability)
C# 版本 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 public class DiscreteMethod4Sample { private readonly Random _random = new Random(); private readonly List<Double> _discreteTable; public Double[] ProbabilitiesOrWeights { get ; private set ; } public Double[] DiscreteTable { get { return _discreteTable.ToArray(); } } public DiscreteMethod4Sample (ICollection<Double> probabilitiesOrWeights ) { if (probabilitiesOrWeights == null ) { throw new ArgumentNullException("probabilitiesOrWeights" ); } if (probabilitiesOrWeights.Count == 0 ) { throw new ArgumentException("probabilitiesOrWeights is empty." , "probabilitiesOrWeights" ); } var rawProbabilitiesOrWeights = probabilitiesOrWeights.ToArray(); var discreteTable = new Double[probabilitiesOrWeights.Count]; discreteTable[0 ] = rawProbabilitiesOrWeights[0 ]; for (var i = 1 ; i < rawProbabilitiesOrWeights.Length; i++) { discreteTable[i] = discreteTable[i - 1 ] + rawProbabilitiesOrWeights[i]; } var total = rawProbabilitiesOrWeights.Sum(); ProbabilitiesOrWeights = rawProbabilitiesOrWeights; _discreteTable = discreteTable.Select(x => x/total).ToList(); } public Int32 Next ( ) { var index = _discreteTable.BinarySearch(_random.NextDouble()); return index < 0 ? ~index : index; } }