当我们发或抢微信的拼手气红包时,每个人能能抢到的金额是随机的,并且在 1 分钱到 200 元之间。接下来我们实现一个随机切分红包的程序。
ximport random
import time
import typing
# 切分后的单个最大金额是 200 元,MAX_AMOUNT 的单位是分
MAX_AMOUNT: int = 200 * 100
# 切分后的单个最小金额是 1 分,MIN_AMOUNT 的单位是分
MIN_AMOUNT: int = 1
def _split(total: int, num: int) -> list[int]:
"""
随机切分红包。
总金额以及切分后的金额的单位是分,这样做的原因是:
1. float 只提供近似存储,而非精确存储,有些小数没法用 float 表示,容易失真;
2. 转换成最小单位(分)后,所有运算都可以转换成整型运算,比如通过 random 模块从区间内随机选择一个数字
:param total: 总金额,单位是分
:param num: 红包数量
:return: 切分后的金额列表
"""
assert num > 0, "至少需要有一个人抢红包"
if total / num > MAX_AMOUNT:
raise RuntimeError("人均金额超过上限")
if total < MIN_AMOUNT * num:
raise RuntimeError("人均金额低于下限")
# 递归出口:如果只有一个人抢,那么直接返回
if num == 1:
return [total]
# 剩下的 num - 1 个人至少要 (num - 1) * MIN_AMOUNT 分;至多要 (num - 1) * MAX_AMOUNT 分
remaining_min: int = (num - 1) * MIN_AMOUNT
remaining_max: int = (num - 1) * MAX_AMOUNT
# 当前人抢的金额必须在 [total-remaining_min, total-remaining_max] 之间,否则无法满足后面的人的要求,
# 并且当前人抢的金额也必须在 [MIN_AMOUNT, MAX_AMOUNT] 之间,
# 所以取两个区间的交集
left: int = max(total - remaining_max, MIN_AMOUNT)
right: int = min(total - remaining_min, MAX_AMOUNT)
# 如果直接从 [left, right] 之间选择,那么先选的人获取大包的概率非常大
for _ in range(5):
# X% 的概率从区间后半段选(1 <= X < 100)
x: float = 20
if random.random() > (1 - x / 100):
left = (left + right) // 2
else:
right = (left + right) // 2
# 从处理后的区间内,随机选择一个数字
amount: int = random.randint(left, right)
# 递归处理
return [amount] + _split(total - amount, num - 1)
def split(total: float, num: int) -> list[float]:
"""
随机切分红包
:param total: 总金额,单位是元
:param num: 数量
:return: 切分后的金额列表,每个金额的单位是元
"""
ret: typing.Optional[list[float]] = None
# 循环若干次,从中选出一个相对均匀的,这里的实现是选择最大值和最小值之差最小的
diff: typing.Optional[float] = None
for _ in range(200):
tmp: list[float] = [amount / 100 for amount in _split(int(total * 100), num)]
current_diff: float = abs(max(tmp) - min(tmp))
if diff is None or current_diff < diff:
ret = tmp
diff = current_diff
if ret is None:
raise RuntimeError("unreachable")
return ret
if __name__ == "__main__":
total_amount: float = 300
total_num: int = 12
for _ in range(20):
start: float = time.perf_counter()
result: list[float] = split(total_amount, total_num)
print(
f"切分结果: {result},"
f"耗时:{time.perf_counter() - start:.3f} 秒,"
f"浮点计算误差:{sum(result) - total_amount}"
)
运行结果:
xxxxxxxxxx
切分结果: [4.39, 1.2, 4.71, 2.5, 29.19, 52.69, 52.23, 9.27, 12.71, 39.09, 47.19, 44.83],耗时:0.006 秒,浮点计算误差:0.0
切分结果: [3.56, 3.44, 50.2, 50.49, 50.22, 52.46, 0.27, 34.87, 14.85, 6.15, 10.83, 22.66],耗时:0.006 秒,浮点计算误差:0.0
切分结果: [15.15, 4.1, 1.72, 30.5, 50.82, 3.83, 49.27, 3.15, 37.51, 0.14, 53.72, 50.09],耗时:0.006 秒,浮点计算误差:0.0
切分结果: [53.2, 0.56, 28.07, 54.02, 45.29, 9.36, 2.9, 0.82, 27.83, 27.03, 2.68, 48.24],耗时:0.006 秒,浮点计算误差:0.0
切分结果: [3.56, 5.26, 55.13, 25.27, 2.7, 54.07, 22.24, 33.41, 14.22, 0.69, 22.06, 61.39],耗时:0.006 秒,浮点计算误差:0.0
切分结果: [16.27, 29.58, 50.46, 53.01, 42.2, 3.17, 8.45, 48.86, 1.07, 1.44, 23.26, 22.23],耗时:0.006 秒,浮点计算误差:-5.684341886080802e-14
切分结果: [12.69, 4.36, 26.91, 37.15, 40.92, 23.57, 49.98, 28.4, 0.56, 22.65, 15.06, 37.75],耗时:0.006 秒,浮点计算误差:0.0
切分结果: [46.8, 56.0, 5.56, 14.63, 3.28, 57.42, 38.27, 61.22, 0.48, 0.44, 0.56, 15.34],耗时:0.007 秒,浮点计算误差:0.0
切分结果: [36.02, 52.15, 1.91, 43.68, 43.8, 10.19, 2.77, 27.6, 7.03, 25.47, 12.37, 37.01],耗时:0.006 秒,浮点计算误差:0.0
切分结果: [55.2, 52.62, 49.51, 12.2, 19.77, 44.36, 1.34, 8.32, 3.82, 16.47, 4.66, 31.73],耗时:0.006 秒,浮点计算误差:0.0
切分结果: [7.87, 40.99, 26.23, 51.24, 1.54, 7.35, 43.96, 6.65, 15.54, 0.58, 49.45, 48.6],耗时:0.006 秒,浮点计算误差:5.684341886080802e-14
切分结果: [4.92, 7.48, 1.42, 55.03, 3.42, 1.07, 52.37, 24.97, 46.15, 27.87, 44.84, 30.46],耗时:0.006 秒,浮点计算误差:-5.684341886080802e-14
切分结果: [42.13, 37.85, 3.55, 12.17, 54.33, 1.54, 5.89, 6.81, 2.07, 33.77, 50.68, 49.21],耗时:0.006 秒,浮点计算误差:0.0
切分结果: [52.25, 56.0, 37.51, 46.85, 4.48, 28.11, 11.16, 1.12, 2.81, 9.83, 31.31, 18.57],耗时:0.006 秒,浮点计算误差:0.0
切分结果: [9.63, 27.47, 60.18, 3.06, 51.62, 39.13, 55.53, 31.99, 5.46, 2.4, 8.59, 4.94],耗时:0.006 秒,浮点计算误差:-5.684341886080802e-14
切分结果: [47.29, 50.15, 3.21, 41.25, 4.59, 23.97, 19.49, 4.7, 6.27, 21.17, 41.5, 36.41],耗时:0.006 秒,浮点计算误差:0.0
切分结果: [27.81, 51.2, 51.55, 4.53, 46.08, 16.47, 54.12, 7.01, 10.72, 6.42, 0.1, 23.99],耗时:0.006 秒,浮点计算误差:1.1368683772161603e-13
切分结果: [59.74, 55.82, 47.01, 20.15, 1.38, 49.99, 18.63, 2.53, 23.37, 3.18, 7.25, 10.95],耗时:0.006 秒,浮点计算误差:0.0
切分结果: [4.89, 22.52, 56.65, 28.9, 3.48, 0.92, 3.68, 46.93, 50.76, 42.06, 1.35, 37.86],耗时:0.006 秒,浮点计算误差:5.684341886080802e-14
切分结果: [43.42, 29.4, 51.11, 23.35, 7.49, 6.0, 21.04, 39.87, 21.49, 15.12, 0.52, 41.19],耗时:0.007 秒,浮点计算误差:0.0