🧧问题描述
例如一个人在群里发了10块钱的红包,群里有5个人一起来抢红包,每人抢到的金额随机分配。
红包功能需要满足哪些具体规则呢?
- 所有人抢到的金额之和要等于红包金额,不能多也不能少
- 每个人至少抢到1分钱
- 要保证红包拆分的金额尽可能分布均衡,不要出现两极分化太严重的情况
【金额处理】
- 先将输入的
totalMoney
(元)扩大100倍,换做分 - 在输出时用
BigDecimal
做转换
// 换做分输出
for (int i = 0; i < amount; i++) {
System.out.println("抢到金额" + new BigDecimal(redPackage[i]).divide(new BigDecimal(100)));
}
1. 基础实现-非公平
【思路】
- 每次抢红包的时候直接随机就好啦,随机的上限是剩余的红包金额
- 每次抢到的金额 = 随机区间 ( 0, 剩余金额 )
public class _01_RedPackage {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int total = in.nextInt() * 100;
in.nextLine();
int amount = in.nextInt();
int[] redPackage = new int[amount];
getRandomMoney(total, amount, redPackage);
// 换做分输出
for (int i = 0; i < amount; i++) {
System.out.println("抢到金额" + new BigDecimal(redPackage[i]).divide(new BigDecimal(100)));
}
}
public static void getRandomMoney(int total, int amount, int[] redPackage) {
final int MIN_RED = 1;
Arrays.fill(redPackage, MIN_RED);
int last = total - amount;
Random random = new Random();
for (int i = 0; i < amount - 1; i++) {
//rand.nextInt(MAX - MIN + 1) + MIN
int randRedValue = random.nextInt(last - 1);
redPackage[i] += randRedValue;
last -= randRedValue;
}
redPackage[amount - 1] += last;
}
}
【存在的问题】
如果以这种方式随机,先抢的人会有很大优势,越往后的人随机到的平均金额越小。
假设有10个人,红包总额100元。
- 第一个人的随机范围是(0,100元),平均可以抢到50元。假设第一个人随机到50元,那么剩余金额是100-50 = 50 元。
- 第二个人的随机范围是 (0, 50元),平均可以抢到25元。假设第二个人随机到25元,那么剩余金额是50-25 = 25 元。
- 第三个人的随机范围是 (0, 25元),平均可以抢到12.5元。
以此类推,每一次随机范围越来越小。
2. 二倍均值法
剩余红包金额M,剩余人数N,那么:
每次抢到金额 = 随机区间(0, M/N*2)
保证了每次随机金额的平均值是公平的
假设10人,红包金额100元
- 第一人:100/10*2=20,随机范围(0,20),平均可以抢到10元
- 第二人:90/9*2=20,随机范围(0,20),平均可以抢到10元
- 第三人:80/8*2=20,随机范围(0,20),平均可以抢到10元
以此类推,每次随机范围的均值是相等的
缺点:除了最后一次,任何一次抢到的金额都不会超过人均金额的两倍,并不是任意的随机
public class _02_RedPackage {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
// 输入金额,分为单位, 扩大100倍
int total = in.nextInt() * 100;
in.nextLine();
int amount = in.nextInt();
int[] redPackage = new int[amount];
getRandomMoney(total, amount, redPackage);
// 换做分输出
for (int i = 0; i < amount; i++) {
System.out.println("抢到金额" + new BigDecimal(redPackage[i]).divide(new BigDecimal(100)));
}
}
public static void getRandomMoney(int total, int amount, int[] redPackage) {
int last_money = total;
int last_people = amount;
Random random = new Random();
for (int i = 0; i < amount - 1; i++) {
//随机范围:[1,剩余人均金额的2倍 - 1] 分
int randRedValue = random.nextInt(last_money / last_people * 2 - 1) + 1;
redPackage[i] = randRedValue;
last_money -= randRedValue;
last_people--;
}
//最后一人分剩余金额, 此处为非公平
redPackage[amount - 1] = last_money;
}
}
3. 线段切割法
前两种方法,并不能达到真正意义上的公平。而线段切割法,可以达到要求。
我们可以把红包总金额想象成一条很长的线段,而每个人抢到的金额,则是这条主线段所拆分出的若干子线段。
如何确定每一条子线段的长度呢?由“切割点”来决定。当N个人一起抢红包的时候,就需要确定N-1个切割点。
因此,当N个人一起抢总金额为M的红包时,我们需要做N-1次随机运算,以此确定N-1个切割点。随机的范围区间是(1, M)
当所有切割点确定以后,子线段的长度也随之确定。这样每个人来抢红包的时候,只需要顺次领取与子线段长度等价的红包金额即可。
这就是线段切割法的思路。在这里需要注意以下两点:
- 当随机切割点出现重复,如何处理
- 如何尽可能降低时间复杂度和空间复杂度
public class _03_RedPackage {
//线段分割法
private static List<Integer> getRandomMoney(int totalMoney, int amount) {
//验证参数合理校验
//为了使用random.nextInt(Integer)方法, 不得不先把红包金额放大100倍,最后在main函数里面再除以100
//这样就可以保证每个人抢到的金额都可以精确到小数点后两位
int redMoney = (int) (totalMoney * 100);
if (redMoney < amount || redMoney < 1) {
System.out.println("红包个数必须大于0,并且最小红包不少于1分");
}
List<Integer> boards = new ArrayList<>();
boards.add(0);
boards.add(redMoney);
//红包个数和线段个数的关系
while (boards.size() <= amount) {
// 随机生成切割点
int index = new Random().nextInt(redMoney - 1) + 1;
if (boards.contains(index)) {
//保证线段的位置不相同
continue;
}
boards.add(index);
}
//计算每个红包的金额,将两个板子之间的钱加起来
Collections.sort(boards);
List<Integer> list = new ArrayList<>();
for (int i = 0; i < boards.size() - 1; i++) {
Integer e = boards.get(i + 1) - boards.get(i);
list.add(e);
}
return list;
}
public static void main(String[] args) {
List<Integer> accountList = getRandomMoney(10, 5);
BigDecimal count = new BigDecimal(0);
for (Integer amount : accountList) {
//将抢到的金额再除以100进行还原
BigDecimal tmpcount = new BigDecimal(amount).divide(new BigDecimal(100));
count = count.add(tmpcount);
System.out.println("抢到金额:" + tmpcount);
}
System.out.println("total = " + count);
}
}
【文章参考】