憋疯了?在家宅不下去了?想出去透透气了?

千万别!

虽然你对疫情已经麻木了,觉得没什么大碍了。

但现实并非如此,抗疫战斗仍在继续,还没有达到钟南山等专家所说的爆发期。
如果大家现在要是出门,真的是在疫情防控添乱!

不信?一名程序员,连夜打造的计算机仿真程序,模拟新冠病毒传播,在B站上播放量已达到数百万:

视频链接:

https://www.bilibili.com/video/av86478875

它告诉我们:

如果现在出门逛,迎接我们的,就是疫情越来越难控制的局面。

本文对火爆B站的仿真程序进行了深度解读:

一、JFrame面板组件布局

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

为什么要讲高斯分布?

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

2.1 高斯分布概念

一个非常常见的连续概率分布。正态分布在统计学上十分重要,经常用在自然和社会科学来代表一个不明的随机变量。

例如说人的体重、身高、某种疾病的患病年龄、城市人口的分布,基本都是符合高斯分布的统计学应用。高斯分布的公式为:

纵观整个高斯公式,有两个很重要的参数:μ(可以谐音为 “谬”),σ(发音为sigma);那么跟大家解释一下μ为平均值,σ为标准差。

标准差决定了整个样本数据的分布密度,标准差越小,数据越集中,如下图非常直观的描述的μ与σ的关系,以及σ对整个概率分布的影响。

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

2.2 正态变量的标准化

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

v 为服从高斯分布的数据

σ 为标准差

μ 为平均值

W 对v进行标准化处理后的数据,依然是服从高斯分布的

正态变量的标准化公式:

W = (V - μ) / σ

那么V值的计算公式为:

V = W * σ + μ

2.3 高斯分布在Java中的应用

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

/* * @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() {
   

// See Knuth, ACP, Section 3.4.1 Algorithm C. if (haveNextNextGaussian) {
   

haveNextNextGaussian = false; return nextNextGaussian;

} else {
   


double v1, v2, s;

do {
   

v1 = 2 * nextDouble() - 1; // between -1 and 1

v2 = 2 * nextDouble() - 1; // between -1 and 1 s = v1 * v1 + v2 * v2;

} while (s >= 1 || s == 0);

double multiplier = StrictMath.sqrt(-2 * StrictMath.log(s)/s); nextNextGaussian = v2 * multiplier;

haveNextNextGaussian = true;

return v1 * multiplier;

}

}

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

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

double value = sigma * new Random().nextGaussian() + 0.99;

三、核心代码解读

3.1启动类函数:

3.2画布相关代码

初始化画布:

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

重写的run方法如下:

如上图所示的MyPanel.this.repaint()方法为一个钩子函数,会调用MyPanel类中法paint方法,paint方***重新设置人员的状态,数据的变更,医院的床位等。

在paint方法中会调用这个person.update()方法,这个方法至关重要。在后续有一节专门介绍。

3.3初始化感染人员


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

PersonPool的构造函数:

3.4Person类


Person的构造方法:

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

Person类中的wantMove法的实现:

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

3.5Person中的action方法

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


/** * 不同状态下的单个人实例运动行为 */

private void action() {
   

if (state == State.FREEZE || state == State.DEATH) {
    return;

//如果处于隔离或者死亡状态,则无发行动

}

if (!wantMove()) {
   

//如果不想移动

return;

}

//存在流动意愿的,将进人流动,流动位移仍然遵循标准正态分布

  if (moveTarget == null || moveTarget.isArrived()) {
   

// 如果人员没有目标的话,可能就是在家里呆烦了,他又想出⻔,那就在其目前移动的位置在随机移动


double targetX = targetSig * new Random().nextGaussian() + targetXU; double targetY = targetSig * new Random().nextGaussian() + targetYU;

// 最终想要到达的目的地

moveTarget = new MoveTarget((int) targetX, (int) targetY);

}

//计算运动位移

int dX = moveTarget.getX() - x; int dY = moveTarget.getY() - y;

// 勾股定理

double length = Math.sqrt(Math.pow(dX, 2) + Math.pow(dY, 2));//

与目标点的距离

if (length < 1) {
   

//判断是否到达目标点

moveTarget.setArrived(true); return;

}

/** * 如果没有到达目标,一步一步的走,根据坐标方向,如果为正方向,就往前移动 1, * 如果坐标轴的反方向,就移动 -1 * udx的结果只可能为两种三种情况: -1 1 0 */

int udX = (int) (dX / length); 

//x轴移动步,符号为沿x轴前进方向

if (udX == 0 && dX != 0) {
   

if (dX > 0) {
   

udX = 1;

} else {
    udX = -1;

}

}

int udY = (int) (dY / length);

//y轴移动步,符号为沿x轴前进方向

if (udY == 0 && dY != 0) {
   

if (dY > 0) {
    udY = 1;

} else {
    udY = -1;

}

} 

// 如果超过边界,就往回走

if (x > 700) {
   

//这个700也许是x方向边界的意思,因为画布大小

1000x800

//TODO:如果是边界那么似乎边界判断还差一个y方向

moveTarget = null;

if (udX > 0) {
    udX = -udX;

}

}

3.6Person中的update方法

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

/** * 对各种状态的人进行不同的处理 */

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;

//刷新时间

}

//处理病死者

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;

}

}
}

最后,希望大家能够多点耐心,身体健康最重要。等疫情过去,再撒欢儿玩~