【问题标题】:Weighted probability random choice array加权概率随机选择数组
【发布时间】:2022-01-08 19:42:19
【问题描述】:

我有一个数组并返回随机值。

const array = [ 1, 2 ,3 ,4 ,5, 6, 7, 8]
const rand = array[~~(Math.random() * array.length)]

我想返回数组的一个随机元素,但具有较高索引(索引)不太可能返回的加权概率。即 8 比 1 更不可能返回。

我怎样才能做到这一点?

【问题讨论】:

标签: javascript statistics probability


【解决方案1】:

这是实现这一目标的有效方法。此方法使用二进制搜索(尽管已根据您的需要进行了修改)。

以下是其工作原理的摘要:

  • 您表示某些元素在数组中被选中的概率。因此,如果您的概率“A”为 50%,“B”为 20%,C 为 10%,D 为 5%,E 为 5%,F 为 0.1%,G 为 9.9%,这将是数组中的[.5, .2, .1, .05, .05, .001, .099]。然而这并不好,因为我们不能在二进制搜索中使用它,因为它没有排序 - 但是如果我们对其进行排序,概率将不再对应于我们的字母数组 ([A,B,C,D,E,F,G])。因此,我们需要将每个概率相加,直到得到 1。现在概率数组如下所示:[.5, .7, .8, .85, .9, .901, 1]。现在已经排好序了,仍然对应上面的字母数组。
  • 现在我们在概率数组中创建一个介于 0 和最大值之间的随机小数。 Math.random() 非常适合。
  • 现在我们看看概率数组中的哪个值最接近这个分数。但有一个问题 - “最接近”的值不能小于分数。
  • 一旦我们有了这个“最接近”值的索引,我们就可以使用相同的索引从字母数组中选择一个值。下面是一个 JavaScript 示例:

function find(arr, x , start=0, end=arr.length) {
  if(end < start) return -1;
  else if(end == start) return end;
  const mid = Math.floor((start + end) / 2);  
  if(arr[mid] === x) return mid+1;
  else if(arr[mid] < x) return find(arr, x, mid+1, end);
  else
    return find(arr, x, start, mid);
};


const table_of_corresponding_probabilities = [.5,.7,.8,.85,.9,.901,1];
const values_to_pick_from = ["A", "B", "C", "D", "E", "F", "G"];


function weighted_random_pick(items, weights) {
    return items[find(weights, Math.random())];
};

console.log(weighted_random_pick(values_to_pick_from, table_of_corresponding_probabilities));

因此,有了这些概率,我们应该在 50% 的时间里得到 As,在其余时间里得到其他字母。下面是测试上述算法随机性的测试:

function find(arr, x , start=0, end=arr.length) {
  if(end < start) return -1;
  else if(end == start) return end;
  const mid = Math.floor((start + end) / 2);  
  if(arr[mid] === x) return mid+1;
  else if(arr[mid] < x) return find(arr, x, mid+1, end);
  else
    return find(arr, x, start, mid);
};
const prob = [.5,.7,.8,.85,.9,.901,1];
const vals = ["A", "B", "C", "D", "E", "F", "G"];
const results = {A:0, B:0, C:0, D:0, E:0, F:0, G:0};
const times_it_ran = 160000;
for(let i = 0; i<times_it_ran; i++) {
    results[vals[find(prob, Math.random())]]++
};
for(letter in results) {
    console.log(letter+":",(results[letter]/(times_it_ran/100)).toFixed(3),"%");
};

当您运行上述 sn-p 时,您应该会发现每个字母被选中的次数百分比接近于该字母被选中的预期概率。当然它永远不会绝对相等,因为毕竟它是随机的(或至少是伪随机的)。

好的,速度和效率如何?让我们也测试一下:

