憋瘋了?在家宅不下去了?想出去透透氣了?
千萬別!
雖然你對疫情已經麻木了,覺得沒什麼大礙了。
但現實並非如此,抗疫戰鬥仍在繼續,還沒有達到鍾南山等專家所說的爆發期。
如果大家現在要是出門,真的是在疫情防控添亂!
不信?一名程序員,連夜打造的計算機仿真程序,模擬新冠病毒傳播,在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 * newRandom().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) {/<person>
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;
}
}
}
最後,希望大家能夠多點耐心,身體健康最重要。等疫情過去,再撒歡兒玩~
閱讀更多 小丸子是程序猿 的文章