正如我在上面的评论中提到的那样,建议您在过度复杂化代码之前先对此进行简介。快速for
循环求和骰子比复杂的数学公式和表构建/搜索容易得多,而且易于修改。始终先进行概要分析,以确保您正在解决重要问题。;)
就是说,有两种主要方法可以一口气采集复杂的概率分布:
1.累积概率分布
有一个巧妙的技巧,可以仅使用一个统一的随机输入从连续概率分布中进行采样。它与累积分布有关,该函数回答“获得不大于 x 的值的概率是多少?”
此功能是不变的,从0开始,在其范围内上升到1。下面显示了两个六边形骰子的总和的示例:
如果您的累积分布函数具有方便计算的逆(或者您可以使用分段函数(如贝塞尔曲线)对其进行近似),则可以使用此函数从原始概率函数中进行采样。
逆函数处理将0和1之间的域分割为映射到原始随机过程的每个输出的间隔,每个区域的捕获区域与其原始概率相匹配。(对于连续分布,这是无限无限的。对于骰子掷骰子等离散分布,我们需要仔细舍入)
这是使用它来模拟2d6的示例:
int SimRoll2d6()
{
// Get a random input in the half-open interval [0, 1).
float t = Random.Range(0f, 1f);
float v;
// Piecewise inverse calculated by hand. ;)
if(t <= 0.5f)
{
v = (1f + sqrt(1f + 288f * t)) * 0.5f;
}
else
{
v = (25f - sqrt(289f - 288f * t)) * 0.5f;
}
return floor(v + 1);
}
比较一下:
int NaiveRollNd6(int n)
{
int sum = 0;
for(int i = 0; i < n; i++)
sum += Random.Range(1, 7); // I'm used to Range never returning its max
return sum;
}
明白我在代码清晰度和灵活性方面的不同吗?幼稚的方法可能具有循环的幼稚性,但它简短而简单,其作用显而易见,并且易于缩放至不同的裸片尺寸和数量。更改累积分布代码需要一些不平凡的数学运算,并且很容易破坏并导致意外结果,而不会出现任何明显的错误。(我希望以上没有做过)
因此,在消除明确的循环之前,请绝对确定这确实是一个性能问题,值得为此牺牲。
2.别名方法
当您可以将累积分布函数的反函数表达为简单的数学表达式时,累积分布方法会很好地工作,但这并不总是那么容易甚至不可能。离散分布的可靠替代方法是Alias方法。
这样,您仅使用两个独立的,均匀分布的随机输入即可从任意离散的概率分布中进行采样。
它的工作原理是采用类似于左下方的分布(不用担心面积/权重之和不等于1,因为Alias方法我们关心相对权重)并将其转换为表格,例如正确的位置:
- 每个结果都有一栏。
- 每列最多分为两个部分,每个部分与原始结果之一相关。
- 保留每个结果的相对面积/权重。
(图基于这篇关于采样方法的优秀文章的图片)
在代码中,我们用两个表(或具有两个属性的对象表)表示这一点,该表表示从每一列中选择替代结果的可能性,以及该替代结果的标识(或“别名”)。然后我们可以像这样从分布中采样:
int SampleFromTables(float[] probabiltyTable, int[] aliasTable)
{
int column = Random.Range(0, probabilityTable.Length);
float p = Random.Range(0f, 1f);
if(p < probabilityTable[column])
{
return column;
}
else
{
return aliasTable[column];
}
}
这涉及一些设置:
计算每个可能结果的相对概率(因此,如果您滚动1000d6,我们需要计算将每个总和从1000转换为6000的方法的数量)
建立一对表格,每个结果都有一个条目。完整的方法超出了此答案的范围,因此,我强烈建议参考对“别名方法”算法的解释。
存储这些表,并在每次需要通过此发行版进行新的随机模版印刷时参考它们。
这是时空的权衡。预计算步骤有些穷举,我们需要根据结果的数量来分配内存(尽管即使是1000d6,我们也要讲个位数的千字节,所以没有什么可以失去睡眠的),但是作为交换我们的采样无论我们的分布多么复杂,它都是固定时间的。
我希望这些方法中的一种可能有用(或者我已经说服您,朴素的方法的简单性值得花时间循环);)