在问 · 算法题解

528. 按权重随机选择

中等二分查找前缀和 + 二分查找力扣原题 ↗

题目描述

设计类 Solution,按权重随机抽取下标。

Solution(w):用正整数数组 w 初始化,w[i] 是下标 i 的权重,下标从 0 开始。

pickIndex():随机返回 [0, w.length - 1] 中的一个下标,抽到下标 i 的概率必须等于 w[i] / sum(w)。例如 w = [1,3] 时,返回 0 的概率是 1/4,返回 1 的概率是 3/4。

由于结果随机,只要概率分布正确,任何一组输出都可以被接受。

示例 1
输入:操作 = ["Solution","pickIndex"], 参数 = [[[1]],[]]
输出:[null, 0]

解释:只有一个下标,每次都只能返回 0。

示例 2
输入:操作 = ["Solution","pickIndex","pickIndex","pickIndex","pickIndex","pickIndex"]
参数 = [[[1,3]],[],[],[],[],[]]
输出:[null, 0, 1, 1, 1, 1]

解释:下标 1 的概率为 3/4,下标 0 为 1/4;这只是可能的输出之一。

数据范围

  • 1 <= w.length <= 10^4
  • 1 <= w[i] <= 10^5
  • pickIndex 最多调用 10^4 次。

解题思路:前缀和 + 二分查找

把权重拼成一条长度为 sum(w) 的线段:下标 i 占 (pre[i-1], pre[i]] 这 w[i] 个整数点,pre 是前缀和。均匀随机取 x ∈ [1, sum],落在哪一段就返回哪个下标。pre 严格递增,“第一个 pre[i] >= x 的 i”就是 bisect_left(pre, x)。

为什么正确

每个下标 i 恰好对应 w[i] 个 x 值,所以被选中概率是 w[i] / sum。

复杂度分析

构造 O(n),每次 pickIndex O(log n),空间 O(n)。

易错点

随机数范围和查找方式要配套:x 取 [1, sum] 配 bisect_left;若取 [0, sum) 就要配 bisect_right。配错会让某个下标多一个或少一个点,甚至越界。

参考代码

Python、Java、C++、Go、JavaScript、TypeScript 六种语言,逻辑一致。

class Solution:
    def __init__(self, w: List[int] = ()):
        self.pre = []
        s = 0
        for x in w:
            s += x
            self.pre.append(s)

    def pickIndex(self) -> int:
        x = random.randint(1, self.pre[-1])
        return bisect_left(self.pre, x)

载入编辑器,自己跑一遍 →

其他解法

前缀和加二分把权重铺在数轴上,每次采样做一次二分;别名法预处理成 n 个“最多两种结果”的桶,把单次采样降到 O(1)。

解法 2:别名法(Vose Alias Method) 时间 初始化 O(n),每次选取 O(1),空间 O(n)

把每个下标的概率乘以 n,平均值变为 1。准备 n 个容量为 1 的桶:每次取一个不足 1 的下标 s 放进自己的桶,用一个不少于 1 的下标 l 把桶补满,记录 prob[s] 和别名 alias[s] = l,l 的剩余量相应减少,再按是否小于 1 放回对应列表。选取时先等概率选一个桶 i,再以 prob[i] 的概率返回 i,否则返回 alias[i]。

每次选取只需两次随机数,时间 O(1),适合大量采样。构建过程较难理解,并涉及浮点数,存在微小的精度误差。

class Solution:
    def __init__(self, w: List[int] = ()):
        # Vose 别名法:把每个下标的概率放大 n 倍后,切成 n 个容量为 1 的桶,
        # 每个桶最多装两个下标:自己(概率 prob[i])和别名 alias[i]
        n = len(w)
        total = sum(w)
        self.n = n
        self.prob = [0.0] * n
        self.alias = list(range(n))
        scaled = [x * n / total for x in w]
        small = [i for i, p in enumerate(scaled) if p < 1]
        large = [i for i, p in enumerate(scaled) if p >= 1]
        while small and large:
            s, l = small.pop(), large.pop()
            self.prob[s] = scaled[s]
            self.alias[s] = l          # 桶 s 的剩余部分由 l 填满
            scaled[l] -= 1 - scaled[s]
            (small if scaled[l] < 1 else large).append(l)
        for i in small + large:      # 剩下的桶恰好装满(浮点误差处理)
            self.prob[i] = 1.0

    def pickIndex(self) -> int:
        # 等概率选一个桶,再以 prob 决定取桶主人还是别名
        i = random.randrange(self.n)
        return i if random.random() < self.prob[i] else self.alias[i]

二分查找 专题的其他题目

查看 二分查找 全部题目 →