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》數學系學生《羅鴻偉》以及他的數學鬼才同學提供的數學理論幫助。


分享到:


相關文章: