Skip to content

Fix predecessor DAG construction in k-shortest paths algorithm#2

Merged
networmix merged 1 commit intomainfrom
claude/fix-major-bug-8dCnf
Feb 8, 2026
Merged

Fix predecessor DAG construction in k-shortest paths algorithm#2
networmix merged 1 commit intomainfrom
claude/fix-major-bug-8dCnf

Conversation

@networmix
Copy link
Owner

Summary

This PR fixes bugs in the predecessor DAG (Directed Acyclic Graph) construction within the k-shortest paths algorithm. The changes correct how parent nodes and edges are indexed when building the DAG structure.

Key Changes

  • Fixed DAG parent/edge indexing: Replaced sequential indexing with proper offset-based indexing using dag.parent_offsets. The previous implementation used a simple counter (idx) that didn't account for the correct position of each node in the parent arrays, causing incorrect DAG construction.

  • Updated path comparison logic: Changed the path prefix comparison to use j + 1 instead of j when comparing node sequences. This ensures the correct number of nodes are compared when identifying paths that share a common prefix.

Implementation Details

The main issue was in how parents and edges were being stored in the DAG arrays. The old code:

std::size_t idx = 0;
for (std::size_t i = 1; i < P.nodes.size(); ++i) {
  dag.parents[idx] = P.nodes[i-1]; 
  dag.via_edges[idx] = P.edges[i-1]; 
  ++idx;
}

Has been corrected to:

for (std::size_t i = 1; i < P.nodes.size(); ++i) {
  auto v = P.nodes[i];
  auto base = static_cast<std::size_t>(dag.parent_offsets[static_cast<std::size_t>(v)]);
  dag.parents[base] = P.nodes[i-1]; 
  dag.via_edges[base] = P.edges[i-1];
}

This ensures each parent is stored at the correct offset corresponding to its destination node in the DAG structure. These fixes appear in two locations within the algorithm (lines 203-210 and 338-345).

https://claude.ai/code/session_011Wz1nQQWEnVTaLwfgz9LDA

…edDAG fill order

Bug 1 (off-by-one in Yen's root path comparison):
In Yen's k-shortest-paths algorithm, when determining which spur edges to
exclude, the code compared only j prefix nodes instead of j+1. This meant
the spur node itself was not included in the comparison, causing edges from
paths with different root paths to be incorrectly excluded. This resulted
in valid paths being missed during enumeration.

Bug 2 (PredDAG CSR fill order):
When converting discovered paths to PredDAG output, entries were filled
sequentially using a linear index. However, the CSR-like parent_offsets
structure requires entries to be placed at the correct offset for each
node. For paths visiting nodes out of numerical order (e.g., [0,2,1,3]),
this produced malformed PredDAGs with parent/edge entries assigned to
wrong nodes. Fixed by using node-specific offsets instead of linear index.

Both bugs affected the same function and were interconnected: Bug 1 hid
Bug 2 because the missing paths were exactly those visiting nodes out of
numerical order.

https://claude.ai/code/session_011Wz1nQQWEnVTaLwfgz9LDA
Copilot AI review requested due to automatic review settings February 8, 2026 01:57
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes incorrect predecessor DAG (PredDAG) construction in k_shortest_paths by aligning parent/edge storage with the project’s CSR-style parent_offsets layout, and corrects spur-prefix matching when excluding edges during candidate generation.

Changes:

  • Store dag.parents / dag.via_edges using parent_offsets[v] (CSR base) instead of a sequential index.
  • Fix prefix comparison during edge masking to compare j + 1 nodes (not j) when detecting shared prefixes.
Comments suppressed due to low confidence (1)

src/k_shortest_paths.cpp:258

  • Add a focused test for the spur-prefix comparison/masking behavior around j: verify that only paths sharing the prefix of length (j+1) are considered for excluding edge P.edges[j]. This helps prevent regressions where comparing only j nodes causes overly aggressive edge masking and can drop valid candidates.
      for (auto const& P : paths) {
        if (P.nodes.size() > j && std::equal(P.nodes.begin(), P.nodes.begin() + static_cast<std::ptrdiff_t>(j + 1), last.nodes.begin())) {
          // Exclude edge at position j for this path
          if (P.edges.size() > j) {
            edge_mask_local[static_cast<std::size_t>(P.edges[j])] = 0;
          }
        }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 206 to 210
for (std::size_t i = 1; i < P.nodes.size(); ++i) {
dag.parents[idx] = P.nodes[i-1]; dag.via_edges[idx] = P.edges[i-1]; ++idx;
auto v = P.nodes[i];
auto base = static_cast<std::size_t>(dag.parent_offsets[static_cast<std::size_t>(v)]);
dag.parents[base] = P.nodes[i-1]; dag.via_edges[base] = P.edges[i-1];
}
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a regression test that validates the returned PredDAG encodes the concrete path in true CSR form (parents/via_edges stored in the range parent_offsets[v]:parent_offsets[v+1] for each node v). The previous sequential fill could pass basic size/monotonicity checks but still produce incorrect parent lookups; a test that reconstructs the path from the DAG (e.g., via resolve_to_paths or by directly checking parents[parent_offsets[v]]) on a path whose node IDs are not in increasing order would catch this.

Copilot uses AI. Check for mistakes.
@networmix networmix merged commit ff9cecf into main Feb 8, 2026
17 checks passed
@networmix networmix deleted the claude/fix-major-bug-8dCnf branch February 8, 2026 12:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants