算法之集邮问题

2026/07/17 algorithm

最近做了一个集卡的活动,基本就是支付宝五福卡的翻版,只是在计算期望的时候,发现不好算,问了一下AI,说这个是”集邮问题“。

什么是集邮问题

每次随机获得一张邮票,可能重复;目标是收集齐全部 8 种邮票,研究平均需要抽多少次,以及抽若干次后集齐的概率。

如何计算

如果概率一样,有现成的公式可以计算。

def coupon_collector_math(n):
    # 利用公式直接计算期望,O(n)
    harmonic_sum = sum(1/i for i in range(1, n + 1))
    return n * harmonic_sum

如果概率不一样,就需要用动态规划去计算。

def expected_draws(probabilities: list[float]) -> float:
    """
    计算集齐所有邮票需要的期望抽取次数。

    probabilities:
        每一种邮票被抽中的概率,所有概率之和必须为 1。
        例如 [0.25, 0.20, ..., 0.04]
    """
    n = len(probabilities)
    full_state = (1 << n) - 1

    if abs(sum(probabilities) - 1.0) > 1e-9:
        raise ValueError("所有邮票概率之和必须等于 1")

    if any(p <= 0 for p in probabilities):
        raise ValueError("每种邮票的概率都必须大于 0,否则永远无法集齐")

    @lru_cache(maxsize=None)
    def dp(state: int) -> float:
        if state == full_state:
            return 0.0

        duplicate_probability = 0.0
        new_stamp_cost = 0.0

        for i, probability in enumerate(probabilities):
            if state & (1 << i):
                # 已经拥有这张邮票,抽中后状态不变
                duplicate_probability += probability
            else:
                # 抽中新邮票,进入下一个状态
                next_state = state | (1 << i)
                new_stamp_cost += probability * dp(next_state)

        # E = 1 + 重复概率 * E + 新状态的期望
        # 移项后:
        # E = (1 + 新状态的期望) / (1 - 重复概率)
        return (1.0 + new_stamp_cost) / (1.0 - duplicate_probability)

    return dp(0)

测试结果

如果是8张邮票,概率一样,都是0.125,则期望是 21.74 次。

probabilities = [
    0.125,
    0.125,
    0.125,
    0.125,
    0.125,
    0.125,
    0.125,
    0.125
]

如果概率不一样,像下面这种,则期望是 1302.51 次。

期望并不等于一定

期望只是说拉平下来,需要多少次能够集齐。但如果你是非酋,那可能需要很多次。

collection_problem_1

如果是8张邮票,概率一样,则如下。

collection_problem_2