编辑:我已经重写了这个问题,希望目标更加清晰。
这是对此问题here的一个扩展问题,我非常喜欢this answer中提供的功能。
在上面的答案中,人们能够设定击中极端的概率,较高的数字产生较低数字的概率,反之亦然。问题是我必须设置3组的概率。这些组是最低值(LV),最高值(HV)和中间值(MV)。但是,为了简化请求,我们可以考虑EVP=HVP=LVP
。
给定任何范围,HV / LV应基于指定的EVP出现,并且当您从每个极值的范围内进展/下降时,该范围中下一个值的概率将增加,或根据EVP和MVP之间的距离减少。
使用1-6的示例范围,1和6的加权为5%(EVP),概率差为1/6为5%,2/4为15%,3/4为30 %(MVP),总计100%。反之亦然,交换EVP和MVP应产生下图的反转。
这是一个我希望能传达给定例子预期结果的图像。
中等加权:
奖励:如果我能够单独设置HVP和LVP产生类似于下图的结果,那将是最优秀的(注意:图表不符合上述规范< / em>的)。
中等加权(奖金):
谢谢!
答案 0 :(得分:18)
因为我今天因为感冒而被困在家里:(我决定尝试为你解决这个问题。基本上你要求的是某种插值。我使用最简单的(线性)和这些是我的结果和代码。代码有点混乱,我可能会在即将到来的日子里修复它。
<?php
// this function interpolates $a to $b over $steps steps, starting from key $k
// this can be cleaned up significantly
function interpolate($a, $b, $steps, $k) {
@$per_step = abs($a - $b)/$steps; // suppress warnings in case of division by zero
if ($a > $b)
$decreasing = true;
else
$decreasing = false;
$final = array();
for ($i = 1; $i <= $steps-1; ++$i) {
if ($decreasing)
$final[$i+$k] = $a-=$per_step; // linear interpolation
else
$final[$i+$k] = $a+=$per_step; // linear interpolation
}
return $final;
}
// this function combines probability arrays after the interpolation occurs
// this may happen multiple times, think about 1, 3, 5. interpolation would have to occur
// from 1 -> 2 -> 3, and from 3 -> 4 -> 5.
function interpolateProbabilities ($nodes) {
$pNodes = array();
$pNodes = $nodes;
$keys = array_keys($nodes);
for ($i = 0; $i < count($keys); $i++) {
if ($keys[$i+1] - $keys[$i] != 1) {
$pNodes += interpolate($nodes[$keys[$i]], $nodes[$keys[$i+1]], $keys[$i+1] - $keys[$i], $keys[$i]);
}
}
ksort($pNodes);
return $pNodes;
}
// this generates a weighed random value and is pretty much copy-pasted from:
// http://w-shadow.com/blog/2008/12/10/fast-weighted-random-choice-in-php/
// it's robust and re-writing it would be somewhat pointless
function generateWeighedRandomValue($nodes) {
$weights = array_values($nodes);
$values = array_keys($nodes);
$count = count($values);
$i = 0;
$n = 0;
$num = mt_rand(0, array_sum($weights));
while($i < $count) {
$n += $weights[$i];
if($n >= $num) {
break;
}
$i++;
}
return $values[$i];
}
// two test cases
$nodes = array( 1 => 12, 5 => 22, 9 => 31, 10 => 35); // test 1
$nodes = array( 1 => 22, 3 => 50, 6 => 2, 7 => 16, 10 => 10); // test 2
$export = array();
// run it 1000 times
for ($i = 0; $i < 1000; ++$i) {
$export[generateWeighedRandomValue(interpolateProbabilities($nodes))]++;
}
// for copy-pasting into excel to test out distribution
print_r($export);
?>
我认为,结果正是您正在寻找的。 在以下情况下:
$nodes = array( 1 => 12, 5 => 22, 9 => 31, 10 => 35); // test 1
我得到了以下(最终)数组:
Array
(
[5] => 92
[7] => 94
[10] => 162
[8] => 140
[3] => 71
[6] => 114
[2] => 75
[4] => 69
[9] => 131
[1] => 52
)
即,1
应该有12%的时间,5
22%,9
31%和10
35%的时间。让它图表:
它看起来很有希望,但让我们尝试更疯狂的东西......
$nodes = array( 1 => 22, 3 => 50, 6 => 2, 7 => 16, 10 => 10); // test 2
在这种情况下,3
应该在50%的时间内出现,并急剧下降到6
。让我们看看发生了什么!这是数组(回想起来,我应该对这些数组进行排序):
Array
(
[4] => 163
[7] => 64
[2] => 180
[10] => 47
[1] => 115
[5] => 81
[3] => 227
[8] => 57
[6] => 6
[9] => 60
)
让我们看看图片:
看起来很有效:)
我希望我能解决你的问题(或者至少指出你正确的方向)。请注意,我的代码目前有许多规定。也就是说,您提供的初始节点必须具有高达100%的概率,否则您可能会获得一些不稳定的行为。
此外,代码有点乱,但概念相对简单。其他一些很酷的东西是尝试而不是使用线性插值,使用其他类型,这将给你更有趣的结果!
为了避免混淆,我只会准确地说明算法是如何工作的。
我给PHP一个$node
数组,其形式为integer => frequency in percentage
,最后看起来像array( 1 => 22, 3 => 50, 6 => 2, 7 => 16, 10 => 10)
,从上面是test 2
。
Test 2
基本上表示您希望5个控制节点分别放置在1, 3, 6, 7, and 10
,频率为22%, 50%, 2%, 16%, and 10%
。首先,我需要确切地看到 where 我需要进行插值。例如,我不需要在6
和7
之间执行此操作,但我执行需要在1
和3
之间执行此操作(我们需要插入2
)和7
以及10
(我们需要插入8
和9
)。
1 -> 3
之间的插值具有(3 - 1) - 1 = 1
个步骤,应插入原始数组中的key[2]
。 %
插值的值{1 -> 3
)为abs($a - $b) / $steps
,转换为%
的{{1}}减去1
的{{1}}的绝对值%
除以2
,在我们的情况下恰好等于steps + 1
。我们需要看看函数是增加还是减少(你好微积分)。如果函数增加,我们将添加步骤14
保持到新的插值数组,直到我们填充所有空点(如果函数正在减少,我们减去步骤{{1}由于我们只需填写一个字母,我们会返回%
(% value
)。
我们组合数组,结果为2 => 36
。程序内插22 + 14 = 36
,这是我们未明确声明的百分比值。
如果是(1 => 22, 2 => 36, 3 => 50, 6 => 2, 7 => 16, 10 => 10)
,则有2个步骤,步骤百分比为2
,来自7 -> 10
。函数正在递减,因此我们需要重复减去2
。最终插值数组为(16-10) / (3 + 1) = 2
。我们将所有阵列组合在一起。
下图显示绿色(初始值)和红色(插值)。您可能需要“查看图像”才能清楚地看到整个事物。你会注意到我使用2
,因为算法需要弄清楚我们是否应该在一段时间内增加或减少。
这段代码应该用更多的OOP范例编写。我使用数组键玩了很多(例如,我需要传递(8 => 14, 9 => 12)
所以一旦我从±
返回它们就更容易组合数组,因为它们会自动拥有正确的键。这只是一个PHP特性回想起来,我应该开始使用更易读的OOP方法。
这是我的最后一次编辑,我保证:)由于我喜欢使用Excel,这显示了一旦插入数字后百分比如何标准化。这一点很重要,特别是考虑到你的第一张照片,你所展示的内容在某种程度上是不可能的。
$k
interpolate($a, $b, $steps, $k)
您会注意到百分比显着衰减以适应插值。实际上你的第二张图看起来更像是这样:
在此图中,我称重Test 1
,你会看到阻尼效果的极端情况。毕竟,根据定义,百分比必须加起来为100!重要的是要意识到阻尼效应与极端之间的步数成正比。
答案 1 :(得分:4)
假设您可以处理百分比的整数,只需在0到99之间分配一个结果 - 例如0-9可能有1的结果,95-99可能有6的结果(给你的10%= 1和5%= 6的情况)。一旦你有了这个翻译功能(但是你实现了这个 - 你可以使用各种方法),你只需要生成一个0-99范围内的随机数并将其转换为结果。
你的问题在你想要的代码(甚至是哪种语言 - C#或PHP?)方面并不是很清楚,但希望这会有所帮助。
这里有一些C#代码可以让你在合理的范围内得到任何你喜欢的偏见 - 你不必将它表示为百分比,但你可以这样做:
static int BiasedRandom(Random rng, params int[] chances)
{
int sum = chances.Sum();
int roll = rng.Next(sum);
for (int i = 0; i < chances.Length - 1; i++)
{
if (roll < chances[i])
{
return i;
}
roll -= chances[i];
}
return chances.Length - 1;
}
例如,您可以使用
int roll = BiasedRandom(rng, 10, 10, 10, 10, 10, 50) + 1;
每1-5分钟的几率为10%,获得6分的几率为50%。
答案 2 :(得分:2)
C#中快速而肮脏的方式:
T PickWeightedRandom<T>(IEnumerable<Tuple<T,double>> items, Random r)
{
var sum = 0.0;
var rand = r.NextDouble();
return items.First(x => { sum += x.Item2; return rand < sum; }).Item1;
}
测试代码:
var values = new [] {
Tuple.Create(1, 0.05),
Tuple.Create(2, 0.15),
Tuple.Create(3, 0.3),
Tuple.Create(4, 0.3),
Tuple.Create(5, 0.15),
Tuple.Create(6, 0.05),
};
const int iterations = 1000;
var counts = new int[values.Length];
var random = new Random();
for (int i = 0; i < iterations; i++)
{
counts[PickWeightedRandom(values, random)-1]++;
}
foreach (var item in counts)
{
Console.WriteLine(item/(double)iterations);
}
输出(迭代次数= 1000000):
0.050224
0.150137
0.300592
0.298879
0.150441
0.049727
看起来像:
答案 3 :(得分:1)
生成非均匀随机数时的一般技术是使用rejection sampling。即使在这种情况下它可能无效,你仍然应该知道如何做到这一点,因为它适用于你提供的任何密度函数。
function random($density, $max) {
do {
$rand = lcg_value();
$rand2 = lcg_value() * $max;
} while ($density($rand) < $rand2);
return $rand;
}
$density
这里是一个密度函数,它接受0到1之间的浮点数作为参数,并返回一个小于$max
的值。对于您的示例,此密度函数可以是:
$density = function($x) {
static $values = array(
1 => 0.05,
2 => 0.15,
3 => 0.30,
4 => 0.30,
5 => 0.15,
6 => 0.05,
);
return $values[ceil($x * 6)];
};
然后一个示例调用是:
ceil(random($density, 0.3) * 6); // 0.3 is the greatest value returned by $density
// round and * 6 are used to map a 0 - 1 float to a 1 - 6 int.
如果您无法轻松计算分布的倒数,则拒绝采样尤其有用。在这种情况下,使用inverse transform sampling计算逆转很容易,这可能是更好的选择。但Jon's answer已经涵盖了这一点。
PS:上面的实现是通用的,因此使用0到1之间的随机值。通过构建一个仅适用于您的方法的函数,一切都变得更容易:
function random() {
static $values = array(
1 => 0.05,
2 => 0.15,
3 => 0.30,
4 => 0.30,
5 => 0.15,
6 => 0.05,
);
do {
$rand = mt_rand(1, 6);
$rand2 = lcg_value() * 0.3;
} while ($values[$rand] < $rand2);
return $rand;
}
random();
答案 4 :(得分:0)
首先,您需要描述当前的随机数生成器。在PHP的情况下,rand()函数返回一个漂亮的平面轮廓 - 因此不需要预处理。
重新映射输出分布函数,使其下面的区域为1,范围从0开始。然后计算其积分。存储积分(例如,作为值的数组)。然后,当您需要随机数matchnig时,首先从内置生成器获取0到1之间的随机数,然后在积分上找到Y坐标,其中X坐标是您生成的值。最后,将值缩放到所需范围(例如,如果查找介于0和10之间的值,则乘以10,如果查找介于-8和+8之间的值,则多次乘以16并减去8)。
如果您的随机数生成器不生成平面轮廓,那么最简单的方法是使用上述方法的反向将其转换为平面轮廓。
答案 5 :(得分:0)
我没有尝试过,但我认为这可能有用:
$random($probability)
{
$rnd = rand() / getrandmax();
foreach($probability as $num => $prob)
{
$rnd -= $prob;
if($rnd <=0)
return $num;
}
return -1; //this should never happen
}
并像这样称呼它(使用你的第二个例子):
$distribution = array(
1 => 0.10,
2 => 0.15,
3 => 0.30,
4 => 0.27,
5 => 0.14,
6 => 0.04);
$number = random($distribution);