生成随机加权值

时间:2010-10-27 06:12:50

标签: c# php random

编辑:我已经重写了这个问题,希望目标更加清晰。

这是对此问题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应产生下图的反转。

这是一个我希望能传达给定例子预期结果的图像。

中等加权:

Middle Weighted Graph

奖励:如果我能够单独设置HVP和LVP产生类似于下图的结果,那将是最优秀的(注意:图表不符合上述规范< / em>的)。

中等加权(奖金):

Middle Weighted Bonus Graph

谢谢!

6 个答案:

答案 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%的时间。让它图表: graph 1

它看起来很有希望,但让我们尝试更疯狂的东西......

$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
)

让我们看看图片:

alt text

看起来很有效:)

我希望我能解决你的问题(或者至少指出你正确的方向)。请注意,我的代码目前有许多规定。也就是说,您提供的初始节点必须具有高达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 我需要进行插值。例如,我不需要在67之间执行此操作,但我执行需要在13之间执行此操作(我们需要插入2)和7以及10(我们需要插入89)。

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,因为算法需要弄清楚我们是否应该在一段时间内增加或减少。

alt text


这段代码应该用更多的OOP范例编写。我使用数组键玩了很多(例如,我需要传递(8 => 14, 9 => 12)所以一旦我从±返回它们就更容易组合数组,因为它们会自动拥有正确的键。这只是一个PHP特性回想起来,我应该开始使用更易读的OOP方法。


这是我的最后一次编辑,我保证:)由于我喜欢使用Excel,这显示了一旦插入数字后百分比如何标准化。这一点很重要,特别是考虑到你的第一张照片,你所展示的内容在某种程度上是不可能的。

$k alt text interpolate($a, $b, $steps, $k) alt text

您会注意到百分比显着衰减以适应插值。实际上你的第二张图看起来更像是这样:

alt text

在此图中,我称重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);