分多次累计随机出某指定整数(多次随机整数,其和固定)的方法
Spads
摘要
本文描述了同过 n 次取随机整数,使其总和为 m 的方法,并对该方法给出了数学证明。
正文
本文分为 5 个部分
---------- ---------- ---------- ----------
1、提出问题
2、解法程序
3、测试结果
4、测试程序
5、公式证明
【提出问题】
---------- ---------- ---------- ----------
有 n 次机会,每次随机一个整数。希望这 n 个整数之和是 m ;该怎么随机呢?
对于编程语言,惯例是提供了随机函数 r() ,得到 [0, 1) 之间的一个随机浮点数。所以从编程角度来说,随机一个整数,最常见的方式就是通过对 L * r() 向下取整来获取某一个范围内的整数。以上问题就转变成为了,如何获得合适的 L ,来满足 n 次随机的整数之和为 m 。
传统的做法,就是用 2 * m / n 来做这个 L 。问题是因为取整这个操作,让这种算法会产生比较大的误差。具体误差有多大,下边测试结果一栏会详细描述。
【解法程序】 —— 以 Java 程序为例
---------- ---------- ---------- ----------
/** * <b>获取总量固定多次随机的倍率</b><br/> * 当程序需要通过一定次数随机,每次随机一个整数,最终获取总和一定的值, * 可通过此方法获得随机倍率。<br/> * 在获取此倍率 <code>randomLimit</code> 之后,每次随机时通过 * <code>(int) (new Random().nextDouble() * randomLimit)</code> 获得随机 * 结果。<br/><br/> * 本方法的核心算法,基于证明了如下二个关系式: * <pre> * (int) (2 * totalNum / chanceCount) + 1 < randomLimit * (int) (2 * totalNum / chanceCount) + 2 > randomLimit * </pre> * 具体推算方法,请见 Spads 的 Shane Loo Li 发表的日志。<br/> * http://blog.csdn.net/shanelooli/article/details/10831811 * @param totalNum 最终希望各随机值相加后的总量 * @param chanceCount 随机次数 * @return 每次随机,[0, 1) 标准随机值应该乘以的倍率 */ static public double getRandomLimit(int totalNum, int chanceCount) { double calculateBase = 2.0 * totalNum / chanceCount; int calculateBaseInt = (int) calculateBase; double randomLimit = (calculateBaseInt + 2) * (calculateBaseInt + 1) / (2 * calculateBaseInt - calculateBase + 2); return randomLimit; }
【测试结果】
---------- ---------- ---------- ----------
目标总和为 5000000
随机次数 20000
简易方法:实际随机数的总和 = 4993051, 误差 = 0.1389%
Spads Shane的新方法:实际随机数的总和 = 4997315, 误差 = 0.0537%
随机次数 50000
简易方法:实际随机数的总和 = 4946686, 误差 = 1.10661%
Spads Shane的新方法:实际随机数的总和 = 4992772, 误差 = 0.1445%
随机次数 150000
简易方法:实际随机数的总和 = 4915701, 误差 = 1.16858%
Spads Shane的新方法:实际随机数的总和 = 5003719, 误差 = 0.0743%
随机次数 500000
简易方法:实际随机数的总和 = 4758806, 误差 = 4.48234%
Spads Shane的新方法:实际随机数的总和 = 4997350, 误差 = 0.0530%
随机次数 1000000
简易方法:实际随机数的总和 = 4502306, 误差 = 9.99529%
Spads Shane的新方法:实际随机数的总和 = 5003257, 误差 = 0.0651%
随机次数 3000000
简易方法:实际随机数的总和 = 3602468, 误差 = 27.279479%
Spads Shane的新方法:实际随机数的总和 = 4999469, 误差 = 0.0106%
随机次数 5000000
简易方法:实际随机数的总和 = 2500655, 误差 = 49.499820%
Spads Shane的新方法:实际随机数的总和 = 5000972, 误差 = 0.0194%
随机次数 8000000
简易方法:实际随机数的总和 = 1598759, 误差 = 68.680180%
Spads Shane的新方法:实际随机数的总和 = 4996882, 误差 = 0.0623%
随机次数 13000000
简易方法:实际随机数的总和 = 0, 误差 = 100.0000%
Spads Shane的新方法:实际随机数的总和 = 4999994, 误差 = 0.0001%
随机次数 20000000
简易方法:实际随机数的总和 = 0, 误差 = 100.0000%
Spads Shane的新方法:实际随机数的总和 = 4995380, 误差 = 0.0924%
可以见到,无论多少次随机,简易方法比 Spads Shane 的新方法误差都要大。当随机次数较多时,简易方法误差显著增加;如果随机次数和最终所需的和达到同一个数量级,简易方法的误差就会极大,使得这种方法无法再使用。
以上测试结果报告,由下边给出的测试程序直接生成。
【测试程序】
---------- ---------- ---------- ----------
public void testRandomLimit() { // 指定多次随机的整数加起来的预期总和,并显示 int totalNum = 5000000; System.out.println("目标总和为 " + totalNum); // 随机次数 int[] chanceCounts = { 20000, 50000, 150000, 500000, 1000000, 3000000, 5000000, 8000000, 13000000, 20000000 }; // 二种方法 String[] reportTitles = {"\n简易方法:\t\t", "\nSpads Shane的新方法:\t"}; double[] randomLimits = new double[2]; // 一个随机数生成对象即可解决问题,没必要重复生成此对象 Random ran = new Random(); // 遍历各种随机次数,分别测评 for (int index, methodIndex, chanceIndex = -1; ++chanceIndex != chanceCounts.length; ) { // 获取本次测评的随机次数,并显示 StringBuilder report = new StringBuilder(); int chanceCount = chanceCounts[chanceIndex]; report.append("\n随机次数 ").append(chanceCount); // 分别指定传统方法的随机倍率,和 Spads Shane 新方法的随机倍率 randomLimits[0] = 2.0 * totalNum / chanceCount; randomLimits[1] = getRandomLimit(totalNum, chanceCount); // 分别测评二种方法,最终显示结果报告 for (methodIndex = -1; ++methodIndex != 2; ) { report.append(reportTitles[methodIndex]); int realCount = 0; double randomLimit = randomLimits[methodIndex]; for (index = -1; ++index != chanceCount; ) realCount += (int) (ran.nextDouble() * randomLimit); report.append("实际随机数的总和 = ").append(realCount); double error = Math.abs(realCount - totalNum) / (double) totalNum; int percent = (int) (error * 100); report.append(", 误差 = ").append(percent).append('.'); String decimalStr = "0000"; if (percent != 100) { decimalStr = String.valueOf((int) (error * 1000000) - percent); if (decimalStr.length() < 4) report.append("0000".substring(decimalStr.length())); } report.append(decimalStr).append('%'); } System.out.println(report.toString()); } }
【公式证明】
---------- ---------- ---------- ----------
之前我们看到,程序以一个并不太复杂的四则运算式,通过目标总和 m 与随机次数 n ,得到了随机倍率参量 L 。L 自身将是一个大于 1 的实数。因为如果 L <= 1 ,则 int(L * r()) 恒为 0 。
设 Z = 2m / n
L = [int(Z) + 1][int(Z) + 2] / [2int(Z) - Z + 2]
其中 int(x) 为向下取整函数。
接下来,我们来证明这个公式的正确性。
1、通过概率核心定律,求得 L 与 m, n 的关系
概率核心定律,认为有 p 概率发生的事情,在尝试多次后,其发生比例趋近于 p 。
设用 L * r() 取随机数,平均结果为 R ,可知 R * n = m ,即 2R = Z。
这里 r() 为产生 [0, 1) 随机数的函数。
我们总是可以把 L 表示成 a + b ,其中 a = int(L),b∈[0, 1) 。因为 L > 1 ,所以 a >= 1 。
于是能够获得用 L, a 表示的 R 的表达式;其中最重要的一点,就是因为取整这种运算的存在,所以随机出 a 的概率,要小于其余整数。
R = { ∑(0 * 1/L) + (1 * 1/L) + (2 * 1/L) + ... + [(a - 1) * 1/L] } + a * (L - a)/L
根据等差数列求和公式,求 R 的表达式具体形式
R = [0/L + (a - 1)/L] * a / 2 + a * (L - a) / L
Z = 2R
= [a(a - 1) + 2a(L - a)] / L
= (2aL - a^2 - a) / L
2、证明 a = int(Z) + 1 ,开始的推论
因为 a = L - b ,b∈[0, 1) ,所以为证明 a = int(Z) + 1 ,只需要证明如下二个关系式:
int(Z) + 1 < L①
int(Z) + 2 > L②
为此,我们需要用可以确定范围的已知量,来表示 int(Z) 。
我们总是可以把 Z 表示成 int(Z) + c ,其中 c∈[0, 1) 。
根据之前的推论,我们知道 Z = (2aL - a^2 - a) / L
将 L = a + b ,b∈[0, 1) 代入上式
Z = [2a(a + b) - a^2 - a] / (a + b)
= (a^2 + 2ab - a) / (a + b)
= [(a + b)^2 - b^2 - a] / (a + b)
= a + b - (a + b^2) / (a + b)
至此,我们看到 Z = int(Z) + c = a + b - (a + b^2) / (a + b)
如果 b - (a + b^2) / (a + b) 是一个能够把绝对值范围控制在 1 以内的量,就可以通过取整原则,求得用 a, b 表示的 c 的表达式。
3、证明 b - (a + b^2) / (a + b) ∈ [-1, 0)
先证明
b - (a + b^2) / (a + b) < 0
↑
b < (a + b^2) / (a + b)
↑∵ a + b = L > 1 > 0
b(a + b) < a + b^2
↑
ab + b^2 < a + b^2
↑
ab < a
↑∵ a >= 1 > 0
b < 1
原题得证
再证明
b - (a + b^2) / (a + b) >= -1
↑
(a + b^2) / (a + b) - b <= 1
↑
(a + b^2) / (a + b) <= 1 + b
↑∵ a + b = L > 1 > 0
a + b^2 <= (a + b)(1 + b)
↑
a + b^2 <= a + ab + b + b^2
↑
0 <= ab + b
原题得证
因此,可知 b - (a + b^2) / (a + b) ∈ [-1, 0)
4、证明 a = int(Z) + 1 ,原题得证
根据上边的结论,
Z = int(Z) + c = a + b - (a + b^2) / (a + b)
int(Z) + c = a - 1 + [1 + b - (a + b^2) / (a + b)]
因为 b - (a + b^2) / (a + b) ∈ [-1, 0)
所以 1 + b - (a + b^2) / (a + b) ∈ [0, 1)
证明,如果 A + B = C + D ,A, C 为整数,B, D∈[0, 1) ,那么 B = D
∵ A + B = C + D
∴ A - C = D - B
若 A - C = 0 ,则 B = D
若 A - C >= 1 ,则 D - B = A - C >= 1
↑
D >= 1 + B
∵ D < 1 且 B >= 0
∴ D >= 1 + B 不成立。
∴ A - C >= 1 不成立。
∴ B = D 且 A = C
因此可知 c = 1 + b - (a + b^2) / (a + b) 且 int(Z) = a - 1
a = int(Z) + 1
5、求得 L 用 Z 的表达式
Z = (2aL - a^2 - a) / L
ZL = 2int(Z)L + 2L - [int(Z) + 1]^2 - int(Z) - 1
ZL - 2L - 2int(Z)L = -1 - [int(Z) + 1]^2 - int(Z)
L * [2 + 2int(Z) - Z] = [int(Z) + 1]^2 + int(Z) + 1
L = [int(Z)^2 + 2int(Z) + 1 + int(Z) + 1] / [2 + 2int(Z) - Z]
L = [int(Z)^2 + 3int(Z) + 2] / [2int(Z) - Z + 2]
L = [int(Z) + 1][int(Z) + 2] / [2int(Z) - Z + 2]
本文还发表于在其它网站
CSDN : http://blog.csdn.net/shanelooli/article/details/10831811
中国开源社区: http://my.oschina.net/shane1984/blog/158439
51CTO : http://shanelooli.blog.51cto.com/5523233/1286679
相关推荐
本文将深入解析由给定文件中的代码片段实现的无符号整数表达式的判定方法,并对其工作原理、设计思路以及潜在的应用场景进行详尽的解释和分析。 #### 代码结构与功能解析 ##### 类定义与初始化 代码首先定义了一个...
- **多次采集**:对于某些状态信号,可以通过多次重复采集并验证数据的一致性来确认其有效性。 - **延时技术**:在各次采集状态信号之间增加适当的延时,可以进一步提高抗干扰能力。 - **指令冗余和软件陷阱**:用于...
• 产生50~100 的随机整数 • 利用随机函数仅生成数字和字母 • 利用随机函数实现考试座位随机编排 • 日计帐中的余额累计 • 计扣个人所得税 • 统计月末考试中大于等于平均分的总分 • 利用CHAR 函数生成A~Z ...
4. **循环结构**:为了实现多次对战,游戏通常会包含一个循环结构,如`while`或`for`循环,让用户和电脑持续进行多轮猜拳,直到满足某种结束条件(比如达到指定回合数或一方累计达到一定胜场数)。 5. **用户界面与...
- `int`是原始类型,而`Integer`是其包装类,两者之间可以自动装箱拆箱,但`Integer`支持更多方法和特性。 - `ArrayList`和`LinkedList`的区别在于,`ArrayList`基于动态数组,查询快,增删慢;`LinkedList`基于...
4. 生成随机整数:这里使用了`random`库的`sample`方法,从0到100的整数范围内随机选取10个不重复的整数,并将结果存储在列表`List`中,最后打印出这个列表。 5. 网页数据抓取:利用`requests`库发送HTTP请求获取...
遗传算法的输出是经过多次迭代后的最佳染色体,它提供了TSP问题的一个近似最优解。 在MATLAB中,可以编写相应的代码实现这一过程,distTSP.txt文件可能是存储城市间距离的数据。通过读取这个文件,计算适应度,执行...
7、按类别拆分一个工作表中的行记录(组)到新表:是指将一个工作表中的多条记录按按照某一列或某两列的类别关键(第一关键字和第二关键字)字拆分成若干个结构相同的工作表,它适合于档案数据记录的分类。...
输入7个评委的评分,通过去除最高分和最低分,计算平均分。这里使用数组存储评分,通过`For...Next`循环找出最大值和最小值,然后计算平均分。 12. **统计字符出现次数**: 给定一个字符串`s`,使用`For...Next`...
【猜数字小游戏JAVA程序报告】 猜数字小游戏是一款基于JAVA编程...总的来说,猜数字小游戏JAVA程序的开发涵盖了JAVA的基础语法、面向对象编程、异常处理等多个方面,是一次全面的编程实践,有助于提升开发者综合技能。
文档中的内容涉及到多个VB编程的经典例题,涵盖了随机数生成、条件判断、循环控制、数学运算、数值统计以及错误处理等多个知识点。以下是这些知识点的详细解释: 1. 随机数生成:VB中,`Rnd`函数用于生成0到1之间的...
- 使用均匀多点变异,选定染色体并随机选取多个位置进行变异,变异点的值在一个范围内随机变化。 8. **反Grefenstette编码**: - 交叉和变异后的染色体需要通过反Grefenstette编码恢复到原始的自然编码,以便进行...
3. **随机数模块**:Python的`random`模块在这里起到了关键作用,它提供了生成随机数的函数,如`randint(a, b)`,可以生成a到b之间(包括a和b)的随机整数,适用于模拟骰子投掷。 4. **循环结构**:通过`for`或`...
【工作表拆分】 将当前工作表的某列数据按指定条件拆分成多个工作表,可以用任意列的数据以及选定的数据做为拆分条件。 【行列奇偶选择】 可视化对当前工作表的行与列进行快速的奇偶行或奇偶列快速选定操作。 ...
【工作表拆分】 将当前工作表的某列数据按指定条件拆分成多个工作表,可以用任意列的数据以及选定的数据做为拆分条件。 【行列奇偶选择】 可视化对当前工作表的行与列进行快速的奇偶行或奇偶列快速选定操作。 ...
- 累加操作:维护一个变量累计当前的累加和。 - 输出操作:显示最终的累加结果。 #### 8. 找出三个浮点数中的最小值 - **知识点**: - 输入处理:接收用户输入的三个浮点数。 - 比较操作:使用条件语句来确定三...
与开发人员在测试组环境多次重复以上步骤,发现11群的计次表话单有时正常,有时其出中继群号就为一个随机值,发生异常的频率比较高。为什么其它群的话单正常,唯独11群不正常呢?11群是四个群中最小的群,其中继计...
旅行商问题(Traveling Salesman Problem,TSP)是一个经典的组合优化问题,旨在寻找通过所有节点的最短路径,每个节点仅访问一次并返回起点。它在图论中被广泛研究,具有重要的理论和实际应用价值,如物流配送、...