一个关于权重随机数的简单研究——抽奖示例(二)
在上一节中,简单说了一下权重随机数的产生,且验证了其合理性。这一节,我们简单探讨一下权重随机数的几个简单应用。
权重置零的玩法
权重随机数中有一个特殊情况,如果我们把权重数组中的某一项权重设置为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% |
通过表格中的计算,可以很容易看出,保底机制添加之后,出货概率提高了两倍,当然,实际提升的概率与保底次数具备相关性,这里我们不做探究,后面有机会再专门研究一下。
下期预告
构造一个相对完善的,带有小保底、大保底的抽奖模型。