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

在上一节中,简单说了一下权重随机数的产生,且验证了其合理性。这一节,我们简单探讨一下权重随机数的几个简单应用。

权重置零的玩法

权重随机数中有一个特殊情况,如果我们把权重数组中的某一项权重设置为0的话,会出现什么情况?

//测试权重数组,无权重为0的情况
var 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}];
//输出1000次
//{0: 31, 1: 318, 2: 338, 3: 44, 4: 269}

//测试权重数组,有权重为0的情况
var testArr = [{"weigth":0,"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}];
//输出1000次
//{1: 290, 2: 343, 3: 46, 4: 321}

结论很简单,权重为0就不会被抽出来。

总量不限,指定内容限量但不影响出货概率“奖池”——限量但不限权重

根据这个特性,我们可以很容易设计出一个带有“限量奖池”机制的抽取模型,而且不需要修改生成器的代码,只需要在抽取的时候,给传入参数加一个计量的字段即可。

//带“卡池限量”的情况
testArr = [{"weigth":1,"id":0,"count":10,"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}];
var resOutArr = [];
for(let i=0; i<1000; i++){
    var resOut = WeigthRNG(testArr);
    if(resOut.count>0){
        var index = testArr.findIndex((el)=>el.id===resOut.id);
        if(index>-1 && testArr[index].count!=null){
            testArr[index].count--;
            if(testArr[index].count === 0)testArr[index].weigth = 0;
        }
    }
    resOutArr.push(resOut.id);
}

//输出1000次
//{0: 10, 1: 300, 2: 341, 3: 48, 4: 301}

可见,计量参数加入之后,通过计量的改变,来影响权重,使其置0,便实现了奖池的限量机制。

但仔细分析之后,发现一个问题或是一种机制,限量内容被抽取完前,权重并没有改变。这种机制可以做出“一个奖池中某样限定物品出货概率极大,但出了一次后,就不会再出了” 这样的抽奖游戏,比如限定抽数下,必定出某一个物品。

总量有限可以被掏空“奖池”——权重既是限量

限量但不改变权重的“奖池”,一般只会在新手第一发“10连”出现,概率很反直觉,但过了之后就是稍微正常点的抽取了。上面的“奖池”只有特定奖品是限量的,而其他是不限量的,我们进一步,来实现一个总奖池中所有奖品都有限的情况,抽中了某一个内容后,下次抽到该内容的机会就会降低,直至奖池被掏空。

这种设计其实不难,同样不需要修改生成器的代码,只需要在输入做下手脚,直接将权重字段当作奖池数量即可。

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}];
var resOutArr = [];
for(let i=0; i<100; i++){
    var resOut = WeigthRNG(testArr);
    if(resOut.weigth>0){
        var index = testArr.findIndex((el)=>el.id===resOut.id);
        if(index>-1 && testArr[index].weigth!=null){
            testArr[index].weigth--;
        }
    }
    resOutArr.push(resOut.id);
}

//输出100次
//{0: 1, 1: 7, 2: 8, 3: 1, 4: 7, undefined: 76}
//打印抽取的内容
//[4,2,2,1,2,1,1,4,1,4,1,0,2,2,2,4,2,4,4,3,2,1,4,1,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null]

很明显,当所有奖品都被抽出来后,就抽不到东西了。

权重之外的额外要素

不限量但可保底的“奖池”——减少权重的影响

保底看起来可能有点复杂,但实际上实现起来也很简单,同样只需要改造下输入参数(不得不说,动态类型确实方便,静态类型的话,还得改定义),即可实现效果。

为了更方便判断保底是否触发,我们对输出方法做点小调整,让其能显示更多内容。flag表示保底项标记,minGFlag表示触发保底标记。设定9个奖项,保底次数为10次。

//总量不限、指定内容不限但可保底
//设置仅id为0和3的进行保底
var testArr = [{"weigth":1,"id":0,"flag":1,"obj":null},{"weigth":7,"id":1,"obj":null},{"weigth":8,"id":2,"obj":null},{"weigth":1,"id":3,"flag":1,"obj":null},{"weigth":7,"id":4,"obj":null},{"weigth":7,"id":5,"obj":null},{"weigth":7,"id":6,"obj":null},{"weigth":7,"id":7,"obj":null},{"weigth":7,"id":8,"obj":null}];
var resOutArr = [];
//保底次数
var minG = 10;
//保底内容
var minTestArr = [];
//保底临时数组
var minGArr = [];
var minGTemp = minG;
//提取保底内容
for(let i=0; i<testArr.length; i++){
    if(testArr[i] && testArr[i].flag===1){
        //使用扩展运算符是避免浅复制
        minTestArr.push({...testArr[i]});
    }
}
//抽取
for(let i=0; i<10; i++){
    var resOut = WeigthRNG(testArr);
    minGTemp--;
    //没触发保底
    if(resOut.flag===1){
        minGArr = [];
        minGTemp = minG;
    }
    else{
        minGArr.push(resOut);
    }
    //触发保底,强制进行保底
    if(minGTemp===0){
        minGTemp = minG;
        minGArr = [];
        resOut = WeigthRNG(minTestArr);
        resOut.minGFlag = 1;
    }
    resOutArr.push(resOut);
}

console.log('intput:',testArr);
console.log('output:',resOutArr);

//分组计次计算分布情况,验证权重是否生效
var tt = [];
for(let i=0; i<resOutArr.length; i++){
    tt.push({...resOutArr[i]});
}
let countObj = tt.reduce((prev, item) => {
    if(!prev[item.id]) prev[item.id]=item;
    if(!prev[item.id].count)prev[item.id].count=0;
    if (prev[item.id]!=null) {
        prev[item.id].count++;
        if(item.minGFlag && prev[item.id].minGFlag && prev[item.id].count>1)prev[item.id].minGFlag++;
    } else {
        prev[item.id].count = 1;
    }
    return prev;
}, {});
console.log('count result:');
console.log(countObj);

先来两个10连试下水。

weigth id obj flag isMinGFlag
7 8 null
8 2 null
7 5 null
7 1 null
7 4 null
8 2 null
1 0 null 1
7 6 null
7 4 null
1 3 null 1
weigth id obj flag isMinGFlag
7 1 null
7 1 null
8 2 null
7 1 null
7 4 null
8 2 null
7 1 null
7 7 null
7 8 null
1 0 null 1 1

可以看到我第一个10连运气还不错,出了俩不一样的,还没触发保底,第二发10则是触发了保底,看似满足预定的期望,但结论不能这么早下,这两次是独立的前后不影响,还得扩大样本量,直接连续个抽100次。

表格太长,点击展开
weigth id obj flag minGFlag
7 1
7 6
7 5
7 1
7 6
7 6
7 7
7 5
7 5
1 3 1 1
7 1
8 2
8 2
7 4
8 2
7 4
7 6
7 7
8 2
1 3 1 1
8 2
1 0 1
8 2
7 5
7 1
7 8
7 8
7 7
8 2
7 5
7 5
1 0 1
7 7
7 4
7 8
7 6
7 1
7 7
7 8
7 8
7 5
1 3 1 1
7 8
8 2
8 2
7 1
7 4
7 1
7 1
8 2
7 4
1 0 1 1
8 2
1 0 1
7 5
7 7
8 2
7 1
7 4
7 6
8 2
7 1
7 8
1 0 1 1
7 1
7 5
8 2
7 7
7 5
7 1
7 8
7 7
7 8
1 0 1 1
7 8
7 1
7 5
7 7
7 4
7 1
7 8
7 7
7 8
1 0 1 1
7 7
7 6
7 6
7 6
8 2
7 1
8 2
8 2
8 2
1 3 1 1
7 7
7 8
7 6
7 5
8 2
7 8
统计分析结果
weigth id flag obj count minGFlag
1 0 1 7 4
7 1 15
8 2 19
1 3 1 4 4
7 4 7
7 5 12
7 6 10
7 7 12
7 8 14

简单分析可以得到,确实有保底项被触发了,按照权重概率来说,限定项的出货概率为2/52=0.385,不加保底的话,100发大概只会出4发,加了后出了11发,本次没触发保底的限定项只有3次,这不就应了那句玄不救非,氪不改命。值得一提的是,表格中出现了第10发出货了,但没保底,因为代码是先判断第10发有没有出货,如果没出货再触发保底,出货了就仅仅重置保底。

当然,严谨一定,把数据再做大一些,抽取10000次。

weigth id flag obj minGFlag count
1 0 1 379 575
7 1 1244
8 2 1447
1 3 1 413 616
7 4 1231
7 5 1208
7 6 1226
7 7 1224
7 8 1229
**** 总样本量 10000
**** 保底次数 792 限定次数 1191
**** 基础出货概率 4% 综合出货概率 12%
**** 保底出货率 8% 触发保底概率 66%

通过表格中的计算,可以很容易看出,保底机制添加之后,出货概率提高了两倍,当然,实际提升的概率与保底次数具备相关性,这里我们不做探究,后面有机会再专门研究一下。

下期预告

构造一个相对完善的,带有小保底、大保底的抽奖模型。

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