Skip to content

Commit 4f73d17

Browse files
Prevent cyclic predecessor DAG expansion
Co-authored-by: Andrey G <networmix@gmail.com>
1 parent 03ff3b6 commit 4f73d17

1 file changed

Lines changed: 44 additions & 1 deletion

File tree

src/shortest_paths.cpp

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)