背景:

在之前有遇到一个面试题,让实现一个带有权重值的抽奖系统,具体如下:

假设一个类似抽盲盒的场景,配置了三个物品,1,2,3,分别的权重是20,30,50,表明物品1中奖概率20%,物品2中奖概率30%,物品3中奖概率50%,用户买了这个盲盒,实现根据中奖概率返回哪件物品?

infos := []RewardInfo{{RewardId: 1, RewardName: "物品A", Weight: 20},// 20%{RewardId: 2, RewardName: "物品B", Weight: 30},// 30%{RewardId: 3, RewardName: "物品C", Weight: 50},// 50%}

大家可以先想一下具体要怎么实现?

以穿越火线官方活动为例,活动链接:cf道聚城活动:

思路:

前提:以下代码示例都是参考上图的权重值。

  • 首先,求出能使得所有奖品权重值都变为正整数的最大倍数 maxMultiple

    • 找出所有奖品对应的权重值中,小数点后 [数字位数最多的权重值],并根据此权重值小数点后的位数,求出使其权重值变为正整数的最大倍数 maxMultiple。遍历过程中,我们只关注小数类型的权重值,整数类型的权重值由于没有小数点,可以直接跳过。
    • 例如:“奖品A权重值 0.25,奖品B权重值 0.75, 奖品C权重值 1,奖品D权重值 98,加起来一共是 100 ”。我们可以找出 小数点后 [数字位数最多的权重值],即 奖品A的 0.25(或者 奖品B的权重值 0.75)。并且其小数点之后的位数有2位,它需要乘以100(10*10)才能变为正整数 25,因此得到 maxMultiple = 100
  • 然后,根据 [不同奖品的权重值] 乘以 [之前求得的最大倍数 maxMultiple],分配对应个数的奖品到奖池中:
  • 最后,根据 rand 随机种子函数,从构建好的奖池中抽取奖品即可。
    如果需要的话,可以将每次抽取的奖品记录单独输出到一个文件中。

注意:

  • rewardList := make([]int32, 0, maxMultiple*100)
    为了避免奖池切片反复扩容影响性能,最好提前预估cap容量:倍率*100
    例如奖品A的权重为 0.25,乘以100倍率后为 25;奖品D的权重为 98,乘以100倍率后为 9800;那么对于总权重为100,乘以100倍率后就为1w。因此可以预估切片容量为1w。
  • time.Sleep(time.Nanosecond)
    每次抽奖都有一定时间间隔,避免程序运行过快导致一瞬间的随机数种子都一致,我这里每次休眠时长为100纳秒。

更多优化点:

为了避免切片占据过大的内存空间,可以考虑使用 BitMap位图 来代替常规切片数组,来减少内存开销,大家可以自己尝试下如何实现。

代码:

// RewardInfo 奖励信息
type RewardInfo struct {RewardId   int32   `json:"-"`          // 奖品idRewardName string  `json:"RewardName"` // 奖品名称Weight     float32 `json:"Weight"`     // 抽中的权重(省略末尾百分号的形式:例如某奖品权重为 0.1%,那么这里值也为 0.1)GetCnt     int32   `json:"GetCnt"`     // 中奖数
}// 抽奖模拟器
// 入参:chouJiangCnt-用户的抽取次数
func LotterySimulator(chouJiangCnt int) {// 以下权重都是百分制(省略了末尾百分号%),例如 0.1 → 0.1%,79 → 79%,一共是100%infos := []RewardInfo{{RewardId: 1, RewardName: "幻神-CFS2022(皮肤,不可交易)", Weight: 0.1},{RewardId: 2, RewardName: "火麒麟-巅峰荣耀(皮肤,不可交易)", Weight: 0.15},{RewardId: 3, RewardName: "黑骑士-CFS2021(皮肤,不可交易)", Weight: 0.25},{RewardId: 4, RewardName: "毁灭-CFS2020(皮肤,不可交易)", Weight: 0.25},{RewardId: 5, RewardName: "M4A1-雷神-CFS2019(皮肤,不可交易)", Weight: 0.3},{RewardId: 6, RewardName: "毁灭-CFS2018(皮肤,不可交易)", Weight: 0.3},{RewardId: 7, RewardName: "火麒麟-CFS2018(皮肤,不可交易)", Weight: 0.3},{RewardId: 8, RewardName: "屠龙-荣耀之锋(皮肤,不可交易)", Weight: 0.4},{RewardId: 9, RewardName: "修罗-CFS2018(皮肤,不可交易)", Weight: 0.85},{RewardId: 10, RewardName: "王者之石x1", Weight: 7},{RewardId: 11, RewardName: "积分x100", Weight: 0.1},{RewardId: 12, RewardName: "积分x10", Weight: 1},{RewardId: 13, RewardName: "积分x5", Weight: 10},{RewardId: 14, RewardName: "积分x1", Weight: 79},}// 初始化奖品属性maprewardMap := make(map[int32]RewardInfo)for _, rewardInfo := range infos {rewardMap[rewardInfo.RewardId] = RewardInfo{RewardId:   rewardInfo.RewardId,RewardName: rewardInfo.RewardName,Weight:     rewardInfo.Weight,GetCnt:     0,}}// 此处按需添加:将抽奖结果记录文件于当前路径下// 注意:我下面是判断抽到幻神才输出结果记录到文件testJsonFile, _ := os.OpenFile("./cf_reward.json", os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0777)defer testJsonFile.Close()// 当前抽奖已花费总金额,初始为0;单次抽奖花费金额10元costMoneyOfTotal, costMoneyOfEveryTime := 0, 10for {time.Sleep(time.Second) // 每秒抽一次,控制下速率,方便控制台打印观察fmt.Printf("抽奖中...,当前已消费%v元\n", costMoneyOfTotal)for i := 0; i < chouJiangCnt; i++ {rewardId := SelectByWeight(infos)// 组装抽取奖品的数据rewardMap[rewardId] = RewardInfo{RewardId:   rewardMap[rewardId].RewardId,RewardName: rewardMap[rewardId].RewardName,Weight:     rewardMap[rewardId].Weight,GetCnt:     rewardMap[rewardId].GetCnt + 1,}costMoneyOfTotal += costMoneyOfEveryTime// 每次抽奖都有一定间隔,避免运行过快导致一瞬间的随机数种子都一致time.Sleep(time.Nanosecond)}// 以下逻辑按需添加,只是输出指定抽奖结果到文件// 如果 幻神数 > 0,则把抽奖结果记录到文件中,并停止抽奖if rewardMap[1].GetCnt > 0 {jsonRewardMap, _ := json.Marshal(rewardMap)// fmt.Printf("rewardMap: \n%v\n", string(jsonRewardMap))writer := bufio.NewWriter(testJsonFile)writer.WriteString(string(jsonRewardMap) + "\n" + fmt.Sprintf("一共花费RMB:%v", costMoneyOfTotal))writer.Flush()break}}return
}// SelectByWeight 根据不同奖品权重随机抽取奖品并返回
func SelectByWeight(infos []RewardInfo) (RewardId int32) {// 先找出所有奖品数要乘的最大公共倍数 maxMultiplevar maxMultiple int = 1for i := 0; i < len(infos); i++ {strWeight := fmt.Sprintf("%v", infos[i].Weight)// maxMultiple 只关注小数类型权重值,整数类型权重值直接跳过。例如"0.25",我们通过其中的"25"来得到 maxMultiple=100if i := strings.Index(strWeight, "."); i == -1 {continue}var tmpMultiple int = 1for i := 0; i < len(strWeight[i+1:]); i++ {tmpMultiple *= 10}if maxMultiple < tmpMultiple {maxMultiple = tmpMultiple}}// fmt.Println("最大倍数 maxMultiple: ", maxMultiple)// 避免反复扩容,提前预估cap容量:倍率*100// 例如权重为0.85,乘以100倍率后为85;那么对于总权重为100,乘以100倍率后就为1wrewardList := make([]int32, 0, maxMultiple*100)// 根据不同奖品的权重值,分配对应个数的奖品到奖池中for _, info := range infos {for i := 0; i < int(info.Weight*float32(maxMultiple)); i++ { // 填充每个奖品的整数样本个数到容量为1w的奖池中rewardList = append(rewardList, info.RewardId)}}// fmt.Printf("奖池奖品总数 = %v, 奖池奖品明细 = %v \n", len(rewardList), rewardList)// // os.Exit(0)rand.Seed(time.Now().UnixNano())              // UnixNano将t表示为Unix时间,即从时间点January 1, 1970 UTC到时间点t所经过的时间(单位纳秒)return rewardList[rand.Intn(len(rewardList))] // rand.Intn(100)类似于rand.Int()%100
}func main() {// 模拟cf抽奖LotterySimulator(10)
}

运行结果

比如我这次试水了1800,就抽中了概率最低的 幻神−CFS2022\color{red}{幻神-CFS2022}幻神−CFS2022,运气中规中矩。

抽奖结果单独记录到文件:

面试场景题:如何实现带权重值的抽奖程序相关推荐

  1. POJ 2912 Rochambeau(难,好题,枚举+带权并查集)

    下面的是从该网站上copy过来的,稍微改了一点,给出链接:http://hi.baidu.com/nondes/item/26dd0f1a02b1e0ef5f53b1c7 题意:有N个人玩剪刀石头布, ...

  2. offer收割攻略,Web前端面试真题JavaScript系列(带详解)

    临近秋招,不少小伙伴开始了求职之路,面试是十分重要的考验,其次也离不开面试题.网上的面试题零零散散,十分混乱,正好我抽时间帮助大家总结一下.难易程度肯定是从简到难,今天是第一篇的续集--基础篇中的Ja ...

  3. 2017 西安网络赛A Tree(树上静态查询,带权并查集,矩阵乘法压位,好题)

    题目链接 题意: 给出 \(n(n \leq 3000)\) 个结点的一棵树,树上每个结点有一个 \(64 \times 64\) 的 \(0,1\)矩阵,每个结点上的矩阵是根据输入的 \(seed\ ...

  4. 学习笔记——拓展域并查集和带权并查集

    1,拓展域并查集 一般的并查集只能查找出各元素之间是否存在某一种相同的联系,如:a和b是亲戚关系,b和c是亲戚关系,这时就可以查找出a和c也存在亲戚关系.但如果存在多种相对的联系时一般的并查集就不行了 ...

  5. 视频教程-大牛带你全面剖析Python高频面试真题-Python

    大牛带你全面剖析Python高频面试真题 毕业于德国奥格斯堡大学计算机系,先从事分布式网络,搜索引擎等领域的设计开发工作,于2017底混入图灵学院,立志挑战传统培训,做中国最好的Python教育 刘英 ...

  6. poj1182 and 携程预赛2第一题 带权并查集

    题意:       动物王国中有三类动物A,B,C,这三类动物的食物链构成了有趣的环形.A吃B, B吃C,C吃A.  现有N个动物,以1-N编号.每个动物都是A,B,C中的一种,但是我们并不知道它到底 ...

  7. Valentine's Day Round hdu 5176 The Experience of Love [好题 带权并查集 unsigned long long]

    传送门 The Experience of Love Time Limit: 4000/2000 MS (Java/Others)    Memory Limit: 65536/65536 K (Ja ...

  8. Java面试之场景题汇总

    Java面试之场景题汇总 1. HashMap链表长度大于8后一定会转化为红黑树吗 2. ConcurrentMashMap分段锁之间加了什么锁,读写之间是互斥的吗? 3. 三次握手后如果一直不连接会 ...

  9. Java面试之场景题(精准到阿里看到都哇哇完)

    Java面试之场景题汇总 目录 1. HashMap链表长度大于8后一定会转化为红黑树吗 2. ConcurrentMashMap分段锁之间加了什么锁,读写之间是互斥的吗? 3. 三次握手后如果一直不 ...

最新文章

  1. linux chpasswd命令 批量修改用户密码
  2. java 模板引擎_极简 Spring Boot 整合 Thymeleaf 页面模板
  3. 元旦限时特惠,耳机、书籍等大降价
  4. 更改Web.config中对上传文件大小限制
  5. matlab7.x答案,MATLAB习题及参考答案
  6. logo下方显示技术支持信息_LOGO墙形象设计的几种方案
  7. VSCode调试Python时终端输出中文乱码解决方法1
  8. 2020王万生计算机组成原理,计算机组成原理课后习题-王万生(53页)-原创力文档...
  9. 用OpenCV检测图像中的长方形画布或纸张并提取图像内容
  10. javascript配置ckfinder的路径
  11. 遇到 java.io.EOFException 异常的解决办法
  12. Visual Studio 2013 、Matlab 2014产品密钥
  13. C语言题库 part.1
  14. 传说对决服务器无响应怎么办,传说对决一直进不去怎么办
  15. 如何成功地安装OpenEXR
  16. html框架页面不允许滚动条,关于html:使用iframe时如何摆脱双滚动条?
  17. 1.7编程基础字符串11潜伏者
  18. 博客园博客怎样让百度收录?
  19. python itchat库_用python【itchat】库获取群聊信息的正确姿势
  20. 电商数据监测:如何获取想要的电商平台数据?

热门文章

  1. 原子制造:物质科学的未来技术
  2. 【最近抖音上元宇宙虚拟项目猜成语,互动无人直播游戏挤地铁】
  3. 付了房款!兜兜憋了!
  4. setTimeout()中容易忽视的要点
  5. strlen计算字符串长度
  6. Scala 学习入门到掌握-分支和循环[5]
  7. 我用段子讲.NET之依赖注入其二
  8. 杭州1米93程序员征婚贴火了!首付能付260万,孩子以后都随妈姓
  9. 20155339 Exp4 恶意代码分析
  10. MATLAB Appdesigner实用技巧(三):在App中实现示波器效果/动态波形效果