B站模拟病毒传播代码深度解读

一. JFrame面板组件布局


B站模拟病毒传播代码深度解读


二. 数学概念: 高斯(正态)分布

为什么要讲高斯分布?病毒传播代码最精华部分,最精彩部分,最能体现仿真模拟的就是这个高斯分布的代码,在本章尽量也最简单的话语为大家简单的介绍一下高斯分布的理论。

2.1 高斯分布概念

一个非常常见的连续概率分布。正态分布在统计学上十分重要,经常用在自然和社会科学来代表一个不明的随机变量。例如说人的体重、身高、某种疾病的患病年龄、城市人口的分布,基本都是符合高斯分布的统计学应用。高斯分布的公式为:


B站模拟病毒传播代码深度解读

纵观整个高斯公式,有两个很重要的参数:μ(可以谐音为 "谬"),σ(发音为sigma);那么跟大家解释一下μ为平均值,σ为标准差。标准差决定了整个样本数据的分布密度,标准差越小,数据越集中,如下图非常直观的描述的μ与σ的关系,以及σ对整个概率分布的影响。


B站模拟病毒传播代码深度解读

另外,如果μ=0, σ=1的时候,就称之为标准高斯分布。而X轴落在整个高斯曲线内的值,专业的说法为服从高斯分布。

2.2 正态变量的标准化

正态变量的标准化是高斯公式的一个重要推导,这里直接给出结论:

<code>v 为服从高斯分布的数据
σ 为标准差
μ 为平均值
W 对v进行标准化处理后的数据,依然是服从高斯分布的/<code>

正态变量的标准化公式:

<code>W = (V - μ) / σ /<code>

那么V值的计算公式为:

<code>V = W * σ + μ/<code>

2.3 高斯分布在Java中的应用

java.util.Random函数有个方法叫做nextGaussian()函数,定义如下:

<code>/*
* @return the next pseudorandom, Gaussian ("normally") distributed
* {@code double} value with mean {@code 0.0} and
* standard deviation {@code 1.0} from this random number
* generator's sequence
*/
synchronized public double nextGaussian() {
\t// See Knuth, ACP, Section 3.4.1 Algorithm C.
\tif (haveNextNextGaussian) {
\t\thaveNextNextGaussian = false;
\t\treturn nextNextGaussian;
\t} else {
\t\tdouble v1, v2, s;
\t\tdo {
\t\t\tv1 = 2 * nextDouble() - 1; // between -1 and 1
\t\t\tv2 = 2 * nextDouble() - 1; // between -1 and 1

\t\t\ts = v1 * v1 + v2 * v2;
\t\t} while (s >= 1 || s == 0);
\t\tdouble multiplier = StrictMath.sqrt(-2 * StrictMath.log(s)/s);
\t\tnextNextGaussian = v2 * multiplier;
\t\thaveNextNextGaussian = true;
\t\treturn v1 * multiplier;
\t}
} /<code>

根据API的描述:返回一个double类型的值,这个值服从均值为0,均方差为1的标准正态分布。

然后根据正态变量的标准化推导公式:

<code>double value = sigma * new Random().nextGaussian() + 0.99;/<code>

三. 核心代码解读

3.1 启动类函数:


B站模拟病毒传播代码深度解读

3.2 画布相关代码

初始化画布:


B站模拟病毒传播代码深度解读

如上图所示的MyPanel 类实现了Runnable接口:


B站模拟病毒传播代码深度解读

重写的run方法如下:


B站模拟病毒传播代码深度解读

如上图所示的 MyPanel.this.repaint() 方法为一个钩子函数,会调用 MyPanel 类中的 paint 方法,paint 方法会重新设置人员的状态,数据的变更,医院的床位等。在 paint 方法中会调用 person.update() 方法这个方法至关重要。在后续有一节专门介绍

3.3 初始化感染人员


B站模拟病毒传播代码深度解读

代码中会看到PersonPool这个类,这个类中有人员池这样一个静态变量,在加载时候去初始化城市人口:


B站模拟病毒传播代码深度解读

PersonPool的构造函数:


B站模拟病毒传播代码深度解读

3.4 Person类


B站模拟病毒传播代码深度解读

Person的构造方法:


B站模拟病毒传播代码深度解读

为了方便大家理解,给出下面这张图:


B站模拟病毒传播代码深度解读

Person 类中的 wantMove 方法的实现:


B站模拟病毒传播代码深度解读

Person类中distance方法的实现,用以判断是否能被感染:


B站模拟病毒传播代码深度解读

3.5 Person中的action方法

action方法是一个至关重要的方法,故单独提出为一个章节,该方法决定了用户坐标的移动,方法如下:

