抢到红包0.01元,怎么搞的?
抢红包时你为什么只抢到0.01,拼手气红包的随机是如何运作的?
最初的随机算法中,领取越早获得大额红包几率越高,为了避免抢红包变成一个拼手速的游戏,后来的随机算法也对随机范围区间进行了一定调整。
普通随机
剩余值随机红包算法 ,一般都是使用剩余值在计算一把。用余下的值为最大区间进行随机,但可能不均匀,有些人一把随到99,下面很多人都没得随机了。
优点:逻辑简单,
缺点:随机区间步步减少,可以明显看出随机值的递减特性,做抢红包体验很差,对后面玩家极不公平,且容易被抓到规律,造成舆论不满。
二倍均值算法
该算法的核心思想:
在每次剩余的总金额和剩余人数的基础上,执行一个操作:将剩余总金额除以剩余人数,然后乘以2,得到一个边界值E。接着,在从0到E的随机区间内生成一个随机金额R,此时总金额更新为M-R,剩余人数更新为N-1。这个过程重复进行,直到最终剩余人数N-1为0,即所有随机数已经产生完毕。
- 剩余红包金额为M,剩余人数为N,每次抢到的金额 = 随机区间 (0, M / N X 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元。
- 以此类推,每一次随机范围的均值是相等的。
此外,二倍均值算法还基于一个公平的概率策略:即每个人抢到J / N的金额的概率是相同的。例如,如果有一个100元的红包要发给10个人,那么最公平的策略是使每个人抢到10元的概率相同。通过计算随机数区间上界U = J / N * 2,并在(0,U)的区间内生成第一个随机数金额M,然后继续生成第二个、第三个等随机金额,直到最后一个红包金额被生成。
1 | public static List<Double> doubleMeanMethod(double money,int number){ |
优点:
- 它能够保证每次生成的随机金额R的随机性和平等性,同时由于每次总金额M的更新采用递减方式(即M=M-R),可以确保最终产生的所有随机金额之和等于初始的总金额。
- 这种算法通过在每次迭代中更新总金额和剩余人数,确保了每个人获得金额的平均值相等,避免了因抢红包的顺序不同而导致的分配不均问题。
线段分割算法
我们将红包总额度比喻为一条同等长度的线段,如果有10个人领红包,我们就对其切割9次,线段就会分为时段,这时每人拿一段就达到了要求,非常的巧妙。
计算过程:我们用数组或者List去模拟一条线段,第一个数是0,最后一个数是红包总额,有n个人抢红包我们就生成n-1个不同的随机数插入到0和红包总额之间,然后对数组或list中的数字大小进行排序,每个人拿到的金额就是后一个数减去前一个数之差。
1 | function lineSegmentRedbag($Moneys, $userNums, $isEveryHave = 1, $baseMoney = 1) |
但是有个致命缺陷,随机值碰撞,分割数量越接近总金额,碰撞概率越大 ,所以最好用户数量与总金额差的越大越好,利用array_rand一次拿出多个随机值时,随机且去重,且随机区间包括首尾。
1 | function lineSegmentOptimize($Moneys, $userNums, $isEveryHave = 1) //$baseMoney = 1默认为1 |
总结对比
计算机到底是真随机还是假随机?
真随机数
真正的随机数是使用物理现象产生的:比如掷钱币、骰子、转轮、使用电子元件的噪音、核裂变等等,这样的随机数发生器叫做物理性随机数发生器,它们的缺点是技术要求比较高。
伪随机数
真正意义上的随机数(或者随机事件)在某次产生过程中是按照实验过程中表现的分布概率随机产生的,其结果是不可预测的,是不可见的。而计算机中的随机函数是按照一定算法模拟产生的,其结果是确定的,是可见的。我们可以这样认为这个可预见的结果其出现的概率是100%。所以用计算机随机函数所产生的“随机数”并不随机,是伪随机数。
伪随机数其实是有规律的。只不过这个规律周期比较长,但还是可以预测的。主要原因就是伪随机数是计算机使用算法模拟出来的,这个过程并不涉及到物理过程,所以自然不可能具有真随机数的特性。
使用程序产生随机数
1 |
|
执行后貌似是一串随机数。可是,当我们多次执行时,发现它的数值却还是和之前一样,所以需要设置种子。
使用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 | std::chrono::system_clock::now().time_since_epoch().count() |
这两个作为种子是更好的替代,它们都会返回精度很高的时间(单位为 long long),精度可以达到纳秒,即使连续重复调用多次也会得到不一样的返回值,它们具体的含义和差别可以在网上找到,对于算法竞赛来说用哪个都无所谓,使用时需要头文件
1 |
|
random_device(Linux下真随机)
std::random_device 是一种随机数生成器类型,如果在 Linux 下调用会通过系统提供的一个系统噪音收集器来获取真随机数,在 Windows 下就会利用很多数据来计算熵,可以近似认为是真随机的
std::random_device 在头文件
由于是真随机数,效率问题是很明显的(在 Linux 下如果系统没有足够的噪声会造成阻塞,等待系统收集到足够的噪声后才会继续运行),我们通常只用其生成一个随机数作为种子1
2
3
4
5
6
7
8
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
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 |
|
uniform_int_distribution
该函数的作用是生成一个[a,b]范围内的整数,定义的时候传进去相应的参数(数据范围即可)1
2uniform_int_distribution<int> rand1(-100, 100);
cout << rand1(rand) << " ";
uniform_real_distribution
实数域的随机生成器1
2uniform_real_distribution<double> rand2(0.0, 1.0);
cout << rand2(rand) << endl;
normal_distribution
正态分布,使用方法,第一个参数是μ,第二个是σ
1 | normal_distribution<double> N(10.0, 5.0); |
总结
工具名称 | 描述 |
---|---|
std::random_device | 一个真正的随机数生成器,通常用于为其他随机数引擎提供种子。 |
std::mt19937 | 一个基于Mersenne Twister算法的伪随机数生成器。 |
std::default_random_engine | 默认引擎,快速生成随机数 |
std::uniform_int_distribution | 用于生成均匀分布的整数随机数。 |
std::uniform_real_distribution | 用于生成均匀分布的实数随机数。 |
std::normal_distribution | 用于生成正态分布的浮点随机数。 |