From fb20073dc9a4b9e69a3704a51f2aaf7054d4419a Mon Sep 17 00:00:00 2001 From: "alexander.thoms" Date: Wed, 15 Apr 2026 13:42:52 -0700 Subject: [PATCH] (perf) Add early termination to PKC_original_serial k-core decomposition Skip empty core levels by tracking the minimum degree above the current level during the vertex scan. When no vertices exist at the current level, jump directly to the next non-empty level instead of incrementing by one. This avoids redundant scans for sparse graphs with gaps in core numbers. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/pkc.cpp | 17 ++++++- tests/graph_solvers_test.cpp | 93 ++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 1 deletion(-) diff --git a/src/pkc.cpp b/src/pkc.cpp index 2c73fd4..99f8e99 100644 --- a/src/pkc.cpp +++ b/src/pkc.cpp @@ -4,6 +4,8 @@ // Authors: Jingnan Shi, et al. (see THANKS for the full author list) // See LICENSE for the license information +#include + #include int free_graph(robin::pkc::graph_t* g) { @@ -514,12 +516,25 @@ void robin::pkc::PKC_original_serial(const IGraph& g, std::vector* deg) while (visited < n) { + // Scan for vertices at current level, track min degree above level + size_t min_above_level = SIZE_MAX; for (long i = 0; i < n; i++) { - if ((*deg)[i] == level) { buff[end] = i; end++; + } else if ((*deg)[i] > level && (*deg)[i] < min_above_level) { + min_above_level = (*deg)[i]; + } + } + + // If no vertices at this level, skip to next non-empty level + if (end == 0) { + if (min_above_level < SIZE_MAX) { + level = min_above_level; + } else { + break; // All vertices processed } + continue; } while (start < end) { diff --git a/tests/graph_solvers_test.cpp b/tests/graph_solvers_test.cpp index cb76fdd..7d7440e 100644 --- a/tests/graph_solvers_test.cpp +++ b/tests/graph_solvers_test.cpp @@ -296,3 +296,96 @@ TEST_CASE("max clique multiple threads") { runner(g, 126); } } + +TEST_CASE("k-core early termination produces same results") { + SECTION("sparse graph with gaps in core numbers") { + // Triangle (core 2) + isolated edge (core 1) + isolated vertices (core 0) + robin::AdjListGraph g; + g.PopulateVertices(8); + // Triangle: 0-1-2 + g.AddEdge(0, 1); + g.AddEdge(1, 2); + g.AddEdge(0, 2); + // Isolated edge: 3-4 + g.AddEdge(3, 4); + // Isolated vertices: 5, 6, 7 + + robin::KCoreDecompositionSolver solver_bz( + robin::KCoreDecompositionSolver::KCORE_SOLVER_MODE::BZ_SERIAL); + solver_bz.Solve(g); + auto core_bz = solver_bz.GetCoreNumbers(); + + robin::KCoreDecompositionSolver solver_serial( + robin::KCoreDecompositionSolver::KCORE_SOLVER_MODE::PKC_SERIAL); + solver_serial.Solve(g); + auto core_serial = solver_serial.GetCoreNumbers(); + + // Both should produce the same core numbers + REQUIRE(core_bz.size() == core_serial.size()); + for (size_t i = 0; i < core_bz.size(); ++i) { + REQUIRE(core_bz[i] == core_serial[i]); + } + + // Verify specific core numbers + REQUIRE(core_bz[0] == 2); // triangle + REQUIRE(core_bz[1] == 2); + REQUIRE(core_bz[2] == 2); + REQUIRE(core_bz[3] == 1); // edge + REQUIRE(core_bz[4] == 1); + REQUIRE(core_bz[5] == 0); // isolated + REQUIRE(core_bz[6] == 0); + REQUIRE(core_bz[7] == 0); + } + + SECTION("graph with large gap in core numbers") { + // K5 (core 4) plus isolated vertices -- levels 1, 2, 3 are empty + robin::AdjListGraph g; + g.PopulateVertices(10); + // K5 on vertices 0-4 + for (size_t i = 0; i < 5; ++i) { + for (size_t j = i + 1; j < 5; ++j) { + g.AddEdge(i, j); + } + } + // vertices 5-9 are isolated + + robin::KCoreDecompositionSolver solver( + robin::KCoreDecompositionSolver::KCORE_SOLVER_MODE::PKC_SERIAL); + solver.Solve(g); + auto core = solver.GetCoreNumbers(); + + for (size_t i = 0; i < 5; ++i) { + REQUIRE(core[i] == 4); + } + for (size_t i = 5; i < 10; ++i) { + REQUIRE(core[i] == 0); + } + REQUIRE(solver.GetMaxCoreNumber() == 4); + } + + SECTION("complete graph — no early termination opportunity") { + robin::AdjListGraph g; + g.PopulateVertices(5); + for (size_t i = 0; i < 5; ++i) { + for (size_t j = i + 1; j < 5; ++j) { + g.AddEdge(i, j); + } + } + + robin::KCoreDecompositionSolver solver( + robin::KCoreDecompositionSolver::KCORE_SOLVER_MODE::PKC_SERIAL); + solver.Solve(g); + REQUIRE(solver.GetMaxCoreNumber() == 4); + REQUIRE(solver.GetMaxKCore().size() == 5); + } + + SECTION("empty graph") { + robin::AdjListGraph g; + g.PopulateVertices(0); + + robin::KCoreDecompositionSolver solver( + robin::KCoreDecompositionSolver::KCORE_SOLVER_MODE::PKC_SERIAL); + solver.Solve(g); + REQUIRE(solver.GetMaxKCore().empty()); + } +}