- Problema ===========
Aqui é necessário implementar um simulador de SNN (spiking neural network, rede neural de pulsos). Uma SNN é composta pelas seguintes partes:
- Neurônios: atualizam o estado interno seguindo fórmulas, recebem pulsos e podem disparar pulsos.
- Fontes de pulso: disparam pulsos em momentos específicos.
- Sinapses: conectam neurônio-neurônio ou fonte de pulso-neurônio, transmitindo pulsos.
Descrição do Problema
Os neurônios atualizam seu estado interno seguindo regras. O tempo é discretizado com um intervalo \(\Delta t\). Consideramos apenas instentes \(t = k\Delta t\) (\(k\in\mathbb{Z}^+\)). As variáveis no instante \(k\) são calculadas a partir do instante \(k-1\) conforme:
\[v_k = v_{k-1} + \Delta t(0.04 v_{k-1}^2 + 5 v_{k-1} + 140 - u_{k-1}) + I_k\]
\[u_k = u_{k-1} + \Delta t \, a (b v_{k-1} - u_{k-1})\]
Onde \(v\) e \(u\) são variáveis internas que evoluem no tempo; \(a\) e \(b\) são constantes fixas. \(I_k\) é a soma das intensidades de todos os pulsos recebidos pelo neurônio no instante \(k\). Se não houver pulsos, \(I_k = 0\). Após o cálculo, se \(v_k \ge 30\), o neurônio dispara um pulso que se propaga pelas sinapses; então \(v_k\) é reajustado para \(c\) e \(u_k\) para \(u_k + d\), onde \(c\) e \(d\) também são constantes.
As sinapses representam conexões entre neurônios ou fontes de pulso e neurônios, incluindo auto-laços e arestas múltiplas. Quando um nó de entrada (neurônio ou fonte) dispara um pulso no instante \(k\), após um atraso \(D > 0\) (no instante \(k+D\)), o nó de saída (neurônio) recebe um pulso de intensidade \(w\).
As fontes de pulso disparam com uma probabilidade a cada instante. Cada fonte possui um parâmetro \(0 < r < 32767\). A função pseudoaleatória padrão é:
static unsigned long semente = 1;
int meu_rand(void) {
semente = semente * 1103515245 + 12345;
return (unsigned)(semente/65536) % 32768;
}
A cada instante, cada fonte de pulso (em ordem crescente de índice) chama a função. Se \(r > \text{meu_rand}()\), a fonte dispara um pulso naquele instante, que se propaga pelas sinapses.
Dado o estado inicial dos neurônios no instante 0, simula-se do instante 1 até \(T\). Ao final, imprime-se o valor mínimo e máximo de \(v\) entre todos os neurônios no instante \(T\), e o número mínimo e máximo de disparos realizados durante toda a simulação.
Os nós são numerados: \([0, N-1]\) para neurônios, \([N, N+P-1]\) para fontes de pulso.
Use ponto flutuante de precisão dupla.
Formato de Entrada
Da entrada padrão. Primeira linha: quatro enteiros \(N\), \(S\), \(P\), \(T\). Segunda linha: um real positivo \(\Delta t\). Linhas seguintes: para cada bloco de neurônios com mesmos parâmetros, um inteiro \(R_N\) seguido de seis reais \(v, u, a, b, c, d\). A soma dos \(R_N\) é \(N\). Depois: \(P\) linhas, cada uma com um inteiro \(r\) (parâmetro da fonte). Depois: \(S\) linhas, cada uma com dois inteiros \(s\) (nó de entrada) e \(t\) (nó de saída), um real \(w>0\), e um inteiro \(D\) (atraso).
Formato de Saída
Duas linhas. Primeira: dois reais com 3 casas decimais (mínimo e máximo de \(v\) no instante \(T\)). Segunda: dois inteiros (mínimo e máximo de disparos).
Exemplo 1
Entrada:
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
Saída:
-35.608 -35.608
2 2
Exemplo 2
Entrada:
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
Saída:
-60.000 -22.092
1 2
- Abordagem ============
Este é um problema de simulação que também envolve teoria dos grafos para gerenciar os pulsos. A cada instante, as fontes geram pulsos que são enviados para as sinapses com atraso. Podemos armazenar os pulsos futuros em uma estrutura indexada pelo tempo de chegada.
A estratégia é manter um vetor de filas (ou vetor de vetores) para cada instante de tempo, registrando todos os pulsos que chegarão naquele instante. Durante a simulação, processamos os pulsos do instante atual e, para cada disparo, agendamos pulsos futuros.
- Código =========
Implementação em C++ moderno, reestruturada com classes e melhor organização:
#include <bits/stdc++.h>
using namespace std;
// Semente para o gerador pseudoaleatório
unsigned long seed = 1;
int myrand() {
seed = seed * 1103515245 + 12345;
return (unsigned)(seed / 65536) % 32768;
}
// Estrutura de um pulso agendado
struct PulseEvent {
int target; // índice do neurônio de destino
double weight; // intensidade do pulso
};
// Informações de uma sinapse
struct Synapse {
int target; // neurônio de destino
double weight; // peso
int delay; // atraso em unidades de tempo
};
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int N, S, P, T;
double dt;
cin >> N >> S >> P >> T;
cin >> dt;
// Parâmetros dos neurônios (índices 0..N-1)
vector<double> v(N), u(N), a(N), b(N), c(N), d(N);
int idx = 0;
while (idx < N) {
int rn;
double vi, ui, ai, bi, ci, di;
cin >> rn >> vi >> ui >> ai >> bi >> ci >> di;
for (int i = 0; i < rn; ++i) {
v[idx] = vi;
u[idx] = ui;
a[idx] = ai;
b[idx] = bi;
c[idx] = ci;
d[idx] = di;
++idx;
}
}
// Parâmetros das fontes (índices N..N+P-1)
vector<double> r(P);
for (int i = 0; i < P; ++i) {
cin >> r[i];
}
// Grafo de sinapses: lista de adjacência para cada nó (neurônio ou fonte)
// O grafo será indexado de 0 a N+P-1 para os nós de origem.
vector<vector<Synapse>> graph(N + P);
for (int i = 0; i < S; ++i) {
int s, t;
double w;
int D;
cin >> s >> t >> w >> D;
graph[s].push_back({t, w, D});
}
// Eventos futuros: para cada instante de tempo, um vetor de pulsos que chegam
// Tamanho T+2 para garantir acesso seguro até T+1
vector<vector<PulseEvent>> futurePulses(T + 2);
// Contagem de disparos de cada neurônio
vector<int> spikeCount(N, 0);
// corrente de entrada I para cada neurônio (zera a cada passo)
vector<double> I(N, 0.0);
// Simulação para t = 1 até T
for (int time = 1; time <= T; ++time) {
// --- 1. Fontes de pulso disparam ---
for (int i = 0; i < P; ++i) {
if (r[i] > myrand()) {
// Fonte N+i dispara
for (const Synapse& syn : graph[N + i]) {
int arrival = time + syn.delay;
if (arrival <= T) {
futurePulses[arrival].push_back({syn.target, syn.weight});
}
}
}
}
// --- 2. Processar pulsos que chegam neste instante ---
fill(I.begin(), I.end(), 0.0);
for (const PulseEvent& ev : futurePulses[time]) {
I[ev.target] += ev.weight;
}
// Limpar a lista de pulsos futuros para este instante (libera memória)
futurePulses[time].clear();
futurePulses[time].shrink_to_fit();
// --- 3. Atualizar neurônios ---
for (int i = 0; i < N; ++i) {
double old_v = v[i];
double old_u = u[i];
double new_v = old_v + dt * (0.04 * old_v * old_v + 5 * old_v + 140 - old_u) + I[i];
double new_u = old_u + dt * a[i] * (b[i] * old_v - old_u);
v[i] = new_v;
u[i] = new_u;
// Verificar disparo
if (new_v >= 30.0) {
spikeCount[i]++;
v[i] = c[i];
u[i] += d[i];
// Agendar pulsos disparados por este neurônio
for (const Synapse& syn : graph[i]) {
int arrival = time + syn.delay;
if (arrival <= T) {
futurePulses[arrival].push_back({syn.target, syn.weight});
}
}
}
}
}
// Resultados finais
double min_v = *min_element(v.begin(), v.end());
double max_v = *max_element(v.begin(), v.end());
int min_spikes = *min_element(spikeCount.begin(), spikeCount.end());
int max_spikes = *max_element(spikeCount.begin(), spikeCount.end());
cout << fixed << setprecision(3);
cout << min_v << " " << max_v << "\n";
cout << min_spikes << " " << max_spikes << "\n";
return 0;
}