游戏服务器开发还真会常遇到,策划需求根据权重作概率发奖励,比如奖励和权重分别是: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]
方式四、Alias method/别名采样方法
时间复杂度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算法