公司需要做一个抽奖的页面,奖项的概率要能控制。发现一个经典算法,记录一下。
var p_arr = [10, 20, 30, 40];
function getResult(arr) {
var pSum = eval(arr.join("+")); // 获取总概率区间
for (var i = 0; i < arr.length; i++) {
var random = parseInt(Math.random() * pSum); // 获取 0-总概率区间的一个随随机整数
if (random < arr[i]) {
return i; //如果在当前的概率范围内,得到的就是当前概率
} else {
pSum -= arr[i]; //否则减去当前的概率范围,进入下一轮循环
}
}
}
getResult(p_arr);
首先定义一个数组 [10,20,30,40]
对应各个奖项的概率, 该数组各元素的总和代表总概率区间为 100,第一个元素的值为 10,就正好代表抽中第一个的概率为 10%,其他元素的概率依此类推。
- 第一轮循环开始是从 0-100 这个概率范围内筛选第一个数看她是否在他的出现概率范围之内,如果出现,这直接返回该结果,退出循环。
- 如果不在,则将概率空间,也就是数组总和(pSum)减去刚刚的那个数字的概率空间,然后进行第二轮循环
- 在本例当中就是减去 10,也就是说第二个数是在 0-90 这个范围内筛选的。
- 这样筛选到最终,总会有一个数满足要求。
- 就相当于去一个箱子里摸东西,第一个不是,第二个不是,第三个还不是,那最后一个一定是。
理解
-
假设为 a,b,c,d,这四个概率数,那么他们的概率空间(总和)就是 a+b+c+d
-
从 0-(a+b+c+d) 中取一个随机数,把这个随机数跟 a 做对比,情况有两种:
比 a 小,概率为 a/(a+b+c+d)
比 a 大,概率为(b+c+d)/(a+b+c+d)
- 如果取到的值比 a 小,那么说明已经抽中 a 了,直接返回。抽奖结束;如果取到的值比 a 大,说明没有抽到 a,则进行下一轮
- 在第二轮中,需要在之前的概率空间(总和)中去掉 a 的空间,所以第二轮的概率空间为 b+c+d,然后从新的概率空间 0-(b+c+d)中取一个随机数跟 b 做对比,同样情况有两种:
比 b 小,那么它的概率就是 b/(b+c+d). 注意,这个概率出现的前提是前一次的结果要 > a ,所以总概率就是两次乘积: (b+c+d)/(a+b+c+d) * b/(b+c+d) = b/(a+b+c+d)
比 b 大,那么它的概率就是 (c+d)/(b+c+d). 同样,和 >a 的结果同时出现,这种情况的总概率应该是: (b+c+d)/(a+b+c+d) * (c+d)/(b+c+d) = (c+d)/(a+b+c+d).
- 如果取到的值比 b 小,那么说明已经抽中 b 了,直接返回。抽奖结束;如果取到的值比 b 大,说明没有抽到 b,然后进入第三次循环,然后第三次循环时,我们再把 b 的概率空间减去,以此类推……
应用
prize_arr 代表奖品选项,V 就代表每个奖品所对应的概率
var prize_arr = [
{ id: 1, v: 5, prize: "一等奖-80%OFF-平板电脑", coup: "AM-80-OFF-CO" },
{ id: 2, v: 10, prize: "二等奖-60%OFF-数码相机", coup: "AM-60-OFF-CO" },
{ id: 3, v: 15, prize: "三登奖-40%OFF-音箱设备", coup: "AM-40-OFF-CO" },
{ id: 4, v: 20, prize: "四等奖-30%OFF-4G优盘", coup: "AM-30-OFF-CO" },
{ id: 5, v: 50, prize: "五等奖-20%OFF-谢谢参与", coup: "AM-20-OFF-CO" },
];
var gArr = [];
for (var i = 0; i < prize_arr.length; i++) {
gArr.push(prize_arr[i]["v"]);
}
console.log(prize_arr[getResult(gArr)]["prize"]);
问题
结合我公司抽奖实际,发现奖项信息全部在 js 里面写死,这就存在一隐患,只需要稍微懂一点前端知识的人,一看到代码就能获取到所有奖品里面的信息,而恰好我们的奖品都是电子折扣码,就是上面奖品列表中的 coup 字段,那么这个抽奖就变得毫无意义。
【解决办法】
方案一:可以将奖品数据字段进行简单的加密,然后再解密,这样处理使得奖品信息不是太过于直白而已,治标不治本。
方案二:将抽奖代码放在后台,通过前端请求的方式完成,每次请求都只是返回对应抽到的奖品信息,完美解决。 附上 php 实现的后端代码
<?php
/*
* 经典的概率算法,
* $proArr是一个预先设置的数组,
*/
function get_rand($proArr) {
$result = '';
//概率数组的总概率精度
$proSum = array_sum($proArr);
//概率数组循环
foreach ($proArr as $key => $proCur) {
$randNum = mt_rand(1, $proSum);
if ($randNum <= $proCur) {
$result = $key;
break;
} else {
$proSum -= $proCur;
}
}
unset ($proArr);
return $result;
}
/*
* 奖项数组
* 是一个二维数组,记录了所有本次抽奖的奖项信息,
* 其中id表示中奖等级,prize表示奖品,v表示中奖概率。
* 注意其中的v必须为整数,你可以将对应的 奖项的v设置成0,即意味着该奖项抽中的几率是0,
* 数组中v的总和(基数),基数越大越能体现概率的准确性。
* 本例中v的总和为100,那么平板电脑对应的 中奖概率就是1%,
*/
$prize_arr = array(
'0' => array('id'=>1,'prize'=>'一等奖-80%OFF-平板电脑','v'=>1,'coup'=>'AM-80-OFF-CO'),
'1' => array('id'=>2,'prize'=>'二等奖-60%OFF-数码相机','v'=>5,'coup'=>'AM-60-OFF-CO'),
'2' => array('id'=>3,'prize'=>'三登奖-40%OFF-音箱设备','v'=>10,'coup'=>'AM-40-OFF-CO'),
'3' => array('id'=>4,'prize'=>'四等奖-30%OFF-4G优盘','v'=>12,'coup'=>'AM-30-OFF-CO'),
'4' => array('id'=>5,'prize'=>'五等奖-20%OFF-10Q币','v'=>22,'coup'=>'AM-20-OFF-CO'),
'5' => array('id'=>6,'prize'=>'六等奖-0%OFF-下次没准就能中哦','v'=>50,'coup'=>'AM-00-OFF-CO'),
);
/*
* 将中奖奖品保存在数组$res['yes']中,
* 而剩下的未中奖的信息保存在$res['no']中,
* 最后输出json数据给前端页面。
*/
foreach ($prize_arr as $key => $val) {
$arr[$val['id']] = $val['v'];
}
$rid = get_rand($arr); //根据概率获取奖项id
$res['yes']['prize']= $prize_arr[$rid-1]['prize'];
$res['yes']['coup']= $prize_arr[$rid-1]['coup'];
$res['yes']['prize']= $prize_arr[$rid-1]['prize'];
$res['yes']['id']= $prize_arr[$rid-1]['id'];
// unset($prize_arr[$rid-1]); //将中奖项从数组中剔除,剩下未中奖项
shuffle($prize_arr); //打乱数组顺序
for($i=0;$i<count($prize_arr);$i++){
$pr[] = $prize_arr[$i]['prize'];
}
$res['no'] = $pr;
$res = json_encode( $res );
print_r($res) ;
?>
抽奖页面实现
抽奖采用转盘的设计,由于考虑到兼容性问题,故采用现成的轮子 jquery.rotate.js 插件开发,在前端请求到对应数据后,轮盘转到指定的位置。
<script src="assets/js/jquery.rotate.js"></script>;
....
$(function () {
var rotateFunc = function (awards, angle, text) {
isture = true;
$btn.stopRotate();
$btn.rotate({
angle: 0, //旋转的角度数
duration: 4000, //旋转时间
animateTo: angle + 1440, //给定的角度,让它根据得出来的结果加上1440度旋转
callback: function () {
isture = false; // 标志为 执行完毕
alert(text);
},
});
};
var clickfunc = function () {
$.get('chou.php', function (data, status) {
if (status == 'success') {
var prize = JSON.parse(data).yes
// var data = [1, 2, 3, 4, 5, 6];//抽奖
//data为随机出来的结果,根据概率后的结果
// dataId = data[Math.floor(Math.random() * data.length)];//1~6的随机数
rotateFunc(prize.id, (prize.id - 1) * -60, prize.prize);
} else {
console.log("请求失败,请刷新");
}
})
};
var $btn = $(".g-lottery-img"); // 旋转的div
var playnum = 5; //初始次数,由后台传入
$(".playnum").html(playnum); //显示还剩下多少次抽奖机会
var isture = 0; //是否正在抽奖
$(".playbtn").click(function () {
if (isture) return; // 如果在执行就退出
isture = true; // 标志为 在执行
if (playnum <= 0) {
//当抽奖次数为0的时候执行
alert("没有次数了");
$(".playnum").html(0); //次数显示为0
isture = false;
} else {
//还有次数就执行
playnum = playnum - 1; //执行转盘了则次数减1
if (playnum <= 0) {
playnum = 0;
}
$(".playnum").html(playnum);
clickfunc();
}
});
});
网友评论