一个关于权重随机数的简单研究——抽奖示例(一)

前言

某一天的我在打游戏,卡池更新,充了648的我加上一些上版本的存货,几十发下卡池,一点水花都没溅起来,我对这个概率感到疑惑,太非了吧,但仔细一看卡池说明,只有百分之一点几的出货率,加上80发大保底、10发小保底的机制,期望出的次数大概在60±5之间,又能理解。稍微了解了一下,发现除开保底机制,实际上也是伪概率的,不会让你吃到大保底,也不会让你很容易就出。
为啥一定要伪概率,不用真概率呢,以前听过一个说法,真概率+不保底=无限抽,运气差到一定程度,只有靠保底来拯救。
然后我抽了点时间,研究了一下“抽奖”或者是“权重随机数抽取”的一些有趣内容。

随机数的产生

一般的编程语言,都有随机数生成方法,直接定义和调用即可,部分会有语言需要定义一个变动的“种子”来避免生成的随机数规律性“雷同”或重现。
本文用的编程语言是JavaScript,编译及运行工具是edge浏览器,别疑惑为啥不用稍微专业点IDE,很简单,没必要,只是测试算法而非独立项目的话,浏览器本地引擎就足以胜任了。
关于权重,这里不做详细介绍,在本次讨论中,只简单说明为:权重越大,越容易被抽到。

最简单的随机数调用

生成范围为[0,1) 的随机数,默认浮点型。

//随机数生成器
const RNG = ()=>{
    let ra = Math.random();
    console.log(ra);
}

//输出10次
//0.06631025861544249 0.683283041168188 0.7586949449825899 0.42456507700636004 0.18698114218603212 0.5668754365527569 0.10401423479870187 0.6632089775344485 0.3369250172551357 0.3127754552312254

生成范围为[0,100) 的随机数,向下取整。

//随机数生成器
const RNG = ()=>{
    let ra = Math.floor(Math.random() * 100);
    return ra;
}

//输出10次
//67 90 4 45 15 90 0 20 99 17

一个稍微实用点的权重随机数生成器

下面,我们直接上点难度,根据一个权重数组,来取随机数,数组数据格式为[{weigth(number),id(Number or Any),obj(Object)}],代码及测试效果我先放出来,具体实现逻辑后面再介绍。

//随机数生成器
const WeigthRNG = (randArr)=>{
    var randAdd = 0;
    if(randArr && randArr.length && randArr.length>0){
        for(let i = 0; i<randArr.length; i++){
            let el = randArr[i];
            randAdd += el.weigth;
        }
    }
    else return 0;
    //范围为[0,n)
    var ra = Math.floor(Math.random() * randAdd);
    var randAddTemp = 0;
    for(let i = 0; i<randArr.length; i++){
        let el = randArr[i];
        randAddTemp += el.weigth;
        if(randAddTemp > ra) return el;
    }
    return 0;
}

生成一个权重数组

//生成测试输入数据
var testArr = [];
for(let i=0; i<5; i++){
    let weigth = Math.floor(Math.random() * 10);
    testArr.push({weigth:weigth,id:i, obj:null});
}
console.log('intput:',testArr);
//得到权重数组:[{"weigth":1,"id":0,"obj":null},{"weigth":7,"id":1,"obj":null},{"weigth":8,"id":2,"obj":null},{"weigth":1,"id":3,"obj":null},{"weigth":7,"id":4,"obj":null}]

输出10次

//测试输出10次
var resOutArr = [];
for(let i=0; i<10; i++){
    var resOut = WeigthRNG(testArr);
    resOutArr.push(resOut.id);
}
//输出结果
console.log('output:',resOutArr);
//[2,1,2,4,4,1,2,4,2,1]

光输出不太看得出来权重的体现,加上数据分析。

//分组计次计算分布情况,验证权重是否生效
let countObj = resOutArr.reduce((prev, item) => {
  if (item in prev) {
    prev[item]++;
  } else {
    prev[item] = 1;
  }
  return prev;
}, {});
console.log('count result:');
console.log(countObj);

//输出结果
//{1: 3, 2: 4, 4: 3}

我输入的有5个id,10次打印只出了3个,没出现的是是0和3,这也正常,它们权重远低于其他的,下面我们直接输出1000次,看效果咋样,是否符合权重比例。

//单次结果就不放了,数量太多了,直接放分布情况。
//{0: 35, 1: 303, 2: 340, 3: 46, 4: 276}

