@@ -42,23 +42,27 @@ resolve_to_paths(const PredDAG& dag, NodeId src, NodeId dst,
4242 paths.push_back (std::move (p));
4343 return paths;
4444 }
45+ if (dag.parent_offsets .size () < 2 ) return paths;
4546 if (static_cast <std::size_t >(dst) >= dag.parent_offsets .size () - 1 ) return paths;
4647 if (dag.parent_offsets [static_cast <std::size_t >(dst)] == dag.parent_offsets [static_cast <std::size_t >(dst) + 1 ]) return paths;
4748
4849 // Iterative DFS stack: each frame holds current node and index into its parent-groups.
4950 struct Frame { NodeId node; std::size_t idx; std::vector<std::pair<NodeId, std::vector<EdgeId>>> groups; };
5051 std::vector<Frame> stack;
5152 stack.reserve (16 );
53+ std::vector<char > on_path (static_cast <std::size_t >(dag.parent_offsets .size () - 1 ), 0 );
5254 // start from dst
5355 Frame start; start.node = dst; start.idx = 0 ; group_parents (dag, dst, start.groups );
5456 stack.push_back (std::move (start));
57+ on_path[static_cast <std::size_t >(dst)] = 1 ;
5558
5659 std::vector<std::pair<NodeId, std::vector<EdgeId>>> current; // reversed path accum
5760
5861 while (!stack.empty ()) {
5962 auto & top = stack.back ();
6063 if (top.idx >= top.groups .size ()) {
6164 // backtrack
65+ on_path[static_cast <std::size_t >(top.node )] = 0 ;
6266 stack.pop_back ();
6367 if (!current.empty ()) current.pop_back ();
6468 continue ;
@@ -134,13 +138,19 @@ resolve_to_paths(const PredDAG& dag, NodeId src, NodeId dst,
134138 current.pop_back ();
135139 continue ;
136140 }
141+ if (parent < 0 || static_cast <std::size_t >(parent) >= on_path.size () || on_path[static_cast <std::size_t >(parent)]) {
142+ // Skip cyclic/invalid predecessor relation.
143+ current.pop_back ();
144+ continue ;
145+ }
137146 // descend
138147 Frame next; next.node = parent; next.idx = 0 ; group_parents (dag, parent, next.groups );
139148 if (next.groups .empty ()) {
140149 // dead end, backtrack
141150 current.pop_back ();
142151 continue ;
143152 }
153+ on_path[static_cast <std::size_t >(parent)] = 1 ;
144154 stack.push_back (std::move (next));
145155 }
146156
@@ -218,6 +228,35 @@ shortest_paths_core(const StrictMultiDiGraph& g, NodeId src,
218228 return {std::move (dist), std::move (dag)};
219229 }
220230
231+ std::vector<int > seen (static_cast <std::size_t >(N), 0 );
232+ int seen_token = 0 ;
233+ auto parent_reaches_child_via_pred_links = [&](NodeId parent, NodeId child) {
234+ if (parent == child) return true ;
235+ ++seen_token;
236+ if (seen_token == 0 ) {
237+ std::fill (seen.begin (), seen.end (), 0 );
238+ seen_token = 1 ;
239+ }
240+ std::vector<NodeId> st;
241+ st.reserve (16 );
242+ st.push_back (parent);
243+ seen[static_cast <std::size_t >(parent)] = seen_token;
244+ while (!st.empty ()) {
245+ NodeId cur = st.back ();
246+ st.pop_back ();
247+ if (cur == child) return true ;
248+ for (const auto & pr : pred_lists[static_cast <std::size_t >(cur)]) {
249+ NodeId p = pr.first ;
250+ if (p < 0 || p >= N) continue ;
251+ auto p_idx = static_cast <std::size_t >(p);
252+ if (seen[p_idx] == seen_token) continue ;
253+ seen[p_idx] = seen_token;
254+ st.push_back (p);
255+ }
256+ }
257+ return false ;
258+ };
259+
221260 // Priority queue for Dijkstra with capacity-aware node-level tie-breaking.
222261 // QItem is (cost, -residual, node). Negated residual ensures higher capacity = higher priority.
223262 // Lexicographic ordering: cost (minimize) -> residual (maximize) -> node (deterministic).
@@ -344,7 +383,11 @@ shortest_paths_core(const StrictMultiDiGraph& g, NodeId src,
344383 }
345384 // Multipath: found equal-cost alternative path to v.
346385 else if (multipath && new_cost == dist[v_idx]) {
347- pred_lists[v_idx].push_back ({u, std::move (selected_edges)});
386+ // Guard against zero-cost predecessor cycles by rejecting additions
387+ // that would introduce a cycle in predecessor relations.
388+ if (!parent_reaches_child_via_pred_links (u, v)) {
389+ pred_lists[v_idx].push_back ({u, std::move (selected_edges)});
390+ }
348391 // Note: In multipath mode, we don't update min_residual_to_node because
349392 // we're collecting all equal-cost paths, not choosing based on residual.
350393 }
0 commit comments