Skip to content

Update NC k shortest simple path for Undirected graphs #40547

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Aug 16, 2025
Merged
51 changes: 30 additions & 21 deletions src/sage/graphs/path_enumeration.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -471,13 +471,6 @@ def shortest_simple_paths(self, source, target, weight_function=None,
('101', '011', 1),
('011', '111', 1)])]

Feng's algorithm cannot be used on undirected graphs::

sage: list(graphs.PathGraph(2).shortest_simple_paths(0, 1, algorithm='Feng'))
Traceback (most recent call last):
...
ValueError: Feng's algorithm works only for directed graphs

If the algorithm is not implemented::

sage: list(g.shortest_simple_paths(1, 5, algorithm='tip top'))
Expand Down Expand Up @@ -528,7 +521,7 @@ def shortest_simple_paths(self, source, target, weight_function=None,
sage: s == t
True

Check that "Yen" and "Feng" provide same results on random digraphs::
Check that "Yen", "Feng" and "PNC" provide same results on random digraphs::

sage: G = digraphs.RandomDirectedGNP(30, .05)
sage: while not G.is_strongly_connected():
Expand All @@ -546,6 +539,24 @@ def shortest_simple_paths(self, source, target, weight_function=None,
....: raise ValueError(f"something goes wrong u={u}, v={v}, G={G.edges()}!")
....: if i == 100:
....: break

Check that "Yen", "Feng" and "PNC" provide same results on random undirected graphs::

sage: from sage.graphs.generators.random import RandomGNP
sage: G = RandomGNP(30, .05)
Copy link
Contributor

Choose a reason for hiding this comment

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

you could simply use G = graphs.RandomGNP(30, .05).

Also, consider increasing the number of edges. With p=0.05 the graph has roughly 20 edges.

sage: for u, v in list(G.edges(labels=False, sort=False)):
....: G.set_edge_label(u, v, randint(1, 10))
sage: V = G.vertices(sort=False)
sage: shuffle(V)
sage: u, v = V[:2]
sage: it_Y = G.shortest_simple_paths(u, v, by_weight=True, report_weight=True, algorithm='Yen')
sage: it_F = G.shortest_simple_paths(u, v, by_weight=True, report_weight=True, algorithm='Feng')
sage: it_P = G.shortest_simple_paths(u, v, by_weight=True, report_weight=True, algorithm='PNC')
sage: for i, (y, f, p) in enumerate(zip(it_Y, it_F, it_P)):
....: if y[0] != f[0] or y[0] != p[0]:
....: raise ValueError(f"something goes wrong u={u}, v={v}, G={G.edges()}!")
....: if i == 100:
....: break
"""
if source not in self:
raise ValueError("vertex '{}' is not in the graph".format(source))
Expand All @@ -569,9 +580,6 @@ def shortest_simple_paths(self, source, target, weight_function=None,
algorithm = "Feng" if self.is_directed() else "Yen"

if algorithm in ("Feng", "PNC"):
if not self.is_directed():
raise ValueError(f"{algorithm}'s algorithm works only for directed graphs")

yield from nc_k_shortest_simple_paths(self, source=source, target=target,
weight_function=weight_function,
by_weight=by_weight, check_weight=check_weight,
Expand Down Expand Up @@ -897,8 +905,6 @@ def nc_k_shortest_simple_paths(self, source, target, weight_function=None,
Return an iterator over the simple paths between a pair of vertices in
increasing order of weights.

Works only for directed graphs.

For unweighted graphs, paths are returned in order of increasing number
of edges.

Expand Down Expand Up @@ -1001,6 +1007,14 @@ def nc_k_shortest_simple_paths(self, source, target, weight_function=None,
(40.0, [(1, 2, 20), (2, 5, 20)]),
(60.0, [(1, 4, 30), (4, 5, 30)])]

Algorithm works for undirected graphs as well::

sage: g = Graph([(1, 2, 20), (1, 3, 10), (1, 4, 30), (2, 5, 20), (3, 5, 10), (4, 5, 30)])
sage: list(nc_k_shortest_simple_paths(g, 5, 1, by_weight=True))
[[5, 3, 1], [5, 2, 1], [5, 4, 1]]
sage: list(nc_k_shortest_simple_paths(g, 5, 1))
[[5, 2, 1], [5, 4, 1], [5, 3, 1]]
Copy link
Contributor

Choose a reason for hiding this comment

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

The order in which the paths are reported may not always be the same. To make the test robust, you could report the length of the paths.

sage: [len(P) for P in nc_k_shortest_simple_paths(g, 5, 1)]
[3, 3, 3]

the alternative is to use a lexicographic sorting of the paths

sage: sorted(nc_k_shortest_simple_paths(g, 5, 1))
[[5, 2, 1], [5, 3, 1], [5, 4, 1]]


TESTS::

sage: from sage.graphs.path_enumeration import nc_k_shortest_simple_paths
Expand Down Expand Up @@ -1150,9 +1164,6 @@ def nc_k_shortest_simple_paths(self, source, target, weight_function=None,
sage: for i in range(len(A) - 1):
....: assert A[i] <= A[i + 1]
"""
if not self.is_directed():
raise ValueError("this algorithm works only for directed graphs")

if source not in self:
raise ValueError("vertex '{}' is not in the graph".format(source))
if target not in self:
Expand All @@ -1163,9 +1174,11 @@ def nc_k_shortest_simple_paths(self, source, target, weight_function=None,
return

if self.has_loops() or self.allows_multiple_edges():
G = self.to_simple(to_undirected=False, keep_label='min', immutable=False)
G = self.to_simple(to_undirected=self.is_directed(), keep_label='min', immutable=False)
Copy link
Contributor

Choose a reason for hiding this comment

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

Here you force conversion to undirected graph. This seems wrong.

else:
G = self.copy(immutable=False)
if not G.is_directed():
Copy link
Contributor

Choose a reason for hiding this comment

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

we can sometimes save a copy

if self.has_loops() or self.allows_multiple_edges():
    G = self.to_simple(to_undirected=False, keep_label='min', immutable=False)
    if not G.is_directed():
        G = G.to_directed()
elif not self.is_directed():
    # Turn the graph into a mutable directed graph
    G = self.to_directed(data_structure='sparse')
else:
    G = self.copy(immutable=False)

G = G.to_directed()

G.delete_edges(G.incoming_edges(source, labels=False))
G.delete_edges(G.outgoing_edges(target, labels=False))
Expand Down Expand Up @@ -1416,8 +1429,6 @@ def feng_k_shortest_simple_paths(self, source, target, weight_function=None,
Return an iterator over the simple paths between a pair of vertices in
increasing order of weights.

Works only for directed graphs.

For unweighted graphs, paths are returned in order of increasing number
of edges.

Expand Down Expand Up @@ -1487,8 +1498,6 @@ def pnc_k_shortest_simple_paths(self, source, target, weight_function=None,
Return an iterator over the simple paths between a pair of vertices in
increasing order of weights.

Works only for directed graphs.

In case of weighted graphs, negative weights are not allowed.

If ``source`` is the same vertex as ``target``, then ``[[source]]`` is
Expand Down
Loading