function find(arr, x , start=0, end=arr.length) {
  if(end < start) return -1;
  else if(end == start) return end;
  const mid = Math.floor((start + end) / 2);  
  if(arr[mid] === x) return mid+1;
  else if(arr[mid] < x) return find(arr, x, mid+1, end);
  else
    return find(arr, x, start, mid);
};
const array_length = 330000;
const probs = Array.apply(null, {length: array_length}).map((x,i) => (i??0)/(array_length-1)); // Note: this way of creating an array means that each value has an equal chance of getting picked but the array is still very long;
const vals = Array.apply(null, {length: array_length}).map(Function.call, String);
const time = func => {
    console.time("timer");
    func();
    console.timeEnd("timer");
};

// Now time the time it takes to search within this LONG array:
function button_click() {
    var x = time(() => {
        vals[find(probs, Math.random())];
    });
};
&lt;button onclick="button_click();"&gt;Run test&lt;/button&gt;

如您所见,测试非常快。我的平均时间约为 2 毫秒。但是,这只在长度为3.3e5 的数组中搜索。这是我选择的值,否则会出现范围错误(内置函数 Array.apply 的限制)。所以在这里我做了同样的测试,但是使用了不同的方法来生成海量数组(一个 for 循环......我知道这可能是最糟糕的方法,但它确实有效)。

function find(arr, x , start=0, end=arr.length) {
  if(end < start) return -1;
  else if(end == start) return end;
  const mid = Math.floor((start + end) / 2);  
  if(arr[mid] === x) return mid+1;
  else if(arr[mid] < x) return find(arr, x, mid+1, end);
  else
    return find(arr, x, start, mid);
};


const len = 75e6; // 75 million elements in this array!

let probs = [];
for(let i = 0; i < 1; i+=(1/len)) {
    probs.push(i);
};

const time = func => {
    console.time("timer");
    func();
    console.timeEnd("timer");
};

// Now time the time it takes to search within this LONG array:
function button_click() {
    var x = time(() => {
        find(probs, Math.random());
    });
};
&lt;button onclick="button_click();"&gt;Run test&lt;/button&gt;

那么在使用 7500 万个元素运行这个测试之后,我们发现了什么? 第一个测试比我们之前运行的测试(使用 3.3e5 个元素)稍微慢一些,其余的平均在 2ms 到 2.25ms 左右。所以这比使用 227 TIMES 更少元素的数组搜索要慢 (2+2.25)/2 - avg time from last tests = 2.125-2 = 0.125 0.125ms。这就是二进制搜索有效的程度。实际上,我想建议,0.125ms 延迟的一部分可能是由于构建阵列的糟糕方法导致 CPU 内核非常热。是的,我说的是我们必须完成 7500 万次迭代才能创建该数组!

希望您发现效率对您有所帮助!如果你想使用这个算法,只需使用我给你的第一个 sn-p,那里的所有内容都比最后几个 sn-ps 更具可读性。

【讨论】:

    【解决方案2】:

    您可以使用通过加权概率将原始数组克隆到新数组的技巧。

    您可以通过以下方式对其进行修改:

    • 增加要显示更多的项目的权重
    • 减少要减少显示的项目的重量。

    您可以查看以下演示:

    const array = [ 1, 2 ,3 ,4 ,5, 6, 7, 8 ]
    const weight = [ 8, 7, 6, 5, 4, 3, 2, 1 ];
    
    let randomArray = [];
    array.forEach((item, index) => {
       var clone = Array(weight[index]).fill(item);
       randomArray.push(...clone);
    });
    
    const result = randomArray[~~(Math.random() * randomArray.length)]
    
    console.log('random value:', result);

    【讨论】:

    • 这是极其低效和缓慢的。我用 15*300000(=450 万)个元素(array 中的 15 个元素和每个元素的 300000 个权重)尝试了你的方法。我运行了几次,平均花费了 410 毫秒 - 即将近半秒!只是为了比较,我对一个包含双倍元素(= 9.0 mil 元素)的数组进行了二进制搜索,它花费的时间不超过 2 毫秒。比较 2ms 和 410ms。如果你给我几分钟,我会发布一个解释我使用的方法的答案
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-10-20
    • 2020-09-14
    • 1970-01-01
    • 2013-06-19
    • 2021-04-27
    • 1970-01-01
    相关资源
    最近更新 更多