繁体   English   中英

加权概率随机选择数组

[英]Weighted probability random choice array

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

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

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

我怎样才能做到这一点?

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

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

  • 增加你想展示更多的项目的权重
  • 减少你想少展示的项目的权重。

您可以查看以下演示:

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

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

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

  • 您表示在数组中选择某些元素的概率。 因此,如果“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),"%"); };

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

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

 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())]; }); };
 <button onclick="button_click();">Run test</button>

如您所见,测试速度非常快。 我的平均约为 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()); }); };
 <button onclick="button_click();">Run test</button>

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

希望您发现效率有所帮助! 如果您想使用此算法,只需使用我给您的第一个片段,那里的所有内容都比前几个片段更具可读性。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM