既然上次更新了那篇大模擬,再加上又有人催更了(huaji),就來一篇更加猛烈的模擬:脈沖神經網絡!
一、題目
話不多說,繼續湊字數!(((
題目背景
在本題中,你需要實現一個 SNN(spiking neural network,脈沖神經網絡)的模擬器。一個 SNN 由以下幾部分組成:
- 神經元:按照一定的公式更新內部狀態,接受脈沖並可以發放脈沖
- 脈沖源:在特定的時間發放脈沖
- 突觸:連接神經元-神經元或者脈沖源-神經元,負責傳遞脈沖
題目描述
神經元會按照一定的規則更新自己的內部狀態。本題中,我們對時間進行離散化處理,即設置一個時間間隔\(\Delta t\) ,僅考慮時間間隔整數倍的時刻 \(t=k\Delta t(k\in Z^+)\),按照下面的公式,從 \(k-1\) 時刻的取值計算 \(k\) 時刻的變量的取值:
其中 \(v\) 和 \(u\) 是神經元內部的變量,會隨着時間而變化,\(a\) 和 \(b\) 是常量,不會隨着時間變化;其中 \(I_k\) 表示該神經元在 \(k\) 時刻接受到的所有脈沖輸入的強度之和,如果沒有接受到脈沖,那么 \(I_k=0\)。當進行上面的計算后,如果滿足 \(v_k\ge30\),神經元會發放一個脈沖,脈沖經過突觸傳播到其他神經元;同時,\(v_k\) 設為 \(c\) 並且 \(u_k\) 設為 \(u_k+d\),其中 \(c\) 和 \(d\) 也是常量。圖 1 展示了一個神經元 變量隨時間變化的曲線。
圖1: 神經元 \(v\) 變量隨時間變化的曲線
突觸表示的是神經元-神經元、脈沖源-神經元的連接關系,包含一個入結點和一個出結點(可能出現自環和重邊)。當突觸的入結點(神經元或者脈沖源)在 \(k\) 時刻發放一個脈沖,那么在傳播延遲 \(D(D>0)\) 個時刻以后,也就是在 \(k+D\) 時刻突觸的出結點(神經元)會接受到一個強度為 \(w\) 的脈沖。
脈沖源在每個時刻以一定的概率發放一個脈沖,為了模擬這個過程,每個脈沖源有一個參數 \(0<r<32,767\),並統一采用以下的偽隨機函數:
C++ 版本:
static unsigned long next = 1;
/* RAND_MAX assumed to be 32767 */
int myrand(void) {
next = next * 1103515245 + 12345;
return((unsigned)(next/65536) % 32768);
}
Python 版本:
next = 1
def myrand():
global next
next = (next * 1103515245 + 12345) % (2 ** 64)
return (next // 65536) % 32768
Java 版本:
long next = 1;
int myrand() {
next = next * 1103515245 + 12345;
return (int)((Long.divideUnsigned(next, 65536)) % 32768);
}
在每個時間刻,按照編號順序從小到大,每個脈沖源調用一次上述的偽隨機函數,當 \(r>\text{myrand()}\) 時,在當前時間刻發放一次脈沖,並通過突觸傳播到神經元。
進行仿真的時候,已知 0 時間刻各個神經元的狀態,從 1 時間刻開始按照上述規則進行計算,直到完成 \(T\) 時刻的計算,再輸出 \(T\) 時刻神經元的 \(v\) 值和發放的脈沖次數分別的最小值和最大值。
規定輸入數據中結點按如下方式順序編號:\([0,N-1]\) 為神經元的編號,\([N,N+P-1]\) 為脈沖源的編號。
代碼中請使用雙精度浮點類型。
輸入格式
從標准輸入讀入數據。
輸入的第一行包括四個以空格分隔的正整數\(N\) \(S\) \(P\) \(T\) ,表示一共有 \(N\) 個神經元,\(S\) 個突觸和 \(P\) 個脈沖源,輸出時間刻 \(T\) 時神經元的 \(v\) 值。
輸入的第二行是一個正實數 \(\Delta t\) ,表示時間間隔。
輸入接下來的若干行,每行有以空格分隔的一個正整數 \(R_N\) 和六個實數 \(v\) \(u\) \(a\) \(b\) \(c\) \(d\),按順序每一行對應 \(R_N\) 個具有相同初始狀態和常量的神經元:其中 \(v\) \(u\) 表示神經元在時刻 0 時的變量取值;\(a\) \(b\) \(c\) \(d\) 為該神經元微分方程里的四個常量。保證所有的 \(R_N\) 加起來等於 \(N\)。它們從前向后按編號順序描述神經元,每行對應一段連續編號的神經元的信息。
輸入接下來的 \(P\) 行,每行是一個正整數 \(r\),按順序每一行對應一個脈沖源的 \(r\) 參數。
輸入接下來的 \(S\) 行,每行有以空格分隔的兩個整數 \(s(0\leq s<N+P)\)、\(t(0\leq t<N)\) 、一個實數 \(w(w>0)\) 和一個正整數 \(D\),其中 \(s\) 和 \(t\) 分別是入結點和出結點的編號;\(w\) 和 \(D\) 分別表示脈沖強度和傳播延遲。
輸出格式
輸出到標准輸出。
輸出共有兩行,第一行由兩個近似保留 3 位小數的實數組成,分別是所有神經元在時刻 \(T\) 時變量 \(v\) 的取值的最小值和最大值。第二行由兩個整數組成,分別是所有神經元在整個模擬過程中發放脈沖次數的最小值和最大值。
只要按照題目要求正確實現就能通過,不會因為計算精度的問題而得到錯誤答案。
樣例1輸入
1 1 1 10
0.1
1 -70.0 -14.0 0.02 0.2 -65.0 2.0
30000
1 0 30.0 2
樣例1輸出
-35.608 -35.608
2 2
樣例1解釋
該樣例有 1 個神經元、1 個突觸和 1 個脈沖源,時間間隔 \(\Delta t=0.1\)。唯一的脈沖源通過脈沖強度為 30.0、傳播延遲為 2 的突觸傳播到唯一的神經元。
該樣例一共進行 10 個時間步的模擬,隨機數生成器生成 10 次隨機數如下:
16838
5758
10113
17515
31051
5627
23010
7419
16212
4086
因此唯一的脈沖源在時刻 1-4 和 6-10 發放脈沖。在時間刻從 1 到 10 時,唯一的神經元的 取值分別為:
-70.000
-70.000
-40.000
-8.200
-65.000
-35.404
-32.895
0.181
-65.000
-35.608
該神經元在時刻 5 和時刻 9 發放,最終得到的 \(v=-35.608\) 。
樣例2輸入
2 4 2 10
0.1
1 -70.0 -14.0 0.02 0.2 -65.0 2.0
1 -69.0 -13.0 0.04 0.1 -60.0 1.0
30000
20000
2 0 15.0 1
3 1 20.0 1
1 0 10.0 2
0 1 40.0 3
樣例2輸出
-60.000 -22.092
1 2
子任務
子任務 | \(T\) | \(N\) | \(S\) | \(P\) | \(D\) | 分值 |
---|---|---|---|---|---|---|
1 | \(\leq10^2\) | \(\leq10^2\) | \(\leq10^2\) | \(\leq10^2\) | \(\leq10^2\) | 30 |
2 | \(\leq10^3\) | \(\leq10^3\) | \(\leq10^3\) | \(\leq10^3\) | \(\leq10^3\) | 40 |
3 | \(\leq10^5\) | \(\leq10^3\) | \(\leq10^3\) | \(\leq10^3\) | \(\leq10\) | 30 |
二、思路
這種恐怖的模擬題,還要處理一個重要至極的事情:圖論!
其實,我們只要處理一下脈沖就好了。
訂一個vector[i][j],表示在i的時刻所有的脈沖。
別的懶得說了。
三、代碼
官方66,運行超時了。(真不要臉,沒對還來寫題解)
#include<iostream>//頭文件emm
#include<vector>
#include<cstring>
using namespace std;
struct edge{//一條邊
int d;//D
double w;//w
int to;//連接的那一個點
};
vector<edge> G[2005];//存圖
vector<edge> js[100005];//上面寫的vector[i][j]
double I[2005];//公式中的I
int fa[2005];//發送多少次脈沖
struct sjy{//神經元
double a,b,c,d;//如果是脈沖源,用a表示r
double v,u;//變量
}th[2005];//th=things,各種東西
int n,s,p,t,rn,acnt=0;//nspt,rn表示R_N,acnt表示R_N之和
double jg;//delta_t
static unsigned long nextf = 1;//因為這個玩意似乎會編譯錯誤,所以改成nextf了
/* RAND_MAX assumed to be 32767 */
int myrand(void) {
nextf = nextf * 1103515245 + 12345;
return((unsigned)(nextf/65536) % 32768);
}
inline void input(){//輸入
ios::sync_with_stdio(0);//加速
cin>>n>>s>>p>>t;
cin>>jg;
double v,u,a,b,c,d;
while(acnt<n){//一個小技巧
cin>>rn>>v>>u>>a>>b>>c>>d;
for(int i=1;i<=rn;i++){//每次加上
th[acnt+i].a=a,th[acnt+i].b=b,th[acnt+i].c=c,th[acnt+i].d=d,th[acnt+i].v=v,th[acnt+i].u=u;
}
acnt+=rn;
}
for(int i=1;i<=p;i++) cin>>th[++acnt].a;//r
int from,to,ys;//s,t,D
double w;
for(int i=1;i<=s;i++){
cin>>from>>to>>w>>ys;
G[from+1].push_back({ys,w,to+1});//注意,有向圖!
}
}
inline void fasong(int x,int nowt){//發送脈沖
fa[x]++;//發送的多一個
edge u;//表示那邊的點
for(int i=0;i<G[x].size();i++){//遍歷
u=G[x][i];//找到那個節點
if(nowt+u.d<=t) js[nowt+u.d].push_back(u);//注意,要判斷一下是否超時
}
}
inline void chuli(int time){//定時處理
for(int i=0;i<js[time].size();i++){//找每一條
I[js[time][i].to]+=js[time][i].w;//加上I
}
vector<edge>().swap(js[time]);//這是vector一個特殊功能,就是說把一個空vector和我們這里交換
//那么js[time]就會變成空的,而這個新vector就被扔掉了,就完成了清空,可以省空間。
}
int main(){
input();
double newv,newu;
for(int time=1;time<=t;time++){//模擬每一個時刻
memset(I,0,sizeof(I));//初始化
for(int i=n+1;i<=n+p;i++){//檢查脈沖源
if(th[i].a>myrand()){
fasong(i,time);
}
}
chuli(time);//計算脈沖
for(int i=1;i<=n;i++){//檢查神經元
newv=th[i].v+jg*(0.04*th[i].v*th[i].v+5*th[i].v+140-th[i].u)+I[i];//公式
newu=th[i].u+jg*th[i].a*(th[i].b*th[i].v-th[i].u);
th[i].v=newv,th[i].u=newu;
if(newv>=30){//發送脈沖
fasong(i,time);
th[i].v=th[i].c;
th[i].u+=th[i].d;
}
}
}
double mx1=-0x3f3f3f3f,mn1=0x3f3f3f3f;//注意:答案可能有負數
int mx2=-0x3f3f3f3f,mn2=0x3f3f3f3f;
for(int i=1;i<=n;i++){//比較
mx1=max(mx1,th[i].v);
mn1=min(mn1,th[i].v);
mx2=max(mx2,fa[i]);
mn2=min(mn2,fa[i]);
}
printf("%.3lf %.3lf\n",mn1,mx1);//輸出
printf("%d %d\n",mn2,mx2);
return 0;
}
這個純模擬的題目,比較考腦子和手。
下集預告:DP!