游戏服务器算法-关于权重的随机抽取算法,抽一个次或抽多次的实现

    游戏服务器开发还真会常遇到,策划需求根据权重作概率发奖励,比如奖励和权重分别是: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算法

基于权重 的随机选择算法 - 知乎

  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值