繁体   English   中英

如何选择 Javascript 中的加权随机数组元素?

[英]How to choose a weighted random array element in Javascript?

例如:数组中有四个项目。 我想随机得到一个,像这样:

array items = [
    "bike"    //40% chance to select
    "car"     //30% chance to select
    "boat"    //15% chance to select
    "train"   //10% chance to select
    "plane"   //5%  chance to select
]

上面的两个答案都依赖于会很快变慢的方法,尤其是被接受的方法。

function weighted_random(items, weights) {
    var i;

    for (i = 0; i < weights.length; i++)
        weights[i] += weights[i - 1] || 0;
    
    var random = Math.random() * weights[weights.length - 1];
    
    for (i = 0; i < weights.length; i++)
        if (weights[i] > random)
            break;
    
    return items[i];
}

截至 2020 年 12 月,我用这个解决方案替换了我的旧 ES6 解决方案,因为旧浏览器不支持 ES6,我个人认为这个解决方案更具可读性。

如果您更愿意使用具有itemweight属性的对象:

function weighted_random(options) {
    var i;

    var weights = [];

    for (i = 0; i < options.length; i++)
        weights[i] = options[i].weight + (weights[i - 1] || 0);
    
    var random = Math.random() * weights[weights.length - 1];
    
    for (i = 0; i < weights.length; i++)
        if (weights[i] > random)
            break;
    
    return options[i].item;
}

解释:

我制作了这张图表,展示了它是如何工作的:

图表显示了项目的权重以及它们如何相加并与随机数相互作用。如果没有这个图,这个解释不是很有帮助,所以如果它不会为你呈现,请随时在评论中问我任何关于它的问题。

该图显示了当给出权重为[5, 2, 8, 3]的输入时会发生什么。 通过对权重进行部分求和,你只需要找到第一个和随机数一样大的,那就是随机选择的项目。

如果在两个权重的边界上选择一个随机数,例如图中的715 ,我们选择较长的一个。 这是因为Math.random可以选择0而不能选择1 ,所以我们得到了一个公平的分布。 如果我们选择较短的, A可以在 18 次4选择3 20 1 ,从而赋予它比应有的更高的权重。

一些带有通配符处理的 es6 方法:

const randomizer = (values) => {
    let i, pickedValue,
            randomNr = Math.random(),
            threshold = 0;

    for (i = 0; i < values.length; i++) {
        if (values[i].probability === '*') {
            continue;
        }

        threshold += values[i].probability;
        if (threshold > randomNr) {
                pickedValue = values[i].value;
                break;
        }

        if (!pickedValue) {
            //nothing found based on probability value, so pick element marked with wildcard
            pickedValue = values.filter((value) => value.probability === '*');
        }
    }

    return pickedValue;
}

示例用法:

let testValues = [{
    value : 'aaa',
    probability: 0.1
},
{
    value : 'bbb',
    probability: 0.3
},
{
    value : 'ccc',
    probability: '*'
}]

randomizer(testValues); // will return "aaa" in 10% calls, 
//"bbb" in 30% calls, and "ccc" in 60% calls;

这是一种比其他答案建议的更快的方法......

您可以通过以下方式实现您想要的:

  1. 根据每个元素的概率将 0 到 1 段划分为每个元素的部分(例如,概率为 60% 的元素将占据该段的 60%)。
  2. 生成一个随机数并检查它落在哪个段。

步骤1

为概率数组制作一个前缀和数组,其中的每个值将表示其相应部分的结束位置。

例如:如果我们有概率:60% (0.6)、30%、5%、3%、2%。 前缀和数组将是: [0.6,0.9,0.95,0.98,1]

所以我们将有一个这样划分的段(大约): [ | | ||] [ | | ||]

第2步

生成一个介于 0 和 1 之间的随机数,并在前缀和数组中找到它的下限。 您将找到的索引是随机数所在的段的索引

以下是实现此方法的方法:

let obj = {
    "Common": "60",
    "Uncommon": "25",
    "Rare": "10",
    "Legendary": "0.01",
    "Mythical": "0.001"
}
// turning object into array and creating the prefix sum array:
let sums = [0]; // prefix sums;
let keys = [];
for(let key in obj) {
    keys.push(key);
    sums.push(sums[sums.length-1] + parseFloat(obj[key])/100);
}
sums.push(1);
keys.push('NONE');
// Step 2:
function lowerBound(target, low = 0, high = sums.length - 1) {
    if (low == high) {
        return low;
    }
    const midPoint = Math.floor((low + high) / 2);
  
    if (target < sums[midPoint]) {
        return lowerBound(target, low, midPoint);
    } else if (target > sums[midPoint]) {
        return lowerBound(target, midPoint + 1, high);
    } else {
        return midPoint + 1;
    }
}

