0%

拼手气红包只抢了0.01,阴谋!

抢到红包0.01元,怎么搞的?

抢红包时你为什么只抢到0.01,拼手气红包的随机是如何运作的?
最初的随机算法中,领取越早获得大额红包几率越高,为了避免抢红包变成一个拼手速的游戏,后来的随机算法也对随机范围区间进行了一定调整。

普通随机

剩余值随机红包算法 ,一般都是使用剩余值在计算一把。用余下的值为最大区间进行随机,但可能不均匀,有些人一把随到99,下面很多人都没得随机了。

优点:逻辑简单,
缺点:随机区间步步减少,可以明显看出随机值的递减特性,做抢红包体验很差,对后面玩家极不公平,且容易被抓到规律,造成舆论不满。

二倍均值算法

该算法的核心思想:
在每次剩余的总金额和剩余人数的基础上,执行一个操作:将剩余总金额除以剩余人数,然后乘以2,得到一个边界值E。接着,在从0到E的随机区间内生成一个随机金额R,此时总金额更新为M-R,剩余人数更新为N-1。这个过程重复进行,直到最终剩余人数N-1为0,即所有随机数已经产生完毕。

  1. 剩余红包金额为M,剩余人数为N,每次抢到的金额 = 随机区间 (0, M / N X 2),
  2. 假设有10个人,红包总额100元。
  3. 100/10X2 = 20, 所以第一个人的随机范围是(0,20 ),平均可以抢到10元。
  4. 假设第一个人随机到10元,那么剩余金额是100-10 = 90 元。
  5. 90/9X2 = 20, 所以第二个人的随机范围同样是(0,20 ),平均可以抢到10元。
  6. 假设第二个人随机到10元,那么剩余金额是90-10 = 80 元。
  7. 80/8X2 = 20, 所以第三个人的随机范围同样是(0,20 ),平均可以抢到10元。
  8. 以此类推,每一次随机范围的均值是相等的。

此外,二倍均值算法还基于一个公平的概率策略:即每个人抢到J / N的金额的概率是相同的。例如,如果有一个100元的红包要发给10个人,那么最公平的策略是使每个人抢到10元的概率相同。通过计算随机数区间上界U = J / N * 2,并在(0,U)的区间内生成第一个随机数金额M,然后继续生成第二个、第三个等随机金额,直到最后一个红包金额被生成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static List<Double> doubleMeanMethod(double money,int number){
List<Double> result = new ArrayList<Double>();
if(money<0&&number<1)
return null;
double amount,sum=0;
int remainingNumber=number;
int i=1;
while(remainingNumber>1){
amount= nextDouble(0.01,2*(money/remainingNumber));
sum+=amount;
System.out.println("第"+i+"个人领取的红包金额为:"+format(amount));
money -= amount;
remainingNumber--;
result.add(amount);
i++;
}
result.add(money);
System.out.println("第"+i+"个人领取的红包金额为:"+format(money));
sum+=money;
System.out.println("验证发出的红包总金额为:"+format(sum));

return result;
}

优点:

  • 它能够保证每次生成的随机金额R的随机性和平等性,同时由于每次总金额M的更新采用递减方式(即M=M-R),可以确保最终产生的所有随机金额之和等于初始的总金额。
  • 这种算法通过在每次迭代中更新总金额和剩余人数,确保了每个人获得金额的平均值相等,避免了因抢红包的顺序不同而导致的分配不均问题。

线段分割算法

我们将红包总额度比喻为一条同等长度的线段,如果有10个人领红包,我们就对其切割9次,线段就会分为时段,这时每人拿一段就达到了要求,非常的巧妙。

计算过程:我们用数组或者List去模拟一条线段,第一个数是0,最后一个数是红包总额,有n个人抢红包我们就生成n-1个不同的随机数插入到0和红包总额之间,然后对数组或list中的数字大小进行排序,每个人拿到的金额就是后一个数减去前一个数之差。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
function lineSegmentRedbag($Moneys, $userNums, $isEveryHave = 1, $baseMoney = 1)
{
if ($Moneys <= 0 || $userNums <= 0) {
return ['code' => -3, 'msg' => '红包金额或拆红包总人数不合法'];
}
if ($isEveryHave && $userNums > $Moneys) {
return ['code' => -4, 'msg' => '红包数量不足'];
}

$cutPoints = []; //切割点数组
$pointNums = $userNums - 1; //存放的
$userMoney = []; //每一个用户该分得的钱
//正式线段分割,完全随机
// $j = 0;
// 当 用户数 和 总金额差距不大时,这种写法效率极差
while ($pointNums > 0) {
if ($isEveryHave == 1) {
$randVal = mt_rand(1, $Moneys - 1); //每个人都有,mt_rand包含区间边界的,即包含最大值 和 最小值 ,1和2都会出现
} else {
$randVal = mt_rand(0, $Moneys); //所有用户,全区间随机,保证了公平,所有人概率一致 0~10。如果$Moneys设置-1,导致最后一位必定不为0
}

if (in_array($randVal, $cutPoints)) { //这边会产生随机碰撞,500个随机需要2500次左右才能覆盖。
// $j++;
continue;
}
$cutPoints[] = $randVal;
$pointNums--;
}

// echo "无效循环次数:" . $j . "<br>";
// echo "最终切割点数组数量:" . count($cutPoints) . "<br>";
// var_dump($cutPoints);
// return;

//根据cutPoint计算每个人所得 同时考虑:就一个人
$lastVal = 0;
if (count($cutPoints) > 0) {
sort($cutPoints);
foreach ($cutPoints as $RandPoint) {
$userMoney[] = $RandPoint - $lastVal;
$lastVal = $RandPoint;
}
}

$lastDiff = $Moneys - $lastVal;
$userMoney[] = $lastDiff;

// echo "总数:" . count($userMoney) . "<br>";
// echo "总值:" . array_sum($userMoney) . "<br>";
return ['code' => 0, 'msg' => "success", 'redbag' => $userMoney];
}

