微信红包分析
n元的手气红包,假设发放m个:
1、m个红包的总额为n元。
2、每个红包至少0.01元。
3、每个红包大小的概率一样。
最直观的依次以剩余金额减去0.01×剩余红包数(保证不会出现前面几个红包用完所有的金额)为上限产生m个随机数,但这很显然越前面抢的人,红包金额越大的概率越大。
二倍均值法
每次抢到的金额区间为:[0.01, 剩下的钱/剩下的个数×2)
举个例子,有100元的红包,要分成10个,则:
(1)100/10×2=20,第一个人抢的红包是[0.01-20),平均可以抢到10元;
(2)第二个人从剩余的90元中抢红包的范围也是[0.01-20);
以此类推到第9个人,每次概率都是相等的;
第10个人所选择的是100-前九个红包的和。这样能够保证每次的金额是随机的,而且每次金额的平均值相等,不会因为抢红包的顺序而造成不公平。
/*
* 产生m个n元的手气红包
* @param 红包总额
* @param 红包个数
*/
public float[] redEnvelopes(float n, int m) {
float[] result = new float[m];
if(m == 1) {
result[0] = n;
return result;
}
for(int i = 0; i < m-1; i++) {
float tmp1 = n / (m - i) * 2 - 0.01f;
float tmp2 = 0.01f + (float) (tmp1 * Math.random()); // 产生[0.01~n/(m-i)*2)的随机数
result[i] = (float) (Math.floor(tmp2*100))/100; // 乘以100在除以100是为了只保留两位小数
n -= result[i];
}
result[m-1] = (float) (Math.round(n*100))/100;
return result;
}
10次运行结果
[3.11, 1.59, 0.24, 26.66, 22.26, 3.41, 9.55, 2.22, 15.8, 15.16]
[0.91, 17.69, 13.33, 15.93, 3.14, 12.72, 10.99, 7.11, 3.31, 14.87]
[9.59, 4.53, 1.39, 14.19, 13.39, 11.69, 0.11, 25.55, 9.69, 9.87]
[5.5, 14.0, 1.64, 8.18, 14.03, 3.63, 22.74, 0.29, 5.09, 24.9]
[0.48, 10.45, 5.51, 16.74, 16.46, 13.77, 16.35, 5.14, 0.28, 14.82]
[3.8, 0.21, 6.36, 15.06, 18.87, 15.44, 4.91, 5.55, 6.0, 23.8]
[1.77, 3.38, 3.38, 3.55, 27.36, 12.69, 3.78, 23.94, 12.48, 7.67]
[7.27, 9.78, 2.05, 1.89, 11.09, 9.68, 1.62, 1.33, 16.54, 38.75]
[19.08, 9.7, 17.27, 13.77, 13.07, 6.02, 0.65, 9.18, 10.59, 0.67]
[3.17, 13.89, 8.88, 0.9, 3.58, 5.77, 7.85, 5.72, 40.93, 9.31]
这种方法存在一个问题:除了最后一个红包,前m-1个人所能抢到的红包最大金额都不会超过其所设的上限,以上面的例子来说,第一个人抢到的金额不可能超过20,虽然能够保证公平,但这不是真正的随机。
线段切割法
此方法把金额n看作是一条长度为n的线段,随机生成m-1个值将线段分成m段,则产生的m段的长度即为m个红包的金额。但需要注意的是产生重复值碰撞的情况,出现这种情况,可以再重新产生一个随机数或者也可以等概率对产生的重复数字加减0.01,如果还是重复继续上述操作。
/*
* explian:产生m个n元的手气红包
* @param 红包总额
* @param 红包个数
*/
public float[] redEnvelopes(float n, int m) {
float[] result = new float[m];
if(m == 1) {
result[0] = n;
return result;
}
float[] tmp = new float[m+1];
tmp[0] = 0.0f;
int len = 0;
while(len < m-1) {
float tmp1 = n - 0.01f;
float tmp2 = (float) (tmp1 * Math.random());
float tmp3 = 0.01f + (float) (Math.round(tmp2*100))/100;
if(!isContains(tmp, tmp3, 1, len)) {
tmp[len+1] = tmp3;
len++;
}
}
tmp[m] = n;
//System.out.print(tmp);
Arrays.sort(tmp);
for(int i = 0; i < m; i++) {
result[i] = (float) (Math.round((tmp[i+1] - tmp[i])*100))/100;
}
return result;
}
/*
* 判断target是否存在于数组中
* 数组
* 目标数字
* 起始位置
* 终止位置
*/
public boolean isContains(float[] nums, float target, int start, int end) {
if(start > end)
return false;
for(int i = start; i <= end; i++) {
if(Math.abs(nums[i] - target) <= 0.01) {
return true;
}
}
return false;
}
10次运行结果
[6.09, 2.32, 13.51, 7.13, 3.4, 4.69, 8.43, 4.93, 32.91, 16.59]
[0.61, 9.95, 3.44, 14.94, 0.25, 12.63, 15.83, 16.14, 16.63, 9.58]
[24.84, 3.15, 0.59, 4.95, 11.82, 0.49, 28.46, 2.51, 3.42, 19.77]
[0.12, 7.1, 21.57, 15.02, 5.0, 11.71, 20.42, 9.4, 7.03, 2.63]
[7.44, 25.34, 4.41, 10.57, 5.34, 7.25, 14.41, 15.53, 5.19, 4.52]
[14.55, 2.03, 4.81, 1.42, 12.64, 2.96, 13.01, 10.76, 24.36, 13.46]
[6.58, 3.99, 9.26, 0.81, 20.32, 32.23, 12.19, 4.16, 10.23, 0.23]
[3.46, 11.89, 11.1, 2.68, 0.66, 1.23, 9.62, 1.49, 11.77, 46.1]
[4.25, 13.9, 0.67, 10.93, 2.34, 4.79, 26.23, 2.49, 26.88, 7.52]
[13.97, 4.7, 0.89, 0.61, 0.02, 60.1, 1.28, 0.73, 5.75, 11.95]
这种方法也有一个问题就是可能会一直在循环里面,比如0.06元分成5个红包,产生碰撞的概率将会很大,很难跳出循环。
还有其他方法的大佬们,欢迎探讨!!!
求探讨!!!