function getRandom() {
    return lowerBound(Math.random());
}

console.log(keys[getRandom()], 'was picked!');

希望你觉得这很有帮助。 注意:(在计算机科学中)列表/数组中值的下限是大于或等于它的最小元素。 例如,数组: [1,10,24,99]和值 12。下限将是值为 24 的元素。当数组从最小到最大排序时(就像在我们的例子中)找到每个的下限使用二进制搜索(O(log(n)))可以非常快速地完成值。

这是一个 O(1) (常数时间)算法来解决你的问题。

生成一个从 0 到 99 的随机数(总共 100 个)。 如果在给定的子范围内有 40 个数字(0 到 39),那么随机选择的数字有 40% 的概率落在这个范围内。 请参阅下面的代码。

const number = Math.floor(Math.random() * 99); // 0 to 99
let element;

if (number >= 0 && number <= 39) {
    // 40% chance that this code runs. Hence, it is a bike.
    element = "bike";
}

else if (number >= 40 && number <= 69) {
    // 30% chance that this code runs. Hence, it is a car.
    element = "car";
}

else if (number >= 70 && number <= 84) {
    // 15% chance that this code runs. Hence, it is a boat.
    element = "boat";
}

else if (number >= 85 && number <= 94) {
    // 10% chance that this code runs. Hence, it is a train.
    element = "train";
}

else if (number >= 95 && number <= 99) {
    // 5% chance that this code runs. Hence, it is a plane.
    element = "plane";
}

还记得这个小学的数学原理吗? “指定分布中的所有数字被随机选择的概率均等。”

这告诉我们每个随机数在特定范围内出现的概率均等,无论该范围有多大或多小。

就是这样。 这应该工作!

我将我的解决方案添加为一种适用于较小数组(无缓存)的方法:

    static weight_random(arr, weight_field){
    
        if(arr == null || arr === undefined){
            return null;
        }
        const totals = [];
        let total = 0;
        for(let i=0;i<arr.length;i++){
            total += arr[i][weight_field];
            totals.push(total);
        }
        const rnd = Math.floor(Math.random() * total);
        let selected = arr[0];
        for(let i=0;i<totals.length;i++){
            if(totals[i] > rnd){
                selected = arr[i];
                break;
            }
        }
    return selected;

}

像这样运行它(提供数组和权重属性):

const wait_items =  [
    {"w" : 20, "min_ms" : "5000", "max_ms" : "10000"},
    {"w" : 20, "min_ms" : "10000", "max_ms" : "20000"},
    {"w" : 20, "min_ms" : "40000", "max_ms" : "80000"}
]   

const item = weight_random(wait_items, "w");
console.log(item);

ES2015 版Radvylf Programs 的回答

function getWeightedRandomItem(items) {
    const weights = items.reduce((acc, item, i) => {
        acc.push(item.weight + (acc[i - 1] || 0));
        return acc;
    }, []);
    const random = Math.random() * weights[weights.length - 1];
    return items[weights.findIndex((weight) => weight > random)];
}

和 ES2022

function getWeightedRandomItem(items) {
    const weights = items.reduce((acc, item, i) => {
        acc.push(item.weight + (acc[i - 1] ?? 0));
        return acc;
    }, []);
    const random = Math.random() * weights.at(-1);
    return items[weights.findIndex((weight) => weight > random)];
}

你当然可以。 这是一个简单的代码:

    // Object or Array. Which every you prefer.
var item = {
    bike:40, // Weighted Probability
    care:30, // Weighted Probability
    boat:15, // Weighted Probability
    train:10, // Weighted Probability
    plane:5 // Weighted Probability
    // The number is not really percentage. You could put whatever number you want.
    // Any number less than 1 will never occur
};

function get(input) {
    var array = []; // Just Checking...
    for(var item in input) {
        if ( input.hasOwnProperty(item) ) { // Safety
            for( var i=0; i<input[item]; i++ ) {
                array.push(item);
            }
        }
    }
    // Probability Fun
    return array[Math.floor(Math.random() * array.length)];
}

console.log(get(item)); // See Console.

暂无
暂无

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

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