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

背景:

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

假设一个类似抽盲盒的场景,配置了三个物品,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:"-"`          // 奖品id
	RewardName 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},
	}

	// 初始化奖品属性map
	rewardMap := 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, 10
	for {
		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) {
	// 先找出所有奖品数要乘的最大公共倍数 maxMultiple
	var maxMultiple int = 1

	for i := 0; i < len(infos); i++ {
		strWeight := fmt.Sprintf("%v", infos[i].Weight)
		// maxMultiple 只关注小数类型权重值,整数类型权重值直接跳过。例如"0.25",我们通过其中的"25"来得到 maxMultiple=100
		if i := strings.Index(strWeight, "."); i == -1 {
			continue
		}

		var tmpMultiple int = 1
		for 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倍率后就为1w
	rewardList := 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,就抽中了概率最低的 幻神 − C F S 2022 \color{red}{幻神-CFS2022} 幻神CFS2022,运气中规中矩。
在这里插入图片描述
抽奖结果单独记录到文件:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值