Answers:
硬编码的概率解决方案的缺点是您需要在代码中设置概率。您无法在运行时确定它们。这也很难维护。
这是同一算法的动态版本。
这是以模板类的形式在Java中实现的示例实现,您可以为游戏使用的任何对象实例化。然后,您可以使用方法添加对象,.addEntry(object, relativeWeight)
并选择之前添加的条目之一.get()
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class WeightedRandomBag<T extends Object> {
private class Entry {
double accumulatedWeight;
T object;
}
private List<Entry> entries = new ArrayList<>();
private double accumulatedWeight;
private Random rand = new Random();
public void addEntry(T object, double weight) {
accumulatedWeight += weight;
Entry e = new Entry();
e.object = object;
e.accumulatedWeight = accumulatedWeight;
entries.add(e);
}
public T getRandom() {
double r = rand.nextDouble() * accumulatedWeight;
for (Entry entry: entries) {
if (entry.accumulatedWeight >= r) {
return entry.object;
}
}
return null; //should only happen when there are no entries
}
}
用法:
WeightedRandomBag<String> itemDrops = new WeightedRandomBag<>();
// Setup - a real game would read this information from a configuration file or database
itemDrops.addEntry("10 Gold", 5.0);
itemDrops.addEntry("Sword", 20.0);
itemDrops.addEntry("Shield", 45.0);
itemDrops.addEntry("Armor", 20.0);
itemDrops.addEntry("Potion", 10.0);
// drawing random entries from it
for (int i = 0; i < 20; i++) {
System.out.println(itemDrops.getRandom());
}
这是为您的Unity,XNA或MonoGame项目在C#中实现的同一类:
using System;
using System.Collections.Generic;
class WeightedRandomBag<T> {
private struct Entry {
public double accumulatedWeight;
public T item;
}
private List<Entry> entries = new List<Entry>();
private double accumulatedWeight;
private Random rand = new Random();
public void AddEntry(T item, double weight) {
accumulatedWeight += weight;
entries.Add(new Entry { item = item, accumulatedWeight = accumulatedWeight });
}
public T GetRandom() {
double r = rand.NextDouble() * accumulatedWeight;
foreach (Entry entry in entries) {
if (entry.accumulatedWeight >= r) {
return entry.item;
}
}
return default(T); //should only happen when there are no entries
}
}
这是JavaScript中的一个:
var WeightedRandomBag = function() {
var entries = [];
var accumulatedWeight = 0.0;
this.addEntry = function(object, weight) {
accumulatedWeight += weight;
entries.push( { object: object, accumulatedWeight: accumulatedWeight });
}
this.getRandom = function() {
var r = Math.random() * accumulatedWeight;
return entries.find(function(entry) {
return entry.accumulatedWeight >= r;
}).object;
}
}
优点:
相反:
O(n)
运行时复杂度)。因此,当您有大量项目并且经常绘制时,它可能会变慢。一种简单的优化方法是将最可能出现的项目放在首位,以便在大多数情况下算法尽早终止。您可以执行的更复杂的优化是利用对数组进行排序的事实并进行二等分搜索。这只需要O(log n)
时间。 O(n)
最坏情况的运行时)如果您只有少量的项目并且概率永远不变,则其他解决方案也可以。但是,如果有很多项目或更改了概率(例如,选择项目后将其删除),您将需要更强大的功能。
这是两个最常见的解决方案(以上两个库均包含在其中)
一个聪明的解决方案,如果您的概率是恒定的,则非常快(O(1)
!)。本质上,该算法从您的概率中创建一个2D飞镖板(“别名表”)并向其投掷飞镖。
唯一的问题是,如果您的概率发生变化,则需要重新生成别名表,这很慢。因此,如果您需要在拾取物品后将其删除,这不是您的解决方案。
另一个常见的解决方案是创建一个数组,其中每个项目存储其概率与之前所有项目的和。然后只需从[0,1)生成一个随机数,然后对该数字在列表中的位置进行二进制搜索。
此解决方案非常易于编码/理解,但是进行选择的速度比Walker的Alias方法要慢,并且更改概率仍然是O(n)
。我们可以通过将数组变成二叉搜索树来改进它,其中每个节点都跟踪其子树中所有项的概率之和。然后,当我们从[0,1)生成数字时,我们可以沿着树走去查找它代表的项。
这使我们O(log n)
可以选择一个项目并更改概率!这NextWithRemoval()
非常快!
结果
以下是上述库中的一些快速基准测试,比较了这两种方法
加权随机化器基准| 树| 表 -------------------------------------------------- --------------------------------- 加()x10000 + NextWithReplacement()x10:| 4毫秒| 2毫秒 加()x10000 + NextWithReplacement()x10000:| 7毫秒| 4毫秒 加()x10000 + NextWithReplacement()x100000:| 35毫秒| 28毫秒 (Add()+ NextWithReplacement())x10000(交错)| 8毫秒| 5403毫秒 加()x10000 + NextWithRemoval()x10000:| 10毫秒| 5948毫秒
正如您所看到的,对于静态(不变)概率的特殊情况,Walker的Alias方法的速度要快50-100%。但是在动态情况下,树快了几个数量级!
当项目库中的概率具有相当大的公分母并且需要非常频繁地从中提取时,可以使用此方法。
创建一个选项数组。但是将每个元素多次放入其中,每个元素重复项的数量与它出现的机会成正比。对于上面的示例,所有元素的概率都是5%的乘数,因此您可以创建一个包含20个元素的数组,如下所示:
10 gold
sword
sword
sword
sword
shield
shield
shield
shield
shield
shield
shield
armor
armor
armor
armor
potion
potion
然后,只需生成一个介于0和数组长度-1之间的随机整数,即可从该列表中选择一个随机元素。
缺点:
好处:
Epic Scepter of the Apocalypse
。这样的两层方法利用了这两种方法的优点。
[('gold', 1),('sword',4),...]
对所有权重求和,然后将一个随机数从0滚动到总和,然后迭代该数组并计算该随机数落在哪里(即a reduce
)。对于经常更新且无大内存占用的阵列,效果很好。
从加权集合中找到随机项的最简单方法是遍历一串if-else语句,其中每个if-else可能会增加,因为前一个未命中。
int rand = random(100); //Random number between 1 and 100 (inclusive)
if(rand <= 5) //5% chance
{
print("You found 10 gold!");
}
else if(rand <= 25) //20% chance
{
print("You found a sword!");
}
else if(rand <= 70) //45% chance
{
print("You found a shield!");
}
else if(rand <= 90) //20% chance
{
print("You found armor!");
}
else //10% chance
{
print("You found a potion!");
}
条件条件等于其机会加上所有先前条件机会的原因是因为先前条件已经消除了其成为那些项目的可能性。因此,对于盾牌的有条件攻击else if(rand <= 70)
,70等于盾牌的45%几率,再加上5%的金币几率和20%的剑几率。
好处:
缺点:
在C#中,您可以使用Linq扫描来运行累加器,以检查0到100.0f范围内的随机数并获取.First()来获取。就像一行代码一样。
所以像这样:
var item = a.Select(x =>
{
sum += x.prob;
if (rand < sum)
return x.item;
else
return null;
}).FirstOrDefault());
sum
是一个零初始化整数,a
是一个概率/项目结构/元组/实例的列表。rand
是该范围内先前生成的随机数。
这只是在范围列表上累计总和,直到超过先前选择的随机数,然后返回该项或null,如果随机数范围(例如100)错误地小于总加权范围,则将返回null ,并且所选的随机数不在总加权范围内。
但是,您会注意到OP中的权重与正态分布(贝尔曲线)非常匹配。我认为一般来说,您将不需要特定的范围,而是倾向于在钟形曲线周围或仅在递减的指数曲线上(例如)逐渐变细的分布。在这种情况下,您可以仅使用数学公式来生成项数组的索引,并按偏好概率的顺序进行排序。CDF 正态分布就是一个很好的例子
这里也是一个例子。
另一个示例是,您可以采用90度到180度之间的随机值来获取圆的右下象限,使用cos(r)取x分量,然后使用它来索引优先列表。
使用不同的公式,您可能会有一种通用的方法,您只需输入任意长度的优先级列表(例如N),然后通过乘法运算映射公式的结果(例如:cos(x)为0到1)(例如:Ncos(x )= 0到N)以获取索引。
概率不需要硬编码。这些项目和阈值可以一起放在一个数组中。
for X in items’range loop
If items (X).threshold < random() then
Announce (items(X).name)
Exit loop
End if
End loop
您仍然必须累积阈值,但是可以在创建参数文件时进行编码而不是对其进行编码。
random()
在循环中调用?
我完成了此功能:https : //github.com/thewheelmaker/GDscript_Weighted_Random 现在!在您的情况下,您可以像这样使用它:
on_normal_case([5,20,45,20,10],0)
它只给出0到4之间的数字,但是您可以将其放在可以放置项目的数组中。
item_array[on_normal_case([5,20,45,20,10],0)]
或在功能上:
item_function(on_normal_case([5,20,45,20,10],0))
这是代码。我可以在GDscript上实现,但是可以改变其他语言,还可以检查逻辑错误:
func on_normal_case(arrayy,transformm):
var random_num=0
var sum=0
var summatut=0
#func sumarrays_inarray(array):
for i in range(arrayy.size()):
sum=sum+arrayy[i]
#func no_fixu_random_num(here_range,start_from):
random_num=randi()%sum+1
#Randomies be pressed down
#first start from zero
if 0<=random_num and random_num<=arrayy[0]:
#print(random_num)
#print(array[0])
return 0+ transformm
summatut=summatut+arrayy[0]
for i in range(arrayy.size()-1):
#they must pluss together
#if array[i]<=random_num and random_num<array[i+1]:
if summatut<random_num and random_num<=summatut+arrayy[i+1]:
#return i+1+transform
#print(random_num)
#print(summatut)
return i+1+ transformm
summatut=summatut+arrayy[i+1]
pass
它的工作方式如下:on_normal_case([50,50],0)这给出0或1,两者的概率相同。
on_normal_case([50,50],1)这给出1或2,两者的概率相同。
on_normal_case([20,80],1)这给出1或2,它的变化更大,得到2。
on_normal_case([20,80,20,20,30],1)这给出了1-5的随机数,较大的数字比较小的数字更有可能。
on_normal_case([20,80,0,0,20,20,30,0,0,0,0,33,,45)此掷骰在数字45,46,49,50,51,56之间看到为零,它永远不会发生。
因此,该函数仅返回一个随机数,该随机数取决于该数组的长度和transformm的数量,并且数组中的int是可能会出现一个数字的概率权重,其中该数字是数组上的位置加上transformm的数量。