但是有个致命缺陷,随机值碰撞,分割数量越接近总金额,碰撞概率越大 ,所以最好用户数量与总金额差的越大越好,利用array_rand一次拿出多个随机值时,随机且去重,且随机区间包括首尾。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
function lineSegmentOptimize($Moneys, $userNums, $isEveryHave = 1) //$baseMoney = 1默认为1
{
if ($Moneys <= 0 || $userNums <= 0) {
return ['code' => -3, 'msg' => '红包金额或拆红包总人数不合法'];
}
if ($isEveryHave && $userNums > $Moneys) {
return ['code' => -4, 'msg' => '红包数量不足'];
}

$cutPoints = [];
$userMoney = [];

if ($isEveryHave) {
$MoneysArr = array_fill(1, $Moneys - 1, 0); //转成数组时,去掉头尾得-1,如果10,则下标是1-9
} else {
$MoneysArr = array_fill(0, $Moneys + 1, 0); //转成数组,为了保留头尾得+1,如果10,则下标是0-10,array_rand区间包含首尾
}

if ($userNums == 1) {
$userMoney[] = $Moneys;
return ['code' => 0, 'msg' => "success", 'redbag' => $userMoney];
}

$cutPoints = array_rand($MoneysArr, $userNums - 1); //多随机、且去重、且区间包含首尾,array_rand第二个值不可为0
sort($cutPoints);
$lastVal = 0;
foreach ($cutPoints as $randPoint) {
$diff = $randPoint - $lastVal;
$userMoney[] = $diff;
$lastVal = $randPoint;
}
$lastDiff = $Moneys - $lastVal;
$userMoney[] = $lastDiff;

// echo "总数:" . count($userMoney) . "<br>";
// var_dump($userMoney);
// echo "总值:" . array_sum($userMoney) . "<br>";
return ['code' => 0, 'msg' => "success", 'redbag' => $userMoney];
}

总结对比

计算机到底是真随机还是假随机?

随机数网站

真随机数

真正的随机数是使用物理现象产生的:比如掷钱币、骰子、转轮、使用电子元件的噪音、核裂变等等,这样的随机数发生器叫做物理性随机数发生器,它们的缺点是技术要求比较高。

真随机数图像

伪随机数

真正意义上的随机数(或者随机事件)在某次产生过程中是按照实验过程中表现的分布概率随机产生的,其结果是不可预测的,是不可见的。而计算机中的随机函数是按照一定算法模拟产生的,其结果是确定的,是可见的。我们可以这样认为这个可预见的结果其出现的概率是100%。所以用计算机随机函数所产生的“随机数”并不随机,是伪随机数。

伪随机数其实是有规律的。只不过这个规律周期比较长,但还是可以预测的。主要原因就是伪随机数是计算机使用算法模拟出来的,这个过程并不涉及到物理过程,所以自然不可能具有真随机数的特性。

伪随机数图像

使用程序产生随机数

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include <stdlib.h>

int main ()
{
int random = rand();
printf("%d\n",random);
random = rand();
printf("%d\n",random);
random = rand();
printf("%d\n",random);
}

执行后貌似是一串随机数。可是,当我们多次执行时,发现它的数值却还是和之前一样,所以需要设置种子。

使用time作为种子

最理想的就是时间了。时间每分每秒都在变动,正好符合我们的要求。
srand函数必须放在循环或者循环调用的外面,否则还是会得出重复的数字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
#include <time.h>
#include <stdlib.h>

int main ()
{
srand ((unsigned)time(NULL));
int random = rand();
printf("%d\n",random);
random = rand();
printf("%d\n",random);
random = rand();
printf("%d\n",random);
}

我想让产生的随机数在1-100范围内,用int random = rand()%100,这样行吗?答案是不行!!这种只适用范围较小的随机数

结果是在范围内的。但是你如果短时间内连续执行,会发现它是有规律可循的,会随着时间的推移慢慢上涨,到100后再回到0,再重新上涨,

