现阶段有个需求:
现有一个水果礼包,礼包包含多品类、多规格、多数量的拼手气小红包生成。
转化为简单json 如: 礼包内有 {A={a1=3, a2=1}, B={b1=1}, C={c1=5}}
有A商品的a1规格3个,a2规格1个…
条件:有n个人按照拼手气抽取对应的水果。当然,抽取每种水果的几率是一样的,然后如果有多规格,再随机规格。
说一下我的思路吧,首先也是需要找到类似红包算法(因为是小白,算法这类的暂时是弱项了,网上大都说:截断正态分布或者叫做截尾正态分布)简单的说就是每次不能真正随机,要在平均数左右随机占大部分才是最符合红包的场景的,为此找了好久的算法,但是最后还是找到了类似的算法,这里因为计算精度的问题,又自己做了修改:
class LeftMoneyPackage {
private int remainSize;
private String remainMoney;
public int getRemainSize() {
return remainSize;
}
public void setRemainSize(int remainSize) {
this.remainSize = remainSize;
}
public String getRemainMoney() {
return remainMoney;
}
public void setRemainMoney(String remainMoney) {
this.remainMoney = remainMoney;
}
}
public static double getRandomMoney(LeftMoneyPackage _leftMoneyPackage) {
// remainSize 剩余的红包数量
// remainMoney 剩余的钱
if (_leftMoneyPackage.remainSize == 1) {
_leftMoneyPackage.remainSize--;
return Double.parseDouble(_leftMoneyPackage.remainMoney);
}
Random r = new Random();
String min = "0.01";
String max = MoneyUtil
.moneyMul(MoneyUtil.moneydiv(_leftMoneyPackage.remainMoney, (_leftMoneyPackage.remainSize) + ""), "2");
String money = MoneyUtil.moneyMul(max, r.nextDouble() + "");
money = MoneyUtil.moneyComp(min, money) ? "0.01" : money;
_leftMoneyPackage.remainSize--;
_leftMoneyPackage.remainMoney = MoneyUtil.moneySub(_leftMoneyPackage.remainMoney, money);
return Double.parseDouble(money);
}
@Test
public void testCount() {
Map<Integer, Double> map = new HashMap<>();
for (int a = 0; a < 10; a++) {//调整次数
LeftMoneyPackage lp = new LeftMoneyPackage();
lp.setRemainMoney("100");
lp.setRemainSize(10);
double totalMoney = 0;
while (lp.getRemainSize() > 0) {
double randomMoney = getRandomMoney(lp);
// System.out.println("第 " + (10 - lp.getRemainSize()) + "次" + " " + randomMoney);
totalMoney = Double.parseDouble(MoneyUtil.moneyAdd(totalMoney + "", randomMoney + ""));
int remainSize = lp.getRemainSize();
if (map.get(remainSize) == null) {
map.put(remainSize, randomMoney);
} else {
map.put(remainSize, map.get(remainSize) + randomMoney);
}
}
System.out.println("总价:" + totalMoney);
}
Iterator<Entry<Integer, Double>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
Entry<Integer, Double> next = iterator.next();
System.out.println("第 " + next.getKey() + "个数平均值" + (next.getValue() / 10));//除以总次数
}
}
原理还没弄明白,但是有一点是需要注意的,这个算法不能保证每个人拿到的就是“0.01”,所以使用的时候应该把0.01改成0,先用总金额减去每个人的0.01拿出来再随机剩下的人可以拿到的金额。(先提出每人最少金额,再随机剩下的钱得到没人可以拿到的金额)
这个算法是修改后的,使用精度计算的java.math.BigDecimal;
其中有用到的MoneyUtil工具类:(如果审核没通过或者没有c币的童鞋可以联系我,或者百度上也有挺多的)
MoneyUtil.java
=-= 找这个花了好久的时间,还有验证,这个算法在随机次数越高的情况下,越接近我的理想值,所以我认为这是符合我的预期的算法。
接下来就是构造成为水果礼包的代码:这个还修改了上面算法有可能领取为0的情况(每人先取最少的份数)
@Test
public void test() {
try {
for(int a = 0 ;a<10;a++) {
Map<String, Map<String, Integer>> fruit_specification = new HashMap<>();
Map<String, Integer> a1 = new HashMap<>();
a1.put("a1", 3);
a1.put("a2", 1);
fruit_specification.put("A", a1);
Map<String, Integer> b1 = new HashMap<>();
b1.put("b1", 1);
fruit_specification.put("B", b1);
Map<String, Integer> c1 = new HashMap<>();
c1.put("c1", 5);
fruit_specification.put("C", c1);
List<Map<String, Map<String, Integer>>> randomGoods = getRandomGoods(fruit_specification, 3, 10);
System.out.println(JSON.toJSON(randomGoods));
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public List<Map<String, Map<String, Integer>>> getRandomGoods(Map<String, Map<String, Integer>> fruit_specification, Integer personCount,
Integer goodsNum) throws Exception {
List<Map<String, Map<String, Integer>>> userGetList = new ArrayList<>();//随机出来的水果列表
// 得出当次应该随机几次 可随机分配的份数 = goodsNum -总人数(每人至少得到一件商品)
goodsNum = goodsNum-personCount;
for (; personCount > 0; personCount--) {
Map<String, Map<String, Integer>> fruitMap = new HashMap<>();
if(personCount == 1) {//如果剩下最后一个人,剩下的水果都是这个人的
userGetList.add(fruit_specification);
continue;
}
int getCount = getRandomCount(personCount, goodsNum);
goodsNum = goodsNum - getCount;//减少随机的数量
getCount= getCount+1;//每个人参与的人至少得到一件商品
System.out.println("第"+personCount+"个人--数量:"+getCount );
for (int j = 0; j < getCount; j++) {// 一份中需要取出getCount份水果
// 随机选择水果
List<String> fruitIds = new ArrayList<>(fruit_specification.keySet());// 水果列表,用于随机水果
int size = fruitIds.size();
if (size == 0) {
throw new Exception("礼包水果异常");
}
Random rd = new Random();
int index = rd.nextInt(size);// 从水果列表随机选出一种水果
Map<String, Integer> map = fruit_specification.get(fruitIds.get(index));// 规格列表
List<String> specifications = new ArrayList<>(map.keySet());
if (specifications.size() == 1) {
// 如果只有单个规格,则直接选取该规格一份
Integer specNum = map.get(specifications.get(0));
if(specNum == 1) {//剩下最后一份了,拿完要删除,避免后面的人也拿到
fruit_specification.remove(fruitIds.get(index));
//放入规格
putRandomGoodsList(fruitMap, fruitIds.get(index), specifications.get(0));
}else {
map.put(specifications.get(0), specNum-1);
//放入规格
putRandomGoodsList(fruitMap, fruitIds.get(index), specifications.get(0));
}
} else {
//否则是多个规格
int size2 = specifications.size();
int index2 = rd.nextInt(size2);// 随机选取规格
Integer specNum = map.get(specifications.get(index2));//规格剩余数量
if(specNum == 1) {
map.remove(specifications.get(index2));
//放入规格
putRandomGoodsList(fruitMap, fruitIds.get(index), specifications.get(index2));
}else {
map.put(specifications.get(index2), specNum-1);
//放入规格
putRandomGoodsList(fruitMap, fruitIds.get(index), specifications.get(index2));
}
}
}
//加入每个人的份数
userGetList.add(fruitMap);
}
return userGetList;
}
/***
* 放入一次水果随机记录
* @param fruitMap
* @param fruitId
* @param specId
*/
private void putRandomGoodsList(Map<String, Map<String, Integer>> fruitMap,String fruitId,String specId) {
if(fruitMap.get(fruitId) == null) {
Map<String, Integer> specMap = new HashMap<>();
specMap.put(specId, 1);
fruitMap.put(fruitId, specMap);
}else {
Map<String, Integer> specMap = fruitMap.get(fruitId);
if(specMap.get(specId) == null) {//规格为空
specMap.put(specId, 1);
fruitMap.put(fruitId, specMap);
}else {//两次抢到同一个商品
specMap.put(specId, specMap.get(specId)+1);
}
}
}
/**
* 根据抢礼包的人数和商品份数得到此次随机抽取次数
*
* @param personCount
* @param goodsNum
* @return
*/
public Integer getRandomCount(Integer personCount, Integer goodsNum) {
if (personCount == 1) {
return goodsNum;
}
Integer getNum = 0;
Random r = new Random();
if(goodsNum < personCount) {//既然份数都这么少,我就给他们随机了 =-=
getNum = r.nextInt(goodsNum+1);
}else {
Integer min = 0;
Integer max = getRoundNum(goodsNum.doubleValue() / personCount.doubleValue()) * 2;
getNum = getRoundNum(r.nextDouble() * max);
getNum = getNum < min ? min : getNum;
getNum = getNum > goodsNum ? goodsNum : getNum;
}
return getNum;
}
/**
* 四舍五入取整
*
* @param num
* @return
*/
public int getRoundNum(double num) {
double n = (double) ((int) num) + 0.5f;
if (num < n) {
return (int) (n - 0.5);
} else {
return (int) (n + 0.5f);
}
}
这边的思路是先随机得到抽取的次数,再每次从水果的map中选取水果,如果有多个水果规格,再随机获取规格。获取完减去/删除对应的数据即可。感觉最后水果礼包好像还没有像截尾正态分布那样,但是也尽量做到了平均了吧,=-=。
如果有更好的见解欢迎留言区留言 ╰( ̄▽ ̄)╮