赞
踩
游戏服务器开发还真会常遇到,策划需求根据权重作概率发奖励,比如奖励和权重分别是:A10、B20、C70,这时候出现A的概率就要是10%,B就是20%,C是70%,就是出现的概率是当前权重 / 总权重。该怎么设计算法呢?抽多个奖励ID并且每个奖励ID只能出现一次时候改怎样实现呢?
通过总权重随机值,再线性扫描,通过权重随机值去查找所在的权重区间。
时间复杂度:O(N)
过程:
1、先计算出所有道具的权重总和S
2、然后调用随机函数得到一个区间在[1, S]的随机值N
3、扫描列表,如果N小于当前的权重,则返回当前道具
4、若N大于当前权重,则把N减去当前权重
- # 测试数据
- # [(奖励ID: 权重)]
- pool_data = [
- (1, 50),
- (2, 20),
- (3, 40),
- (4, 10),
- ]
-
- def linear_random(pool_data):
- sum_weight = sum(a[1] for a in pool_data)
- n = random.randint(1, sum_weight)
- for key, weight in pool_data:
- if n <= weight:
- return key
- else:
- n -= weight

在方法一的基础上做优化,即权重大的(概率大的)排序在前面,即线性查找时先从概率大的开始查找,命中率会更高。
时间复杂度:O(N)
过程:
1、将奖池根据权重从大到小排序
2、方法同上
- # 测试数据
- # [(奖励ID: 权重)]
- pool_data = [
- (1, 50),
- (2, 20),
- (3, 40),
- (4, 10),
- ]
-
- def sort_linear_random(pool_data):
- pool = sorted(pool_data, key=lambda a: a[1], reverse=True)
- sum_weight = sum(a[1] for a in pool)
- n = random.randint(1, sum_weight)
- for key, weight in pool:
- if n <= weight:
- return key
- else:
- n -= weight

基于方法一线性扫描的优化,线性扫描既然每次只移动一个位置,既然权重表是从大到小排序,后面的权重值就一定小于等于当前值,就可以一次移动多个位置。随机权重值 / 当前权重值 = 3,下次可以直接移动3个位置。
时间复杂度:O(N)
过程:
1、先计算出所有道具的权重总和S
2、然后调用随机函数得到一个区间在[1, S]的随机值N
3、重建权重表,列表中每个权重是前面所有权重值的总和,得到有序权重表
3、通过n / 当前权重,得到的值J大于0,则是J是要跳跃得阶级数(因为有序,后面的权重肯定比当前小)
- # 测试数据
- # [(奖励ID: 权重)]
- pool_data = [
- (1, 50),
- (2, 20),
- (3, 40),
- (4, 10),
- ]
-
- def jump_random(pool_data):
- sort_pool = sorted(pool_data, key=lambda a: a[1], reverse=True)
-
- add = 0
- pool = []
- for key, weight in sort_pool:
- pool.append((key, weight + add))
- add += weight
- sum_weight = add
-
- pool_length = len(pool)
- n = random.randint(1, sum_weight)
-
- i = 0
- while i < pool_length - 1:
- key, weight = pool[i]
- if weight > n:
- return key
- else:
- multiple = n // weight
- i += multiple
- return pool[i][0]

区别于前面的方法,将权重去累加得到一个递增权重表,再随机出一个目标权重值。因为递增,那就可以使用二分查找
时间复杂度:O(LogN)
过程:
1、先计算出所有道具的权重总和S,
2、然后调用随机函数得到一个区间在[1, S]的随机值N
3、重建权重表,列表中每个权重是前面所有权重值的总和,得到有序权重表
3、根据N在有序权重表二分查找
- # 测试数据
- # [(奖励ID: 权重)]
- pool_data = [
- (1, 50),
- (2, 20),
- (3, 40),
- (4, 10),
- ]
-
- def binary_random(pool_data):
- add = 0
- pool = []
- for k, w in pool_data:
- pool.append((k, w + add))
- add += w
- """
- pool= [
- (1, 50),
- (2, 20 + 50),
- (3, 20 + 50 + 40),
- (4, 20 + 50 + 40 + 10),
- ]
- """
- sum_weight = add
- n = random.randint(1, sum_weight)
- mid, left, right, = 0, 0, len(pool) - 1
- while left < right: # 二分查找
- mid = (right + left) // 2
- key, mid_num = pool[mid]
- if mid_num < n:
- left = mid + 1
- elif mid_num > n:
- right = mid
- else:
- return key
- return pool[mid][0]

时间复杂度O(1)
待写。。。
从一个奖池多次抽取奖励,并且个奖励ID只能出现一次。
很简单,每次抽完就把总权重减去当前抽中ID的权重,再在新的总权重中去随机生成权重值。
可以用字典来实现删减操作。
- # {奖励ID: 权重}
- pool_dict = {
- 1: 10,
- 2: 20,
- 3: 30,
- 4: 40,
- }
- def times_linear(pool_dict, cnt):
- pool = pool_dict.copy()
- sum_weight = sum(pool.values())
- rst = []
- for _ in range(cnt):
- n = random.randint(1, sum_weight)
- for key, weight in pool.items():
- if n <= weight:
- rst.append(key)
- sum_weight -= weight
- del pool[key]
- break
- else:
- n -= weight
- return rst

总结:抽取量大、权重较多的时候做优化还是非常必要的。
参考文章:
【数学】时间复杂度O(1)的离散采样算法—— Alias method/别名采样方法_haolexiao的专栏-CSDN博客_alias算法
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。