使用n*rand()/(RAND_MAX+1.0)作为种子

一种通用的做法是

1
int)(n*rand()/(RAND_MAX+1.0))

这样产生随机数的周期会大大缩短

使用chrono作为种子

1
2
std::chrono::system_clock::now().time_since_epoch().count()
std::chrono::steady_clock::now().time_since_epoch().count()

这两个作为种子是更好的替代,它们都会返回精度很高的时间(单位为 long long),精度可以达到纳秒,即使连续重复调用多次也会得到不一样的返回值,它们具体的含义和差别可以在网上找到,对于算法竞赛来说用哪个都无所谓,使用时需要头文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>  
#include <chrono>
#include <random>

int main()
{
// obtain a seed from the system clock:
unsigned seed = std::chrono::system_clock::now().time_since_epoch().count();

std::mt19937 generator(seed); // mt19937 is a standard mersenne_twister_engine
auto number = generator();//生成一个随机数
std::cout << "Random value: " << number << std::endl;

return 0;
}

random_device(Linux下真随机)

std::random_device 是一种随机数生成器类型,如果在 Linux 下调用会通过系统提供的一个系统噪音收集器来获取真随机数,在 Windows 下就会利用很多数据来计算熵,可以近似认为是真随机的
std::random_device 在头文件 中,调用时其会返回一个 unsigned int,值域就是 unsigned int 的值域范围
由于是真随机数,效率问题是很明显的(在 Linux 下如果系统没有足够的噪声会造成阻塞,等待系统收集到足够的噪声后才会继续运行),我们通常只用其生成一个随机数作为种子

1
2
3
4
5
6
7
8
#include <iostream>
#include <random>
using namespace std;
signed main(){
random_device rand;
cout << rand() << endl;
return 0;
}

mt19937

这个奇怪的名字来源于这个生成随机数的算法 Mersenne twister(梅森旋转算法),至于其中的19937则是代表它生成的随机数的周期可以到达2^19937-1的惊人数量级,相比起 rand() 确实可以避免很多问题,而且 mt19937 几乎是最快的伪随机数生成器,效率非常高。有趣的是与很多人的直觉不同,这个算法其实是由两位日本人发明的,其中使用到了梅森质数,所以这个算法才有了这个名字

  • std::mt19937 是一个随机数生成器类型,在头文件 中,调用时其会返回一个 unsigned int,值域是 unsigned int 的值域范围
  • 如果需要更大的值域范围可以使用 std::mt19937_64,唯一的区别就是返回值变成了一个 unsigned long long,值域也就是 unsigned long long 的值域

定义时注意需要传入一个数作为种子(这里以 std::random_device 为例)

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include <random>
int main()
{
/*
* std::random_device rd;
* std::mt19937 Rnd(rd());
*/
std::mt19937 Rnd( std::random_device {} () ); /* 这样写等价于上面注释掉的写法,种子为 random_device */
std::cout << Rnd() << std::endl;
return 0;
}

default_random_engine

一个随机化的前置引擎,这个需要提供时间种子,给后面要用到的函数生成一个随机节点(并没有什么卵用,就是让后面的函数随机化更强)

1
2
3
4
5
6
7
8
#include <iostream>
#include <random>
using namespace std;
signed main(){
default_random_engine rand(time(NULL));
cout << rand() << endl;
return 0;
}

uniform_int_distribution

该函数的作用是生成一个[a,b]范围内的整数,定义的时候传进去相应的参数(数据范围即可)

1
2
uniform_int_distribution<int> rand1(-100, 100);
cout << rand1(rand) << " ";

uniform_real_distribution

实数域的随机生成器

1
2
uniform_real_distribution<double> rand2(0.0, 1.0);
cout << rand2(rand) << endl;

normal_distribution

正态分布,使用方法,第一个参数是μ,第二个是σ

1
2
3
4
5
6
7
8
9
10
normal_distribution<double> N(10.0, 5.0);
//为了方便直观的看出数据分布,把每次生成的数据出现次数+1,测试的时候输出了数据分布图像
for(register int i = 0; i < 10000; i++){
double num = nor(rand);
if ((num >= 0.0) && (num < 20.0)) ++p[int(num)];
}
for (int i = 0; i < 20; ++i) {
cout << i << "-" << (i + 1) << ": ";
cout << string(p[i] * 100 / 10000, '*') << endl;
}

总结

工具名称 描述
std::random_device 一个真正的随机数生成器,通常用于为其他随机数引擎提供种子。
std::mt19937 一个基于Mersenne Twister算法的伪随机数生成器。
std::default_random_engine 默认引擎,快速生成随机数
std::uniform_int_distribution 用于生成均匀分布的整数随机数。
std::uniform_real_distribution 用于生成均匀分布的实数随机数。
std::normal_distribution 用于生成正态分布的浮点随机数。
------赞助耶耶,加快更新!------

给耶耶买杯咖啡喝