<code>/**
* 不同状态下的单个人实例运动行为
*/
private void action() {
\tif (state == State.FREEZE || state == State.DEATH) {
\t\treturn;//如果处于隔离或者死亡状态,则无法行动
\t}
\tif (!wantMove()) { //如果不想移动
\t\treturn;
\t}
\t//存在流动意愿的,将进行流动,流动位移仍然遵循标准正态分布
\tif (moveTarget == null || moveTarget.isArrived()) {
\t\t// 如果人员没有目标的话,可能就是在家里呆烦了,他又想出门,那就在其目前移动的位置在随机移动
\t\tdouble targetX = targetSig * new Random().nextGaussian() + targetXU;
\t\tdouble targetY = targetSig * new Random().nextGaussian() + targetYU;
\t\t// 最终想要到达的目的地
\t\tmoveTarget = new MoveTarget((int) targetX, (int) targetY);
\t}

\t//计算运动位移
\tint dX = moveTarget.getX() - x;
\tint dY = moveTarget.getY() - y;
\t// 勾股定理
\tdouble length = Math.sqrt(Math.pow(dX, 2) + Math.pow(dY, 2));//与目标点的距离

\tif (length < 1) {

\t\t//判断是否到达目标点
\t\tmoveTarget.setArrived(true);
\t\treturn;
\t}
\t/**
\t * 如果没有到达目标,一步一步的走,根据坐标方向,如果为正方向,就往前移动1,
\t * 如果坐标轴的反方向,就移动 -1
* udx的结果只可能为两种三种情况:-1 1 0
\t */
\tint udX = (int) (dX / length); //x轴移动步长,符号为沿x轴前进方向
\tif (udX == 0 && dX != 0) {
\t\tif (dX > 0) {
\t\t\tudX = 1;
\t\t} else {
\t\t\tudX = -1;
\t\t}
\t}

\tint udY = (int) (dY / length);//y轴移动步长,符号为沿x轴前进方向
\tif (udY == 0 && dY != 0) {
\t\tif (dY > 0) {
\t\t\tudY = 1;
\t\t} else {
\t\t\tudY = -1;
\t\t}
\t}

\t// 如果超过边界,就往回走
\tif (x > 700) {
\t\t//这个700也许是x方向边界的意思,因为画布大小1000x800
\t\t//TODO:如果是边界那么似乎边界判断还差一个y方向 \t
\t\tmoveTarget = null;
\t\tif (udX > 0) {
\t\t\tudX = -udX;
\t\t}

\t}
\tmoveTo(udX, udY); //移动对应的目标
}/<code>

3.6 Person中的update方法

update方法是一个至关重要的方法,故单独提出为一个章节,该方法决定了根据用户不同的状态决定如何处理,方法如下:

<code>/**
* 对各种状态的人进行不同的处理
*/
public void update() {
//@TODO找时间改为状态机
if (state == State.FREEZE || state == State.DEATH) {
return;//如果已经隔离或者死亡了,就不需要处理了
}
//处理已经确诊的感染者(即患者)
//
if (state == State.CONFIRMED && dieMoment == 0) {
int destiny = new Random().nextInt(10000)+1;//命运数字,[1,10000]随机数
if (1 <= destiny && destiny <= (int)(Constants.FATALITY_RATE * 10000)) {
//如果命运数字落在死亡区间
int dieTime = (int) (Constants.DIE_VARIANCE * new Random().nextGaussian()+Constants.DIE_TIME);
dieMoment = confirmedTime + dieTime;//发病后确定死亡时刻
//System.out.printf("%d,%f,%d\\n",destiny,Constants.FATALITY_RATE * 10000,dieTime);
}
else {
dieMoment = -1;//逃过了死神的魔爪
}

}
//*/

if (state == State.CONFIRMED && MyPanel.worldTime - confirmedTime >= Constants.HOSPITAL_RECEIVE_TIME) {
//如果患者已经确诊,且(世界时刻-确诊时刻)大于医院响应时间,即医院准备好病床了,可以抬走了

Bed bed = Hospital.getInstance().pickBed();//查找空床位
if (bed == null) {
//没有床位了
// System.out.println("隔离区没有空床位");
} else {
//安置病人
state = State.FREEZE;
x = bed.getX();
y = bed.getY();
bed.setEmpty(false);
}
}
//处理病死者
if((state == State.CONFIRMED || state == State.FREEZE )&& MyPanel.worldTime >= dieMoment && dieMoment > 0) {
state = State.DEATH;//患者死亡
}
//处理发病的潜伏期感染者
if (MyPanel.worldTime - infectedTime > Constants.SHADOW_TIME && state == State.SHADOW) {
state = State.CONFIRMED;//潜伏者发病
confirmedTime = MyPanel.worldTime;//刷新时间
}
//处理未隔离者的移动问题
action();
//处理健康人被感染的问题
List<person> people = PersonPool.getInstance().personList;
if (state >= State.SHADOW) {
return;
}
// 循环判断该用户是否会被其他人感染
for (Person person : people) {
//如果其他人为健康的,就继续判断下一个人
if (person.getState() == State.NORMAL) {
continue;
}
// 随机生成一个值
float random = new Random().nextFloat();
// 如果概率大于感染概率,并且小于安全距离,那么当前这个人肯定会被感染

if (random > Constants.BROAD_RATE && distance(person) < SAFE_DIST) {
this.beInfected();
// 如果被感染了,就继续判断下一个
break;
}
}
}/<person>/<code>

鸣谢:

1.感谢原作者Bruce Young提供的代码。

2.本人提交了2个 issue(https://github.com/KikiLetGo/VirusBroadcast/issues/28, https://github.com/KikiLetGo/VirusBroadcast/issues/25),其中一个已经修复,另外一个待确认。

3.感谢《广州-Java1907》数学系学生《罗鸿伟》以及他的数学鬼才同学提供的数学理论帮助。


分享到:


相關文章: