题目描述

✅ LCR 071. 按权重随机选择

image-20260929005629123

image-20260929005629124

image-20260929005629125

题意分析

给定正整数权重数组 w,每次调用 pickIndex() 都随机返回一个下标,使下标 i 被选中的概率为 $w[i]/\sum w$。多次调用使用同一份权重,不会因为某个下标被选过就减少它的权重。

可以把每份权重理解为一个可被抽中的整数位置。下标 i 占据 w[i] 个位置,从全部位置中均匀选一个,就能按区间长度得到所需概率。无需真的创建这些位置,只需保存每段的边界。

解法:前缀权重区间与二分采样

核心思路

[!blue]

设权重个数为 $m$,用 presum[i] 表示前 i 项权重之和,初值 presum[0] = 0,总权重为 T = presum[m]。下标 i 对应整数区间 $(presum[i],presum[i+1]]$,其中恰好有 w[i] 个整数。

这些区间互不重叠,并完整覆盖 1..T。从中随机选一个落点 x 后,只需找到第一个满足 presum[i+1] >= x 的下标 i;前一段的右端尚未到达落点,当前段的右端已经到达,落点恰好属于当前段。

权重都为正,前缀和严格递增,可以对段号二分。维护包含答案的闭区间,若第 mid 段的右端已经不小于 x,答案在它或其左侧,令 right = mid;否则该段及左侧都无法覆盖落点,令 left = mid+1。最后一段的右端是总权重,必定覆盖某个合法落点,因此答案始终存在。

概率由分段长度保证:总共有 T 个可抽位置,恰有 w[i] 个位置会被定位到下标 i,因此抽中概率与权重成正比。这个结论描述每次抽样的分布,不要求有限次调用的结果严格按权重比例交替出现。

前缀和只在构造时计算,后续查询只负责抽点和定位。题目中长度最多 $10^4$、单项权重最多 $10^5$,总和最多 $10^9$,当前整数类型足够保存。

解题步骤

  1. 创建长度为 w.length+1 的前缀和数组,将权重逐项累加。
  2. 每次查询生成 1..T 内的随机整数。Java 将 [0,1) 的随机数缩放取整后加一;Go 用 rand.Intn(T)+1。
  3. 在原下标范围 [0,w.length-1] 内查找首个右端点不小于落点的段,返回其段号。

查询代码里的 n 是前缀和数组长度,所以总权重位于 presum[n-1],合法段号的右端是 n-2。只有一个权重时,二分直接返回下标零。落点等于某段右端点时仍属于该段,比较条件必须含等号。

代码实现

class Solution {
    private int[] presum;

    public Solution(int[] w) {
        int n = w.length;

        presum = new int[n + 1];

        for (int i = 0; i < n; ++i) {
            presum[i + 1] = presum[i] + w[i];
        }
    }

    public int pickIndex() {
        int n = presum.length;
        int x = (int) (Math.random() * presum[n - 1]) + 1;
        int left = 0;
        int right = n - 2;

        while (left < right) {
            int mid = (left + right) >> 1;

            if (presum[mid + 1] >= x) {
                right = mid;
            } else {
                left = mid + 1;
            }
        }

        return left;
    }
}
import (
    "math/rand"
)

type Solution struct {
    presum []int
}

func Constructor(w []int) Solution {
    n := len(w)
    pre := make([]int, n+1)
    for i := 0; i < n; i++ {
        pre[i+1] = pre[i] + w[i]
    }
    return Solution{pre}
}

func (this *Solution) PickIndex() int {
    n := len(this.presum)
    x := rand.Intn(this.presum[n-1]) + 1
    left, right := 0, n-2
    for left < right {
        mid := (left + right) >> 1
        if this.presum[mid+1] >= x {
            right = mid
        } else {
            left = mid + 1
        }
    }
    return left
}

复杂度分析

  • 时间复杂度:构造为 $O(m)$,每次查询为 $O(\log(m+1))$,由一次随机取点和一次二分定位组成。
  • 空间复杂度:$O(m)$,只保存前缀和,不按总权重展开下标。

关键点总结

[!green]

  • 权重决定区间中的整数数量,均匀抽点后按区间定位即可实现按权重抽样。
  • 本实现采用左开右闭区间,随机落点必须与之配套,取 [1,T]。
  • 前缀和位置比原下标多一位,第 i 段的右端是 presum[i+1],返回值仍是 i。

易错点总结

[!yellow]

  • 抽到零或超过总权重,会让落点落在全部合法区间之外。
  • 把二分条件写成严格大于,会把恰好位于右端点的落点错误分给下一段。
  • 每次查询重新建立前缀和,会重复执行可以一次完成的预处理。
  • 每次抽样后扣减权重,会改变后续调用应使用的概率分布。

相似题目

题目 难度 关联与区别
497. 非重叠矩形中的随机点 中等 该题以矩形格点数作为权重,复用前缀和与二分抽样,再把区间内的偏移量转换为格点坐标。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/28319794
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!