Skip to content

Commit 1a1d0c4

Browse files
author
Release Manager
committed
gh-40284: PNC k shortest simple path (for directed graphs) <!-- ^ Please provide a concise and informative title. --> <!-- ^ Don't put issue numbers in the title, do this in the PR description below. --> <!-- ^ For example, instead of "Fixes #12345" use "Introduce new method to calculate 1 + 2". --> <!-- v Describe your changes below in detail. --> <!-- v Why is this change required? What problem does it solve? --> <!-- v If this PR resolves an open issue, please link to it here. For example, "Fixes #12345". --> Implement PNC k shortest simple path for directed graphs. I implemented for only directed graphs for simplicity first. ### 📝 Checklist <!-- Put an `x` in all the boxes that apply. --> - [x] The title is concise and informative. - [x] The description explains in detail what this PR is about. - [x] I have linked a relevant issue or discussion. - [x] I have created tests covering the changes. - [x] I have updated the documentation and checked the documentation preview. ### ⌛ Dependencies <!-- List all open PRs that this PR logically depends on. For example, --> <!-- - #12345: short description why this is a dependency --> <!-- - #34567: ... --> #40248 #40217 URL: #40284 Reported by: Yuta Inoue Reviewer(s): David Coudert
2 parents 49f37a7 + 211ae0c commit 1a1d0c4

File tree

2 files changed

+263
-0
lines changed

2 files changed

+263
-0
lines changed

src/doc/en/reference/references/index.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,10 @@ REFERENCES:
108108
*Coxeter submodular functions and deformations of Coxeter permutahedra*,
109109
Advances in Mathematics, Volume 365, 13 May 2020.
110110
111+
.. [ACN2023] Ali Al Zoobi, David Coudert, Nicolas Nisse
112+
Finding the k Shortest Simple Paths: Time and Space trade-offs
113+
ACM Journal of Experimental Algorithmics, 2023, 28, pp.23 :doi:`10.1145/3626567`.
114+
111115
.. [ALL2002] P. Auger, G. Labelle and P. Leroux, *Combinatorial
112116
addition formulas and applications*, Advances in Applied
113117
Mathematics 28 (2002) 302-342.

