要求:
- 每人获得金额之和为总金额
- 每人至少0.01元
- 每人获得金额概率上是公平的
- 保留两位小数
逐条分析:
对于1,最后一个人获得的金额就是红包总金额-已抢金额
对于2,我们可以先给每人分配0.01元,即红包金额变为红包总金额-人数*0.01
对于3,是本题核心,我们可以采用二倍均值法 或 线段切割法
对于4,用toFixed(2)
同时本题还涉及到浮点数运算不精确有误差的问题
二倍均值法
假设有10个人,红包总额100元。
100/10X2 = 20, 所以第一个人的随机范围是(0,20 ),平均可以抢到10元。
假设第一个人随机到10元,那么剩余金额是100-10 = 90 元。
90/9X2 = 20, 所以第二个人的随机范围同样是(0,20 ),平均可以抢到10元。
假设第二个人随机到10元,那么剩余金额是90-10 = 80 元。
80/8X2 = 20, 所以第三个人的随机范围同样是(0,20 ),平均可以抢到10元。
以此类推,每一次随机范围的均值是相等的。
但这个解法的问题是,任意一次抢到的金额都低于人均的二倍,不是任意的随机
还是上面的例子,而线段切割法可以理解为:在一条 首值为0 ,尾值为100-10*0.01 的线段上随机(概率分布是平均的,所以公平)选择 10-1 个切割点 ,将这条线段分成了10段,前九个人依次拿走前九段的值,由于我们要保证每人获得金额之和为100,所以最后一个人不能取最后一段(第10段)的值而是另外处理(100-已抢金额)
最终的线段切割法方案如下:
// 线段切割法
function getRandomMoney(total, num) {
if (num == 1) return total;
//先给每人分配0.01元
let max = 100 * total - num * 1; //去除“保底”后的金额
let randomArr = [0];
let accum = 0; //已抢金额
while (randomArr.length < num + 1) {
let random = Math.round(Math.random() * max);
//不能有重复的“切割点”
if (randomArr.indexOf(random) == -1) {
randomArr.push(random);
}
}
randomArr.sort((a, b) => a - b);
let result = [];
for (let j = 1; j < num; j++) {
let money = randomArr[j] - randomArr[j - 1];
accum += money;
let final_money = ((money + 1) * 0.01).toFixed(2);
result.push(Number(final_money));
}
//最后一个人另外处理
result.push(Number(((max - accum + 1) * 0.01).toFixed(2)));
return result;
}
let total = 100;
let num = 10;
let result = getRandomMoney(total, num);
console.log(result);
这里有的坑:
- toFixed返回字符串,要用Number转化一下
- 要把小数运算化成整数运算避免浮点数运算精度问题,这个是这题最易错的点
这里可以参考下另一种写法,这种红包剩余金额*random()的写法的问题就是越后面的人抢到的金额均值越小:
function getRandomMoney_NotFair(total, n) {
let leftMoney = total * 100;
let leftNum = n;
let tmpMoney = 0;
let res = [];
while (leftNum-- > 1) {
tmpMoney = Math.round(
Math.random() * (leftMoney - leftNum * 100 - 100) + 100
);
leftMoney -= tmpMoney;
res.push(Number((tmpMoney * 0.01).toFixed(2)));
}
res.push(Number((leftMoney * 0.01).toFixed(2)));
return res;
}