从0~5,输入的权重比为1:7:8:1:7,1000次输出后的权重比约为1:8.66:9.71:1.31:7.89,看起来有些出入,我们输出10000次试一下。

//{0: 465, 1: 2891, 2: 3317, 3: 395, 4: 2932}

再换算一下权重比列为1:6.22:7.13:0.85:6.31,貌似还是有出入,加量!!!

/ 0 1 2 3 4
10w 4101 29148 33387 4058 29306
20w 8309 58170 66732 8377 58412
30w 12497 86956 100766 12446 87335
40w 16611 116728 133757 16566 116338
50w 20862 146335 166505 20668 145630
60w 24808 175176 200363 24952 174701
70w 28939 204065 233642 29145 204209
80w 33424 233324 266602 33173 233477
90w 37532 262342 300171 37653 262302
100w 41408 292364 333673 41279 291276

比例换算

/ 0 1 2 3 4
weight 1 7 8 1 7
10w 1 7.11 8.14 0.99 7.15
20w 1 7 8.03 1.01 7.03
30w 1 6.96 8.06 1 6.99
40w 1 7.03 8.05 1 7
50w 1 7.01 7.98 0.99 6.98
60w 1 7.06 8.08 1.01 7.04
70w 1 7.05 8.07 1.01 7.06
80w 1 6.98 7.98 0.99 6.99
90w 1 6.99 8 1 6.99
100w 1 7.06 8.06 1 7.03

结论,只要样本量够大,就能够足够接近最开始设定的那个权重值,算法没问题。

代码解析

代码中使用的操作对象是普通数组,严格来说,使用“字典”(js中用map)类型,会更加好处理,毕竟字典天然存在“键”,且能通过键去访问值,这里之所以不用,是为了降低了入门的。

权重随机数生成器

这个代码的逻辑实际非常简单,构造一个比较大的符合比例的随机域,在这个随机域里面取数,再匹配权重序列即可。相当于构造一个足够长的分段线段,在里面随便标记一个点,判断这个点是位于那个分段线段内。

我们把代码再po出来,逐行加上注释:

const WeigthRNG = (randArr)=>{
    //随机域放大系数
    var randAdd = 0;
    //通过累加权重得到随机域放大系数
    if(randArr && randArr.length && randArr.length>0){
        for(let i = 0; i<randArr.length; i++){
            let el = randArr[i];
            randAdd += el.weigth;
        }
    }
    //默认值
    else return 0;
    //Math.random()默认范围为[0,1),加上系数后范围为[0,randAdd),Math.floor()可向下取整
    var ra = Math.floor(Math.random() * randAdd);
    //临时的累加数
    var randAddTemp = 0;
    //判断生成的随机数位于那个权重段内
    for(let i = 0; i<randArr.length; i++){
        let el = randArr[i];
        randAddTemp += el.weigth;
        if(randAddTemp > ra) return el;
    }
    //默认值
    return 0;
}

输入对象构造及处理

输入对象在本系列是非常重要的东西,做些许处理,即可实现很多需求。务必要注意一点的是,生成器每一次生成数据,都是独立的,与上一次无关,所以,输入端在生成后一个随机数后,需要再根据这个随机数调整输入策略,以完成个性化的内容,比如设置保底、限量抽取等,在下一节会详细展示,本节不做分析。

分析方式

一般来说,涉及到大量数据需要处理和验证的情况,没有编程基础的话,用excel录入数据进行计算分析是个比较好的选择。我们计算数据使用程序来,展示用excel(markdown)。为了方便,我用到了一个相对高级数组方法reduce()来分析数组数据,其原型如下,这里不细讲用法,大家有兴趣可以去网上查一下,附一个官方介绍说明:reduce()方法

array.reduce(function(accumulator, currentValue, currentIndex, arr), initialValue);
/*
  accumulator:  必需。累计器
  currentValue: 必需。当前元素
  currentIndex: 可选。当前元素的索引;
  arr:          可选。要处理的数组
  initialValue: 可选。传递给函数的初始值,相当于accumulator的初始值
*/

至于markdown中表格的用法,可参考markdown表格。同时推荐一个在线表格转换工具,可以把ecxel格式表格,快速转换为markdown格式,参考excel to markdown

下期预告

给“抽奖”加奖池限制机制

posted @ 2024-01-10 12:02  SoloShine  阅读(37)  评论(0编辑  收藏  举报