src/sage/graphs/path_enumeration.pyx

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ This module is meant for all functions related to path enumeration in graphs.
1313
:func:`all_paths` | Return the list of all paths between a pair of vertices.
1414
:func:`yen_k_shortest_simple_paths` | Return an iterator over the simple paths between a pair of vertices in increasing order of weights.
1515
:func:`feng_k_shortest_simple_paths` | Return an iterator over the simple paths between a pair of vertices in increasing order of weights.
16+
:func:`pnc_k_shortest_simple_paths` | Return an iterator over the simple paths between a pair of vertices in increasing order of weights.
1617
:func:`all_paths_iterator` | Return an iterator over the paths of ``self``.
1718
:func:`all_simple_paths` | Return a list of all the simple paths of ``self`` starting with one of the given vertices.
1819
:func:`shortest_simple_paths` | Return an iterator over the simple paths between a pair of vertices.
@@ -1383,6 +1384,264 @@ def feng_k_shortest_simple_paths(self, source, target, weight_function=None,
13831384
reduced_cost[e[0], e[1]] = temp_dict[e[0], e[1]]
13841385

13851386

1387+
def pnc_k_shortest_simple_paths(self, source, target, weight_function=None,
1388+
by_weight=False, check_weight=True,
1389+
report_edges=False,
1390+
labels=False, report_weight=False):
1391+
r"""
1392+
Return an iterator over the simple paths between a pair of vertices in
1393+
increasing order of weights.
1394+
1395+
Works only for directed graphs.
1396+
1397+
In case of weighted graphs, negative weights are not allowed.
1398+
1399+
If ``source`` is the same vertex as ``target``, then ``[[source]]`` is
1400+
returned -- a list containing the 1-vertex, 0-edge path ``source``.
1401+
1402+
The loops and the multiedges if present in the given graph are ignored and
1403+
only minimum of the edge labels is kept in case of multiedges.
1404+
1405+
INPUT:
1406+
1407+
- ``source`` -- a vertex of the graph, where to start
1408+
1409+
- ``target`` -- a vertex of the graph, where to end
1410+
1411+
- ``weight_function`` -- function (default: ``None``); a function that
1412+
takes as input an edge ``(u, v, l)`` and outputs its weight. If not
1413+
``None``, ``by_weight`` is automatically set to ``True``. If ``None``
1414+
and ``by_weight`` is ``True``, we use the edge label ``l`` as a
1415+
weight.
1416+
1417+
- ``by_weight`` -- boolean (default: ``False``); if ``True``, the edges
1418+
in the graph are weighted, otherwise all edges have weight 1
1419+
1420+
- ``check_weight`` -- boolean (default: ``True``); whether to check that
1421+
the ``weight_function`` outputs a number for each edge
1422+
1423+
- ``report_edges`` -- boolean (default: ``False``); whether to report
1424+
paths as list of vertices (default) or list of edges, if ``False``
1425+
then ``labels`` parameter is ignored
1426+
1427+
- ``labels`` -- boolean (default: ``False``); if ``False``, each edge
1428+
is simply a pair ``(u, v)`` of vertices. Otherwise a list of edges
1429+
along with its edge labels are used to represent the path.
1430+
1431+
- ``report_weight`` -- boolean (default: ``False``); if ``False``, just
1432+
a path is returned. Otherwise a tuple of path length and path is
1433+
returned.
1434+
1435+
ALGORITHM:
1436+
1437+
This algorithm is based on the ``feng_k_shortest_simple_paths`` algorithm
1438+
in [Feng2014]_, but postpones the shortest path tree computation when non-simple
1439+
deviations occur. See Postponed Node Classification algorithm in [ACN2023]_
1440+
for the algorithm description.
1441+
1442+
EXAMPLES::
1443+
1444+
sage: from sage.graphs.path_enumeration import pnc_k_shortest_simple_paths
1445+
sage: g = DiGraph([(1, 2, 20), (1, 3, 10), (1, 4, 30), (2, 5, 20), (3, 5, 10), (4, 5, 30)])
1446+
sage: list(pnc_k_shortest_simple_paths(g, 1, 5, by_weight=True, report_weight=True))
1447+
[(20.0, [1, 3, 5]), (40.0, [1, 2, 5]), (60.0, [1, 4, 5])]
1448+
sage: list(pnc_k_shortest_simple_paths(g, 1, 5, report_weight=True))
1449+
[(2.0, [1, 2, 5]), (2.0, [1, 4, 5]), (2.0, [1, 3, 5])]
1450+
1451+
TESTS::
1452+
1453+
sage: from sage.graphs.path_enumeration import pnc_k_shortest_simple_paths
1454+
sage: g = DiGraph([(0, 1, 9), (0, 3, 1), (0, 4, 2), (1, 6, 4),
1455+
....: (1, 7, 1), (2, 0, 5), (2, 1, 4), (2, 7, 1),
1456+
....: (3, 1, 7), (3, 2, 4), (3, 4, 2), (4, 0, 8),
1457+
....: (4, 1, 10), (4, 3, 3), (4, 7, 10), (5, 2, 5),
1458+
....: (5, 4, 9), (6, 2, 9)], weighted=True)
1459+
sage: list(pnc_k_shortest_simple_paths(g, 5, 1, by_weight=True, report_weight=True,
1460+
....: labels=True, report_edges=True))
1461+
[(9.0, [(5, 2, 5), (2, 1, 4)]),
1462+
(18.0, [(5, 2, 5), (2, 0, 5), (0, 3, 1), (3, 1, 7)]),
1463+
(19.0, [(5, 2, 5), (2, 0, 5), (0, 1, 9)]),
1464+
(19.0, [(5, 4, 9), (4, 1, 10)]),
1465+
(19.0, [(5, 4, 9), (4, 3, 3), (3, 1, 7)]),
1466+
(20.0, [(5, 4, 9), (4, 3, 3), (3, 2, 4), (2, 1, 4)]),
1467+
(22.0, [(5, 2, 5), (2, 0, 5), (0, 4, 2), (4, 1, 10)]),
1468+
(22.0, [(5, 2, 5), (2, 0, 5), (0, 4, 2), (4, 3, 3), (3, 1, 7)]),
1469+
(23.0, [(5, 2, 5), (2, 0, 5), (0, 3, 1), (3, 4, 2), (4, 1, 10)]),
1470+
(25.0, [(5, 4, 9), (4, 0, 8), (0, 3, 1), (3, 1, 7)]),
1471+
(26.0, [(5, 4, 9), (4, 0, 8), (0, 1, 9)]),
1472+
(26.0, [(5, 4, 9), (4, 0, 8), (0, 3, 1), (3, 2, 4), (2, 1, 4)]),
1473+
(30.0, [(5, 4, 9), (4, 3, 3), (3, 2, 4), (2, 0, 5), (0, 1, 9)])]
1474+
sage: g = DiGraph(graphs.Grid2dGraph(2, 6).relabel(inplace=False))
1475+
sage: for u, v in g.edge_iterator(labels=False):
1476+
....: g.set_edge_label(u, v, 1)
1477+
sage: [w for w, P in pnc_k_shortest_simple_paths(g, 5, 1, by_weight=True, report_weight=True)]
1478+
[4.0, 6.0, 6.0, 6.0, 6.0, 6.0, 6.0, 6.0, 6.0, 6.0, 6.0, 8.0, 8.0,
1479+
8.0, 8.0, 8.0, 8.0, 8.0, 8.0, 8.0, 10.0, 10.0, 10.0, 10.0]
1480+
1481+
Same tests as ``yen_k_shortest_simple_paths``::
1482+
1483+
sage: g = DiGraph([(1, 2, 1), (2, 3, 1), (3, 4, 1), (4, 5, 1),
1484+
....: (1, 7, 1), (7, 8, 1), (8, 5, 1), (1, 6, 1),
1485+
....: (6, 9, 1), (9, 5, 1), (4, 2, 1), (9, 3, 1),
1486+
....: (9, 10, 1), (10, 5, 1), (9, 11, 1), (11, 10, 1)])
1487+
sage: [w for w, P in pnc_k_shortest_simple_paths(g, 1, 5, by_weight=True, report_weight=True)]
1488+
[3.0, 3.0, 4.0, 4.0, 5.0, 5.0]
1489+
1490+
More tests::
1491+
1492+
sage: D = graphs.Grid2dGraph(5, 5).relabel(inplace=False).to_directed()
1493+
sage: A = [w for w, P in pnc_k_shortest_simple_paths(D, 0, 24, report_weight=True)]
1494+
sage: assert len(A) == 8512
1495+
sage: for i in range(len(A) - 1):
1496+
....: assert A[i] <= A[i + 1]
1497+
"""
1498+
if not self.is_directed():
1499+
raise ValueError("this algorithm works only for directed graphs")
1500+
1501+
if source not in self:
1502+
raise ValueError("vertex '{}' is not in the graph".format(source))
1503+
if target not in self:
1504+
raise ValueError("vertex '{}' is not in the graph".format(target))
1505+
if source == target:
1506+
P = [] if report_edges else [source]
1507+
yield (0, P) if report_weight else P
1508+
return
1509+
1510+
if self.has_loops() or self.allows_multiple_edges():
1511+
G = self.to_simple(to_undirected=False, keep_label='min', immutable=False)
1512+
else:
1513+
G = self.copy(immutable=False)
1514+
1515+
G.delete_edges(G.incoming_edges(source, labels=False))
1516+
G.delete_edges(G.outgoing_edges(target, labels=False))
1517+
1518+
by_weight, weight_function = G._get_weight_function(by_weight=by_weight,
1519+
weight_function=weight_function,
1520+
check_weight=check_weight)
1521+
1522+
def reverse_weight_function(e):
1523+
return weight_function((e[1], e[0], e[2]))
1524+
1525+
cdef dict edge_labels
1526+
edge_labels = {(e[0], e[1]): e for e in G.edge_iterator()}
1527+
1528+
cdef dict edge_wt
1529+
edge_wt = {(e[0], e[1]): weight_function(e) for e in G.edge_iterator()}
1530+
1531+
# The first shortest path tree T_0
1532+
from sage.graphs.base.boost_graph import shortest_paths
1533+
cdef dict dist
1534+
cdef dict successor
1535+
reverse_graph = G.reverse()
1536+
dist, successor = shortest_paths(reverse_graph, target, weight_function=reverse_weight_function,
1537+
algorithm='Dijkstra_Boost')
1538+
cdef set unnecessary_vertices = set(G) - set(dist) # no path to target
1539+
if source in unnecessary_vertices: # no path from source to target
1540+
return
1541+
1542+
# sidetrack cost
1543+
cdef dict sidetrack_cost = {(e[0], e[1]): weight_function(e) + dist[e[1]] - dist[e[0]]
1544+
for e in G.edge_iterator()
1545+
if e[0] in dist and e[1] in dist}
1546+
1547+
def sidetrack_length(path):
1548+
return sum(sidetrack_cost[e] for e in zip(path, path[1:]))
1549+
1550+
# v-t path in the first shortest path tree T_0
1551+
def tree_path(v):
1552+
path = [v]
1553+
while v != target:
1554+
v = successor[v]
1555+
path.append(v)
1556+
return path
1557+
1558+
# shortest path
1559+
shortest_path = tree_path(source)
1560+
cdef double shortest_path_length = dist[source]
1561+
1562+
# idx of paths
1563+
cdef dict idx_to_path = {0: shortest_path}
1564+
cdef int idx = 1
1565+
1566+
# candidate_paths collects (cost, path_idx, dev_idx, is_simple)
1567+
# + cost is sidetrack cost from the first shortest path tree T_0
1568+
# (i.e. real length = cost + shortest_path_length in T_0)
1569+
cdef priority_queue[pair[pair[double, bint], pair[int, int]]] candidate_paths
1570+
1571+
# shortest path function for weighted/unweighted graph using reduced weights
1572+
shortest_path_func = G._backend.bidirectional_dijkstra_special
1573+
1574+
candidate_paths.push(((0, True), (0, 0)))
1575+
while candidate_paths.size():
1576+
(negative_cost, is_simple), (path_idx, dev_idx) = candidate_paths.top()
1577+
cost = -negative_cost
1578+
candidate_paths.pop()
1579+
1580+
path = idx_to_path[path_idx]
1581+
del idx_to_path[path_idx]
1582+
1583+
# ancestor_idx_dict[v] := the first vertex of ``path[:t+1]`` or ``path[-1]`` reachable by
1584+
# edges of first shortest path tree from v when enumerating deviating edges
1585+
# from ``path[t]``.
1586+
ancestor_idx_dict = {v: i for i, v in enumerate(path)}
1587+
1588+
def ancestor_idx_func(v, t, len_path):
1589+
if v not in successor:
1590+
# target vertex is not reachable from v
1591+
return -1
1592+
if v in ancestor_idx_dict:
1593+
if ancestor_idx_dict[v] <= t or ancestor_idx_dict[v] == len_path - 1:
1594+
return ancestor_idx_dict[v]
1595+
ancestor_idx_dict[v] = ancestor_idx_func(successor[v], t, len_path)
1596+
return ancestor_idx_dict[v]
1597+
1598+
if is_simple:
1599+
# output
1600+
if report_edges and labels:
1601+
P = [edge_labels[e] for e in zip(path, path[1:])]
1602+
elif report_edges:
1603+
P = list(zip(path, path[1:]))
1604+
else:
1605+
P = path
1606+
if report_weight:
1607+
yield (shortest_path_length + cost, P)
1608+
else:
1609+
yield P
1610+
1611+
# GET DEVIATION PATHS
1612+
original_cost = cost
1613+
for deviation_i in range(len(path) - 1, dev_idx - 1, -1):
1614+
for e in G.outgoing_edge_iterator(path[deviation_i]):
1615+
if e[1] in path[:deviation_i + 2]: # e[1] is red or e in path
1616+
continue
1617+
ancestor_idx = ancestor_idx_func(e[1], deviation_i, len(path))
1618+
if ancestor_idx == -1:
1619+
continue
1620+
new_path = path[:deviation_i + 1] + tree_path(e[1])
1621+
new_path_idx = idx
1622+
idx_to_path[new_path_idx] = new_path
1623+
idx += 1
1624+
new_cost = original_cost + sidetrack_cost[(e[0], e[1])]
1625+
new_is_simple = ancestor_idx > deviation_i
1626+
candidate_paths.push(((-new_cost, new_is_simple), (new_path_idx, deviation_i + 1)))
1627+
if deviation_i == dev_idx:
1628+
continue
1629+
original_cost -= sidetrack_cost[(path[deviation_i - 1], path[deviation_i])]
1630+
else:
1631+
# get a path to target in G \ path[:dev_idx]
1632+
deviation = shortest_path_func(path[dev_idx], target,
1633+
exclude_vertices=unnecessary_vertices.union(path[:dev_idx]),
1634+
reduced_weight=sidetrack_cost)
1635+
if not deviation:
1636+
continue # no path to target in G \ path[:dev_idx]
1637+
new_path = path[:dev_idx] + deviation
1638+
new_path_idx = idx
1639+
idx_to_path[new_path_idx] = new_path
1640+
idx += 1
1641+
new_cost = sidetrack_length(new_path)
1642+
candidate_paths.push(((-new_cost, True), (new_path_idx, dev_idx)))
1643+
1644+
13861645
def _all_paths_iterator(self, vertex, ending_vertices=None,
13871646
simple=False, max_length=None, trivial=False,
13881647
use_multiedges=False, report_edges=False,

0 commit comments

Comments
 (0)