资讯动态

赠券收集问题:从数学原理到动态规划实现

发布时间:2026/10/29 22:13:58 来源:尧图企业网站定制
1. 从奶茶杯套到算法实战赠券收集问题的生活化解读上周在奶茶店排队时我发现一个有趣的现象店家推出集齐5款不同杯套可兑换限定礼品的活动。观察半小时后我意识到这本质上就是概率论中经典的赠券收集问题(Coupon Collectors Problem)。作为算法工程师我决定用这个生活案例带大家彻底搞懂该问题的数学本质与动态规划实现。赠券收集问题描述的是假设有n种不同的赠券奶茶杯套每次收集时随机获得其中一种问需要多少次尝试才能集齐所有类型这个问题在推荐系统如电商凑单、测试用例生成覆盖所有边界条件等领域都有重要应用。下面我将用Python3.8实现两种解法概率公式推导与动态规划并对比它们的适用场景。关键认知动态规划不是银弹。对于赠券收集问题当n较小时数学公式更高效当n较大或需要中间过程数据时DP才显现优势。2. 数学建模与概率解法2.1 问题形式化定义设n为不同杯套种类数如5种T为集齐所有种类所需的总次数t_i为收集到第i种新杯套所需的尝试次数显然有T t₁ t₂ ... tₙ每次收集时第1个新杯套获取概率n/n 1第2个新杯套获取概率(n-1)/n...第n个新杯套获取概率1/n2.2 期望值计算由于t_i服从几何分布其期望E(t_i) 1/p_iE(T) E(t₁) ... E(tₙ) n/n n/(n-1) ... n/1 n * H_n其中H_n是第n个调和数约等于ln(n) γ 1/(2n)γ≈0.5772欧拉常数Python实现import math def coupon_collector_math(n): return sum(n / (n - k) for k in range(n)) # 测试奶茶杯套问题(n5) print(coupon_collector_math(5)) # 输出约11.42这意味着要集齐5种杯套平均需要购买11-12杯奶茶。这个结果可能解释了为什么商家敢做这种促销——实际兑换率比消费者直觉预估的要低得多。2.3 概率分布可视化我们通过蒙特卡洛模拟验证理论值import numpy as np import matplotlib.pyplot as plt def simulate(n, trials10000): results [] for _ in range(trials): collected set() count 0 while len(collected) n: collected.add(np.random.randint(n)) count 1 results.append(count) return results n 5 sim_data simulate(n) print(f模拟均值: {np.mean(sim_data):.2f}) plt.hist(sim_data, binsrange(max(sim_data)1), densityTrue) plt.axvline(coupon_collector_math(n), colorr, linestyle--) plt.title(fn{n}时的收集次数分布) plt.show()运行结果会显示红色虚线理论期望与模拟分布的均值基本重合验证了我们的数学推导。3. 动态规划解法3.1 DP状态定义虽然数学解法优雅但动态规划能提供更多信息。定义dp[i]为已收集i种杯套时集齐全部所需期望次数。边界条件dp[n] 0 已集齐dp[0] E(T) 最终求解目标状态转移 当已有i种杯套时获得新杯套概率(n-i)/n → 需要1 dp[i1]次获得旧杯套概率i/n → 需要1 dp[i]次因此dp[i] (n-i)/n * (1 dp[i1]) i/n * (1 dp[i])3.2 方程化简与实现化简状态转移方程dp[i] 1 (n-i)/n * dp[i1] i/n * dp[i] dp[i] * (1 - i/n) 1 (n-i)/n * dp[i1] dp[i] n/(n-i) dp[i1]Python实现def coupon_collector_dp(n): dp [0] * (n 1) for i in range(n-1, -1, -1): dp[i] dp[i1] n/(n-i) return dp[0] # 验证 print(coupon_collector_dp(5)) # 与数学解法结果一致3.3 DP与数学解法的对比维度数学公式法动态规划法时间复杂度O(n)O(n)空间复杂度O(1)O(n)优势计算快适合单次查询可查中间状态劣势无法获取中间结果需要额外存储空间实际应用建议如果只需要最终期望值优先使用数学公式如果需要知道已收集k种时还需多少次等中间结果则选择DP。4. 算法扩展与变种4.1 非均匀概率分布现实中不同杯套出现概率可能不均等。设第i种杯套概率为p_i则期望公式变为E(T) ∫₀^∞ [1 - ∏(1 - exp(-p_i t))] dtPython实现需要使用数值积分from scipy import integrate def non_uniform_coupon(p): def integrand(t): product 1.0 for pi in p: product * (1 - math.exp(-pi * t)) return 1 - product return integrate.quad(integrand, 0, math.inf)[0] # 示例5种杯套概率分别为[0.4, 0.3, 0.15, 0.1, 0.05] print(non_uniform_coupon([0.4, 0.3, 0.15, 0.1, 0.05])) # 约8.334.2 部分收集场景有时不需要集齐全部只需m种即可。此时期望公式为E(T) n * (1/(n) 1/(n-1) ... 1/(n-m1))DP解法只需修改终止条件def partial_collect(n, m): dp [0] * (n 1) for i in range(n-1, n-m-1, -1): dp[i] dp[i1] n/(n-i) return dp[n-m] print(partial_collect(5, 3)) # 集齐任意3种的期望次数5. 工程实践中的注意事项浮点数精度问题当n很大时调和数计算可能丢失精度。可使用对数近似def harmonic_number(n): return math.log(n) 0.5772156649015328 1/(2*n)内存优化DP解法实际只需保存前一个状态def dp_optimized(n): dp_next 0 for i in range(n-1, -1, -1): dp_current n/(n-i) dp_next dp_next dp_current return dp_next概率验证技巧对于任何随机算法都建议像我们之前那样用蒙特卡洛模拟验证理论值特别是在非均匀分布情况下。6. 实际应用案例6.1 测试用例生成在单元测试中假设有n个代码分支需要覆盖随机测试时预计需要E(T)次测试才能覆盖所有分支帮助评估测试充分性与所需资源6.2 推荐系统设计电商平台推荐商品时用户需要看到多少商品才能覆盖所有类别兴趣可据此优化推荐策略平衡探索与利用6.3 生物学研究在DNA测序中需要多少随机读段才能覆盖整个基因组类似问题也出现在环境微生物采样中7. 常见问题排查为什么DP结果比模拟值略小检查模拟次数是否足够至少1万次确认随机数生成是否均匀用np.random.choice替代randintn很大时程序卡死怎么办对于数学解法使用对数近似对于DP解法改用内存优化版本如何计算收集k套的期望时间修改DP终止条件为dp[k] 0数学公式截断前k项求和这个奶茶杯套问题给了我三点重要启示第一算法源于生活保持观察能发现很多学习素材第二同一个问题往往有多种解法要了解各自适用场景第三概率结果常反直觉数学推导比盲目猜测可靠得多。下次看到类似促销活动你会不会先算一算期望值呢

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