Skip to content
This repository was archived by the owner on Jun 18, 2026. It is now read-only.

Commit a1ccef2

Browse files
fix: isomorphism perf + LT model correctness
1. GraphIsomorphismAnalyzer: eliminate O(V) reverse-map rebuild per isFeasible() call. The backtracking search now maintains an incremental reverse mapping (graph2->graph1) alongside the forward mapping, reducing per-check cost from O(|mapping|) to O(1) for preimage lookups. On large graphs, this removes a major bottleneck from the inner loop of the VF2-style search. 2. InfluenceSpreadSimulator.simulateLT(): fix mid-round activation bug. Previously, nodes activated earlier in the iteration were visible to later nodes in the same round, causing order-dependent cascading. This violates the synchronous activation semantics of the Linear Threshold model. Now collects all activations first, then applies them simultaneously after evaluating all nodes.
1 parent 91188ce commit a1ccef2

2 files changed

Lines changed: 50 additions & 18 deletions

File tree

Gvisual/src/gvisual/GraphIsomorphismAnalyzer.java

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -167,9 +167,10 @@ public int compare(String a, String b) {
167167

168168
// Backtracking search
169169
Map<String, String> mapping = new LinkedHashMap<String, String>();
170+
Map<String, String> reverseMapping = new HashMap<String, String>();
170171
Set<String> used2 = new HashSet<String>();
171172

172-
if (backtrack(vertices1, 0, mapping, used2, adj1, adj2, byDegree2)) {
173+
if (backtrack(vertices1, 0, mapping, reverseMapping, used2, adj1, adj2, byDegree2)) {
173174
return new IsomorphismResult(true, mapping, null,
174175
degSeq1, degSeq2);
175176
}
@@ -236,15 +237,22 @@ private Map<Integer, List<String>> groupByDegree(Graph<String, edge> g,
236237
}
237238

238239
/**
239-
* Backtracking search with degree-based candidate filtering.
240+
* Backtracking search with degree-based candidate filtering and
241+
* incremental reverse-mapping maintenance.
240242
*
241243
* For each vertex in graph1 (in order), try mapping it to each
242244
* candidate vertex in graph2 that has the same degree and hasn't
243245
* been used yet. Check feasibility (all already-mapped neighbors
244246
* must correspond) before recursing.
247+
*
248+
* <p>The reverse mapping (graph2 → graph1) is maintained incrementally
249+
* alongside the forward mapping to avoid rebuilding it from scratch
250+
* in every feasibility check — reducing per-check cost from O(|mapping|)
251+
* to O(1) for the reverse lookup.</p>
245252
*/
246253
private boolean backtrack(List<String> vertices1, int idx,
247254
Map<String, String> mapping,
255+
Map<String, String> reverseMapping,
248256
Set<String> used2,
249257
Map<String, Set<String>> adj1,
250258
Map<String, Set<String>> adj2,
@@ -264,16 +272,18 @@ private boolean backtrack(List<String> vertices1, int idx,
264272
// Feasibility check: for every neighbor of v1 that is
265273
// already mapped, the corresponding mapped vertex must
266274
// be a neighbor of v2
267-
if (isFeasible(v1, v2, mapping, adj1, adj2)) {
275+
if (isFeasible(v1, v2, mapping, reverseMapping, adj1, adj2)) {
268276
mapping.put(v1, v2);
277+
reverseMapping.put(v2, v1);
269278
used2.add(v2);
270279

271-
if (backtrack(vertices1, idx + 1, mapping, used2,
272-
adj1, adj2, byDegree2)) {
280+
if (backtrack(vertices1, idx + 1, mapping, reverseMapping,
281+
used2, adj1, adj2, byDegree2)) {
273282
return true;
274283
}
275284

276285
mapping.remove(v1);
286+
reverseMapping.remove(v2);
277287
used2.remove(v2);
278288
}
279289
}
@@ -288,9 +298,13 @@ private boolean backtrack(List<String> vertices1, int idx,
288298
* the mapped vertex mapping[n1] must be a neighbor of v2.
289299
* Also, for every neighbor n2 of v2 whose preimage is mapped,
290300
* the preimage must be a neighbor of v1.
301+
*
302+
* <p>Uses the incrementally-maintained reverse mapping for O(1)
303+
* preimage lookups instead of rebuilding it from scratch each call.</p>
291304
*/
292305
private boolean isFeasible(String v1, String v2,
293306
Map<String, String> mapping,
307+
Map<String, String> reverseMapping,
294308
Map<String, Set<String>> adj1,
295309
Map<String, Set<String>> adj2) {
296310
Set<String> neighbors1 = adj1.get(v1);
@@ -305,12 +319,8 @@ private boolean isFeasible(String v1, String v2,
305319
}
306320

307321
// Reverse check: mapped neighbors of v2 must come from neighbors of v1
308-
Map<String, String> reverse = new HashMap<String, String>();
309-
for (Map.Entry<String, String> entry : mapping.entrySet()) {
310-
reverse.put(entry.getValue(), entry.getKey());
311-
}
312322
for (String n2 : neighbors2) {
313-
String preimage = reverse.get(n2);
323+
String preimage = reverseMapping.get(n2);
314324
if (preimage != null && !neighbors1.contains(preimage)) {
315325
return false;
316326
}

Gvisual/src/gvisual/InfluenceSpreadSimulator.java

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,14 @@ public SimulationResult simulateIC(Collection<String> seeds,
108108

109109
// ─── Linear Threshold ───────────────────────────────────────
110110

111+
/**
112+
* Linear Threshold simulation.
113+
*
114+
* <p>Uses synchronous activation: all nodes are evaluated against
115+
* the current round's state, and newly activated nodes only become
116+
* visible in the next round. This prevents activation order within
117+
* a single round from affecting results.</p>
118+
*/
111119
public SimulationResult simulateLT(Collection<String> seeds, int maxRounds) {
112120
validateSeeds(seeds);
113121

@@ -129,33 +137,47 @@ public SimulationResult simulateLT(Collection<String> seeds, int maxRounds) {
129137
if (maxRounds > 0 && round > maxRounds) break;
130138
changed = false;
131139

140+
// Collect all activations for this round before applying any
141+
List<String> toActivate = new ArrayList<>();
142+
Map<String, String> activatedBy = new LinkedHashMap<>();
143+
132144
for (String node : graph.getVertices()) {
133145
if (state.get(node) != NodeState.SUSCEPTIBLE) continue;
134146

135147
Collection<String> neighbors = getNeighbors(node);
136148
if (neighbors.isEmpty()) continue;
137149

138150
int activeNeighbors = 0;
151+
String anActiveNeighbor = null;
139152
for (String neighbor : neighbors) {
140153
if (state.get(neighbor) == NodeState.INFECTED ||
141154
state.get(neighbor) == NodeState.RECOVERED) {
142155
activeNeighbors++;
156+
if (anActiveNeighbor == null) {
157+
anActiveNeighbor = neighbor;
158+
}
143159
}
144160
}
145161

146162
double fraction = (double) activeNeighbors / neighbors.size();
147163
if (fraction >= thresholds.get(node)) {
148-
state.put(node, NodeState.INFECTED);
149-
changed = true;
150-
for (String neighbor : neighbors) {
151-
if (state.get(neighbor) == NodeState.INFECTED ||
152-
state.get(neighbor) == NodeState.RECOVERED) {
153-
timeline.add(new InfectionEvent(neighbor, node, round));
154-
break;
155-
}
164+
toActivate.add(node);
165+
if (anActiveNeighbor != null) {
166+
activatedBy.put(node, anActiveNeighbor);
156167
}
157168
}
158169
}
170+
171+
// Apply all activations simultaneously
172+
for (String node : toActivate) {
173+
state.put(node, NodeState.INFECTED);
174+
changed = true;
175+
String source = activatedBy.get(node);
176+
if (source != null) {
177+
timeline.add(new InfectionEvent(source, node, round));
178+
}
179+
}
180+
159181
snapshots.add(createSnapshot(round, state));
160182
}
161183

0 commit comments

Comments
 (0)