java实现以某个概率测试某事是否发生
这是一个JAVA语言中的概率问题。也许在游戏编程中常常会遇到这样一种场景:某样道具有2%概率的暴击效果,在一次攻击中如何判定这种暴击效果会发生呢?这涉及到一个很简单也很有代表性的判断某概率下某次具体的实验中某事件是否发生的问题。我们当然不能人为的规定第几次一定发生或者满足某种条件就一定发生,既然是概率,就说明是事件是随机的,并非人为可控的。如何实现呢?我们知道,说到随机,就难免提及java为我们提供的随机数工具Random,Random的各种方法可以为我们产生伪随机数,虽然这些随机数也是通过某些复杂的算法产生的,与真正自然状态下的随机数有所区别,但在绝大多数情况下把它们视作真正的随机数已经足够了。
以random.nextInt(n)为例,它可以产生在区间[0,n)之间的随机数,这些随机数分布是均匀的,也就是说在[0,n)之间的任一一个数为随机数是等概率的。比如,在r=random.nextInt(100);中r为0,1,2,3,...,99的概率是相等的,均为1/100(1%)。如何来验证这一点呢?这里需要一点点的数学技巧了。一般情况下,概率无法直接计算得到,我们可以用频率来近似的表示概率。而且随着实验次数的不断增加,频率一、会越来越接近概率,并且围绕着某个数值波动,那个数值就是我们要的概率。明白这一点,我们就可以测试random.nextInt(n)产生的结果是否有足够的等可能性性。所以我们将在[0,99)这100个数内以100次,500次,1000次,5000次,10000次,以及1000000的取随机数字测试,并统计各数字出现的频率,如果每个数字出现的频率会逐步稳定在0.1左右,则说明各数字出现的概率是0.1,也就说明了random工具的等概率性。*(注:当然要说明的一点是我们采用的是不完全测试,并且将用不完全测试的结果来进行结论,这是一种不完全归纳。 但因为进行归纳的样本已经足够大,所以我有理由取信归纳的结果。)
用于进行测试的代码如下:
部分输出结果:
<pre name="code" class="java">package com.zyzz.test;
import java.util.Random;
public class PossibilityTest {
public static void main(String[] args) {
// 进行测试
baseTest(100000);
}
/**
* 频率测试
*
* @param time
*/
private static void baseTest(int time) {
// 各数字的出现次数的计数器
final int[] NumberCounter = new int[100];
// 初始化一下计数器
for (int j = 0; j < NumberCounter.length; j++) {
NumberCounter[j] = 0;
}
// 创建一个随机数发生器
final Random random = new Random();
// 用来暂存随机数结果
int rads = 0;
for (int i = 0; i < time; i++) {
rads = random.nextInt(100);
// 取得的随机数的对应计数器加一
NumberCounter[rads]++;
}
// 下面就是打印结果了
// 打印表头
System.out.println("\t\t\t" + time + "次测试\n");
System.out.println("\t数字\t|\t出现的次数\t\t频率\t");
// 频率
float f = 0.0f;
// 总频率
for (int k = 0; k < 100; k++) {
f = (float) NumberCounter[k] / time;
System.out.println("\t" + k + "\t|" + "\t" + NumberCounter[k]
+ "\t" + "\t" + f + " (" + f * 100 + "%)" + "\t");
}
}
private static void possiTest(int time) {
int counter = 0;
for (int i = 0; i < time; i++) {
if (possibility(40))
counter++;
}
System.out.println("进行" + time + "次测试");
System.out.println("true:" + counter + "\nf="
+ ((float) counter / time));
}
}
部分输出结果:
数字 | 出现的次数 频率
0 | 978 0.00978 (0.978%)
1 | 1066 0.01066 (1.066%)
2 | 977 0.00977 (0.97700006%)
3 | 978 0.00978 (0.978%)
4 | 979 0.00979 (0.979%)
5 | 1037 0.01037 (1.0370001%)
6 | 1030 0.0103 (1.03%)
7 | 1027 0.01027 (1.0270001%)
8 | 953 0.00953 (0.95300007%)
9 | 942 0.00942 (0.94200003%)
10 | 987 0.00987 (0.987%)
11 | 1055 0.01055 (1.055%)
12 | 996 0.00996 (0.99600005%)
13 | 1036 0.01036 (1.036%)
14 | 976 0.00976 (0.97599995%)
15 | 1011 0.01011 (1.011%)
16 | 981 0.00981 (0.98099995%)
17 | 967 0.00967 (0.96699995%)
18 | 997 0.00997 (0.997%)
19 | 990 0.0099 (0.99%)
20 | 1013 0.01013 (1.013%)
21 | 1017 0.01017 (1.017%)
22 | 999 0.00999 (0.999%)
23 | 962 0.00962 (0.96199995%)
24 | 1003 0.01003 (1.0029999%)
25 | 952 0.00952 (0.95199996%)
26 | 1065 0.01065 (1.0649999%)
27 | 963 0.00963 (0.963%)
28 | 978 0.00978 (0.978%)
将这几个输出结果用图形表示出来:
从图型可以看出,实验进行的次数越多,就越趋近于一条x=1.0(%)的直线,根据概率与频率的关系,可以得出r区间[0,100)其中任一个数被取出的概率为1%,这样也说明了Random工具取随机数是等概率的。
说明了这一点,利用Random工具来模拟概率问题(比如上文所说的“2%概率暴击是否发生”)就比较简单了。
首先是如何产生某个特定的概率,比如40%。因为random.nextInt(n)可能产生的总的结果数是有限的(可能的结果为[0,n)之间的n个整数),上面也通过论证了每个可能的结果是等概率的。所以适用于古典概型公式:p=符合条件的事件数/总的事件数=m/n。n=100时,需要m=40,才能达到p=m/n=40%的目标,转换成代码:
public final static boolean possibility(int possibility){
final Random random =new Random();
if(random.nextInt(100)<possibility)
return true;
return false;
}
代码就这么简单,下面来测试一下它是不是有效。就以40%为例,还是概率与频率的关系。如果上面的代码是有效的话,那么进行大量的实验,那么什么函数返回真的频率应稳定在0.4(40%)左右。验证一下
package com.zyzz.test;
import java.util.Random;
public class PossibilityTest {
public static void main(String[] args) {
// 进行测试
// baseTest(100000);
possiTest(10000);
}
/**
* 频率测试
*
* @param time
*/
private static void baseTest(int time) {
// 各数字的出现次数的计数器
final int[] NumberCounter = new int[100];
// 初始化一下计数器
for (int j = 0; j < NumberCounter.length; j++) {
NumberCounter[j] = 0;
}
// 创建一个随机数发生器
final Random random = new Random();
// 用来暂存随机数结果
int rads = 0;
for (int i = 0; i < time; i++) {
rads = random.nextInt(100);
// 取得的随机数的对应计数器加一
NumberCounter[rads]++;
}
// 下面就是打印结果了
// 打印表头
System.out.println("\t\t\t" + time + "次测试\n");
System.out.println("\t数字\t|\t出现的次数\t\t频率\t");
// 频率
float f = 0.0f;
// 总频率
for (int k = 0; k < 100; k++) {
f = (float) NumberCounter[k] / time;
System.out.println("\t" + k + "\t|" + "\t" + NumberCounter[k]
+ "\t" + "\t" + f + " (" + f * 100 + "%)" + "\t");
}
}
private static void possiTest(int time) {
int counter = 0;
for (int i = 0; i < time; i++) {
if (possibility(40))
counter++;
}
System.out.println("进行" + time + "次测试");
System.out.println("true:" + counter + "\nf="
+ ((float) counter / time));
}
private final static boolean possibility(int possibility) {
final Random random = new Random();
if (random.nextInt(100) < possibility)
return true;
return false;
}
}
多次测试的部分输出:
进行100次测试
true:37
f=0.37
进行1000次测试
true:385
f=0.385
进行10000次测试
true:4043
f=0.4043
进行1000000次测试
true:400543
f=0.400543
进行10000000次测试
true:4001353
f=0.4001353
...
从上面的输出可以看到,返回true的频率稳定在0.4左右,说明返回true的概率为40%,与我们传入的参数一致,说明这个简单到只有几行的函数还是有用的。
回到最初,2%概率暴击的问题,应该可以这样写了
static boolean attack() {
if (possibility(2)) {
System.out.println("暴击效果!");
return true;
} else {
System.out.println("一般攻击");
return false;
}
}
}
好了。