简单的活动抽奖算法&方案
2017-11-02 19:36
369 查看
前言
原理
算法
库存操作
php实现
随机区间法
自增匹配法
想想,如果是线下举办抽奖,一般会有哪些方案?
可预估奖品数
主动式,抽奖券500份,其中有奖品的只有10份,然后给用户抽,抽中就是你的。例如:买汽水的瓶盖抽奖,刮刮乐。
被动式,带ID的抽奖券500份,给用户抽,然后系统随机抽取10个ID发放奖品。例如:发布会入场券抽奖
不可预估奖品数
用户自己填信息的抽奖券,到时候由系统随机生成一个数,比对一致的就即为中奖者。例如:彩票。
其实线上的抽奖算法,基本上也是基于模拟线下场景方案来模拟的。但绝大多数场景都是黑盒操作,执行抽奖,中间的过程用户是无法获知的。
这个方法随机度高,根据概率论来计算,每个用户的单次中奖概率为
如图所示,上面是一个奖品的分配区间,例如预计抽奖100W人,1等级1个,2等奖3个,3等奖5个,4等奖10个,其余999981都是谢谢惠顾。用户抽奖的时候,获得一个随机数,判断是否在中奖区间即可。发放奖品,则区间内的奖品剩余数-1;回收奖品,则区间内的奖品+1。
当一个奖品被抽完之后,从奖品区间移除(谢谢惠顾一般不算奖品),其余继续抽奖,例如上图的10个4等奖被抽光了。而当所有奖品都抽光了,就会只剩下一个谢谢惠顾的区间,这样用户不论怎么抽,都只会是谢谢惠顾,直到活动日结束。如果需要限制总抽奖次数,则将谢谢惠顾的部分也纳入库存,最终库存全部消耗完,随即提示用户抽奖结束即可。
需要注意的是,评估预计抽奖的人数比较重要(影响到随即数生成区间),我们可以根据历史数据评估,如果不是很清楚,建议评估人数大一些,这样奖品不至于很快被抽完。
2、自增匹配法
此方法简单至极,先设一个全局自增数,然后每个奖品我们设一个数字,有几个奖品设几个数,每次用户抽奖,自增数加一返回,如果自增数此时与奖品的数字一致,则中奖。
好处是,不用记录奖品的剩余数,只用记录自增数。
缺点是,由于不用记录奖品的剩余数,是因为提前进行了分布,所以奖品多的情况不适用。
得益于redis的原子性操作和极高的性能,在高并发情况下也能很快的处理相应的库存增减操作(redis同样适用于秒杀场景)。
概率测试
其中,模拟100W次抽奖仅仅是计算了100W次随机数的分布,多次运行,可以看到概率基本上是完全符合预期的。
以上,简单的营销场景完全够用。
如有更好的方法,欢迎讨论。
原理
算法
库存操作
php实现
随机区间法
自增匹配法
前言
只要是有营销的场景,抽奖可以说几乎是必不可少的功能,如何基于一个简单的抽奖逻辑去支撑种类繁多的抽奖方案,结合之前的经验,总结如下。原理
其实不论上层的抽奖方案是什么(例如,大转盘,刮刮乐,扎气球、砸金蛋等),都只是展示层的提现形式不一样,底层都可以使用同一个抽奖算法。想想,如果是线下举办抽奖,一般会有哪些方案?
可预估奖品数
主动式,抽奖券500份,其中有奖品的只有10份,然后给用户抽,抽中就是你的。例如:买汽水的瓶盖抽奖,刮刮乐。
被动式,带ID的抽奖券500份,给用户抽,然后系统随机抽取10个ID发放奖品。例如:发布会入场券抽奖
不可预估奖品数
用户自己填信息的抽奖券,到时候由系统随机生成一个数,比对一致的就即为中奖者。例如:彩票。
其实线上的抽奖算法,基本上也是基于模拟线下场景方案来模拟的。但绝大多数场景都是黑盒操作,执行抽奖,中间的过程用户是无法获知的。
算法
1、随机区间法这个方法随机度高,根据概率论来计算,每个用户的单次中奖概率为
中奖概率=奖品数/预估抽奖用户人数
如图所示,上面是一个奖品的分配区间,例如预计抽奖100W人,1等级1个,2等奖3个,3等奖5个,4等奖10个,其余999981都是谢谢惠顾。用户抽奖的时候,获得一个随机数,判断是否在中奖区间即可。发放奖品,则区间内的奖品剩余数-1;回收奖品,则区间内的奖品+1。
当一个奖品被抽完之后,从奖品区间移除(谢谢惠顾一般不算奖品),其余继续抽奖,例如上图的10个4等奖被抽光了。而当所有奖品都抽光了,就会只剩下一个谢谢惠顾的区间,这样用户不论怎么抽,都只会是谢谢惠顾,直到活动日结束。如果需要限制总抽奖次数,则将谢谢惠顾的部分也纳入库存,最终库存全部消耗完,随即提示用户抽奖结束即可。
需要注意的是,评估预计抽奖的人数比较重要(影响到随即数生成区间),我们可以根据历史数据评估,如果不是很清楚,建议评估人数大一些,这样奖品不至于很快被抽完。
2、自增匹配法
此方法简单至极,先设一个全局自增数,然后每个奖品我们设一个数字,有几个奖品设几个数,每次用户抽奖,自增数加一返回,如果自增数此时与奖品的数字一致,则中奖。
好处是,不用记录奖品的剩余数,只用记录自增数。
缺点是,由于不用记录奖品的剩余数,是因为提前进行了分布,所以奖品多的情况不适用。
库存操作
曾经使用mysql的时候,需要使用事务、消息队列,来保证并发导致的数据一致性问题。直到后来改为使用redis。得益于redis的原子性操作和极高的性能,在高并发情况下也能很快的处理相应的库存增减操作(redis同样适用于秒杀场景)。
php实现
随机区间法
<?php namespace Home\Controller; use Think\Controller; class IndexController extends Controller { public function index() { $people = 1000000; $prizes = [ ['id' => 0, 'name' => '遗憾,您未抽中任何奖品'],//没抽中 ['id' => 1, 'name' => '一等奖,iPhone X', 'num' => 1], ['id' => 2, 'name' => '二等奖,华为Mate10', 'num' => 3], ['id' => 3, 'name' => '三等奖,三星note8', 'num' => 5], ['id' => 4, 'name' => '四等奖,一加5', 'num' => 10], ]; $this->prize_draw($people, $prizes); } /** * 抽奖 * @param $people 预估的抽奖人数 * @param $prizes 读取奖项设置,可以从数据库读,记得先在redis中初始化库存 */ private function prize_draw($people, $prizes) { $redis = get_redis(); foreach ($prizes as $key => $value) { if ($prizes[$key]['id'] != 0) { $count = $redis->get('prize:count:' . $value['id']); if ($count !== false && $count <= 0) { //检查是否有剩余,没有则减去这部分区间 $people = $people - $prizes[$key]['num']; unset($prizes[$key]); } } } //重新下标数组 $prizes = array_values($prizes); dump($prizes); $rate = []; //计算区间 foreach ($prizes as $key => $item) { if ($key == 0) $rate[$key] = [0, $item['num']]; else if ($key == count($prizes) - 1) $rate[$key] = [$rate[$key - 1][1], $people]; else $rate[$key] = [$rate[$key - 1][1], $rate[$key - 1][1] + $item['num']]; } dump($rate); //抽奖 $rd = mt_rand(0, $people); dump($rd); foreach ($rate as $key => $item) { if ($item[0] <= $rd && $rd < $item[1]) { if ($prizes[$key]['id'] != 0) { $newcount = $redis->incrBy('prize:count:' . $prizes[$key]['id'], -1); if ($newcount !== false && $newcount >= 0) { return $prizes[$key]; } } return $prizes[0]; } } } }
概率测试
//模拟100W次抽奖随机数分布 for ($i = 0; $i < 1000000; $i++) { $rd = mt_rand(0, $people); foreach ($rate as $key => $item) { if ($item[0] <= $rd && $rd < $item[1]) { if ($prizes[$key]['count']) { $prizes[$key]['count'] += 1; } else { $prizes[$key]['count'] = 1; } } } } dump($prizes);
//模拟100W次抽奖随机数分布结果 array(5) { [0] => array(4) { ["id"] => int(1) ["name"] => string(20) "一等奖,iPhone X" ["num"] => int(1) ["count"] => int(1) } [1] => array(4) { ["id"] => int(2) ["name"] => string(24) "二等奖,华为Mate10" ["num"] => int(3) ["count"] => int(2) } [2] => array(4) { ["id"] => int(3) ["name"] => string(23) "三等奖,三星note8" ["num"] => int(5) ["count"] => int(6) } [3] => array(4) { ["id"] => int(4) ["name"] => string(19) "四等奖,一加5" ["num"] => int(10) ["count"] => int(8) } [4] => array(3) { ["id"] => int(0) ["name"] => string(33) "遗憾,您未抽中任何奖品" ["count"] => int(999982) } }
其中,模拟100W次抽奖仅仅是计算了100W次随机数的分布,多次运行,可以看到概率基本上是完全符合预期的。
自增匹配法
<?php namespace Home\Controller; use Think\Controller; class IndexController extends Controller { public function index() { $prizes = [ ['id' => 0, 'name' => '遗憾,您未抽中任何奖品'],//没抽中 ['id' => 1, 'name' => '一等奖,iPhone X', 'num' => [54]], ['id' => 2, 'name' => '二等奖,华为Mate10', 'num' => [100, 386, 999]], ['id' => 3, 'name' => '三等奖,三星note8', 'num' => [798, 6333, 48795]], ['id' => 4, 'name' => '四等奖,一加5', 'num' => [159, 357, 8432, 789456, 123147, 256528, 764565, 999663, 744121, 546478]], ]; dump($this->prize_draw($prizes)); } /* * 抽奖 * @param $prizes */ private function prize_draw($prizes) { $nothing = $prizes[0]; unset($prizes[0]); $redis = get_redis(); $count = $redis->incr('prize:count'); foreach ($prizes as $prize) { foreach ($prize['num'] as $num) { if ($num == $count) { return $prize; } } } return $nothing; } }
以上,简单的营销场景完全够用。
如有更好的方法,欢迎讨论。
相关文章推荐
- 一个简单抽奖算法的实现以及如何预防超中
- 简单随机抽奖小活动
- 随机获取礼物活动总结(抽奖算法)
- 一个简单的抽奖算法
- 关于网站抽奖活动算法的尝试
- 最简单的问题与算法(借书方案知多少)
- 在营销活动中的抽奖算法放送
- 在营销活动中的抽奖算法放送
- 简单抽奖活动js代码
- 图像处理之图像分割(一)之活动轮廓模型:Snake算法简单梳理
- 抽奖简单算法
- 关于网站抽奖活动算法的尝试
- JAVA 两个简单的抽奖算法
- 游戏抽奖活动中下限保底的简单实现
- 抽奖活动(一)-Alias算法
- 简单抽奖算法
- 【水晶报表之图片篇-c】 CR 11版本动态加载的另一种简单方案
- 简单排序算法之快速排序
- [置顶] Android上最简单的IPC方案——Messenger
- 算法笔记_069:Floyd算法简单介绍(Java)