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

Commit 9de4b0d

Browse files
refactor(KCoreDecomposition): O(V+E) incremental core density profile + cache shells
- Replace O(degeneracy × E) getCoreDensityProfile() that re-scanned all edges per core level with incremental shell-peeling approach - Start from full edge count and subtract edges incident to removed shell vertices, giving O(V+E) total work - Cache getCoreShells() result to avoid redundant recomputation across getCoreDensityProfile(), getResult(), getSummary() calls - Use GraphUtils.neighborsOf() for null-safe neighbor access - Fix potential integer overflow in density calc with (long) cast
1 parent 4d82207 commit 9de4b0d

1 file changed

Lines changed: 49 additions & 18 deletions

File tree

Gvisual/src/gvisual/KCoreDecomposition.java

Lines changed: 49 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ public class KCoreDecomposition {
4444
private Map<String, Integer> coreness;
4545
private int degeneracy;
4646
private boolean computed;
47+
private SortedMap<Integer, List<String>> cachedShells; // cached to avoid recomputation
4748

4849
/**
4950
* Creates a new KCoreDecomposition for the given graph.
@@ -59,6 +60,7 @@ public KCoreDecomposition(Graph<String, Edge> graph) {
5960
this.coreness = new LinkedHashMap<String, Integer>();
6061
this.degeneracy = 0;
6162
this.computed = false;
63+
this.cachedShells = null;
6264
}
6365

6466
// ── Core decomposition (Batagelj–Zaversnik) ────────────────────
@@ -208,6 +210,7 @@ public int getDegeneracy() {
208210
*/
209211
public SortedMap<Integer, List<String>> getCoreShells() {
210212
ensureComputed();
213+
if (cachedShells != null) return cachedShells;
211214
SortedMap<Integer, List<String>> shells = new TreeMap<Integer, List<String>>();
212215
for (Map.Entry<String, Integer> entry : coreness.entrySet()) {
213216
int k = entry.getValue();
@@ -220,6 +223,7 @@ public SortedMap<Integer, List<String>> getCoreShells() {
220223
for (List<String> shell : shells.values()) {
221224
Collections.sort(shell);
222225
}
226+
cachedShells = shells;
223227
return shells;
224228
}
225229

@@ -299,35 +303,62 @@ public String toString() {
299303
* The density should increase as k grows, since higher cores are
300304
* progressively denser subgraphs.
301305
*
306+
* <p>Uses an incremental approach: starts with all vertices (0-core)
307+
* and removes vertices shell-by-shell from lowest to highest k,
308+
* subtracting edges incident to removed vertices. This gives O(V + E)
309+
* total work instead of the previous O(degeneracy × E) approach that
310+
* re-scanned all edges per core level.</p>
311+
*
302312
* @return list of CoreDensity objects, sorted by k ascending
303313
*/
304314
public List<CoreDensity> getCoreDensityProfile() {
305315
ensureComputed();
306316
List<CoreDensity> profile = new ArrayList<CoreDensity>();
307317

308-
for (int k = 0; k <= degeneracy; k++) {
309-
Set<String> coreVertices = new HashSet<String>();
310-
for (Map.Entry<String, Integer> entry : coreness.entrySet()) {
311-
if (entry.getValue() >= k) {
312-
coreVertices.add(entry.getKey());
313-
}
314-
}
318+
if (coreness.isEmpty()) return profile;
315319

316-
if (coreVertices.isEmpty()) continue;
320+
// Start with all vertices in the 0-core
321+
Set<String> coreVertices = new HashSet<String>(coreness.keySet());
317322

318-
// Count edges within the k-core
319-
int edgeCount = 0;
320-
for (Edge e : graph.getEdges()) {
321-
String v1 = graph.getEndpoints(e).getFirst();
322-
String v2 = graph.getEndpoints(e).getSecond();
323-
if (coreVertices.contains(v1) && coreVertices.contains(v2)) {
324-
edgeCount++;
325-
}
326-
}
323+
// Count all edges once (the 0-core edge count)
324+
int edgeCount = graph.getEdgeCount();
325+
326+
SortedMap<Integer, List<String>> shells = getCoreShells();
327+
328+
for (int k = 0; k <= degeneracy; k++) {
329+
if (coreVertices.isEmpty()) break;
327330

328331
int v = coreVertices.size();
329-
double density = v > 1 ? (2.0 * edgeCount) / (v * (v - 1)) : 0.0;
332+
double density = v > 1 ? (2.0 * edgeCount) / ((long) v * (v - 1)) : 0.0;
330333
profile.add(new CoreDensity(k, v, edgeCount, density));
334+
335+
// Remove the k-shell vertices (coreness == k) to get the (k+1)-core.
336+
// Count edges to subtract BEFORE removing vertices from the set.
337+
List<String> shell = shells.get(k);
338+
if (shell != null) {
339+
Set<String> shellSet = new HashSet<String>(shell);
340+
341+
// Count edges from shell vertices to the rest of coreVertices.
342+
// Each edge is counted once: either to a higher-core neighbor,
343+
// or between two shell vertices (using compareTo tie-break).
344+
for (String removed : shell) {
345+
for (String neighbor : GraphUtils.neighborsOf(graph, removed)) {
346+
if (!coreVertices.contains(neighbor)) continue;
347+
if (shellSet.contains(neighbor)) {
348+
// Intra-shell edge: count once using vertex ordering
349+
if (removed.compareTo(neighbor) < 0) {
350+
edgeCount--;
351+
}
352+
} else {
353+
// Edge to a vertex staying in the (k+1)-core
354+
edgeCount--;
355+
}
356+
}
357+
}
358+
359+
// Now remove all shell vertices
360+
coreVertices.removeAll(shellSet);
361+
}
331362
}
332363

333364
return profile;

0 commit comments

Comments
 (0)