Skip to content

Commit 98c2f57

Browse files
author
Release Manager
committed
sagemathgh-40364: Improve PNC algorithm <!-- ^ 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 sagemath#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 sagemath#12345". --> Improve the implementation of PNC algorithm. See comments in sagemath#40284. ### 📝 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, --> <!-- - sagemath#12345: short description why this is a dependency --> <!-- - sagemath#34567: ... --> sagemath#40284 URL: sagemath#40364 Reported by: Yuta Inoue Reviewer(s): David Coudert
2 parents a48946e + 3f1ddc6 commit 98c2f57

File tree

2 files changed

+235
-49
lines changed

2 files changed

+235
-49
lines changed

src/sage/graphs/base/c_graph.pyx

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ from sage.rings.integer cimport smallInteger
5656

5757
from sage.rings.integer_ring import ZZ
5858

59+
from collections.abc import Iterable
5960

6061
cdef extern from "Python.h":
6162
int unlikely(int) nogil # Defined by Cython
@@ -3628,6 +3629,134 @@ cdef class CGraphBackend(GenericGraphBackend):
36283629
return Infinity
36293630
return []
36303631

3632+
def shortest_path_to_set(self, source, targets, by_weight=False, edge_weight=None,
3633+
exclude_vertices=None, report_weight=False,):
3634+
r"""
3635+
Return the shortest path from ``source`` to any vertex in ``targets``.
3636+
3637+
INPUT:
3638+
3639+
- ``source`` -- the starting vertex.
3640+
3641+
- ``targets`` -- iterable container; the set of end vertices.
3642+
3643+
- ``edge_weight`` -- dictionary (default: ``None``); a dictionary
3644+
that takes as input an edge ``(u, v)`` and outputs its weight.
3645+
If not ``None``, ``by_weight`` is automatically set to ``True``.
3646+
If ``None`` and ``by_weight`` is ``True``, we use the edge
3647+
label ``l`` as a weight.
3648+
3649+
- ``by_weight`` -- boolean (default: ``False``); if ``True``, the edges
3650+
in the graph are weighted, otherwise all edges have weight 1.
3651+
3652+
- ``exclude_vertices`` -- iterable container (default: ``None``);
3653+
iterable of vertices to exclude from the graph while calculating the
3654+
shortest path from ``source`` to any vertex in ``targets``.
3655+
3656+
- ``report_weight`` -- boolean (default: ``False``); if ``False``, just
3657+
a path is returned. Otherwise a tuple of path length and path is
3658+
returned.
3659+
3660+
OUTPUT:
3661+
3662+
- A list of vertices in the shortest path from ``source`` to any vertex
3663+
in ``targets`` or a tuple of path lengh and path is returned
3664+
depending upon the value of parameter ``report_weight``.
3665+
3666+
EXAMPLES::
3667+
3668+
sage: g = Graph([(1, 2, 10), (1, 3, 20), (1, 4, 30)])
3669+
sage: g._backend.shortest_path_to_set(1, {3, 4}, by_weight=True)
3670+
[1, 3]
3671+
sage: g = Graph([(1, 2, 10), (2, 3, 10), (1, 4, 20), (4, 5, 20), (1, 6, 30), (6, 7, 30)])
3672+
sage: g._backend.shortest_path_to_set(1, {5, 7}, by_weight=True, exclude_vertices=[4], report_weight=True)
3673+
(60.0, [1, 6, 7])
3674+
3675+
TESTS::
3676+
3677+
sage: g = Graph([(1, 2, 10), (1, 3, 20), (1, 4, 30)])
3678+
sage: g._backend.shortest_path_to_set(1, {3, 4}, exclude_vertices=[3], by_weight=True)
3679+
[1, 4]
3680+
sage: g._backend.shortest_path_to_set(1, {1, 3, 4}, by_weight=True)
3681+
[1]
3682+
3683+
``source`` must not be in ``exclude_vertices``::
3684+
3685+
sage: g._backend.shortest_path_to_set(1, {3, 4}, exclude_vertices=[1])
3686+
Traceback (most recent call last):
3687+
...
3688+
ValueError: source must not be in exclude_vertices.
3689+
3690+
When no path exists from ``source`` to ``targets``, raise an error.
3691+
3692+
sage: g._backend.shortest_path_to_set(1, {3, 4}, exclude_vertices=[3, 4])
3693+
Traceback (most recent call last):
3694+
...
3695+
ValueError: no path found from source to targets.
3696+
3697+
``exclude_vertices`` must be iterable::
3698+
3699+
sage: g._backend.shortest_path_to_set(1, {1, 3, 4}, exclude_vertices=100)
3700+
Traceback (most recent call last):
3701+
...
3702+
TypeError: exclude_vertices (100) are not iterable.
3703+
"""
3704+
if not exclude_vertices:
3705+
exclude_vertices = set()
3706+
elif not isinstance(exclude_vertices, Iterable):
3707+
raise TypeError(f"exclude_vertices ({exclude_vertices}) are not iterable.")
3708+
elif not isinstance(exclude_vertices, set):
3709+
exclude_vertices = set(exclude_vertices)
3710+
if source in exclude_vertices:
3711+
raise ValueError(f"source must not be in exclude_vertices.")
3712+
cdef PairingHeap[int, double] pq = PairingHeap[int, double]()
3713+
cdef dict dist = {}
3714+
cdef dict pred = {}
3715+
cdef int x_int = self.get_vertex(source)
3716+
pq.push(x_int, 0)
3717+
dist[x_int] = 0
3718+
3719+
while not pq.empty():
3720+
v_int, d = pq.top()
3721+
pq.pop()
3722+
v = self.vertex_label(v_int)
3723+
3724+
if v in targets:
3725+
# found a vertex in targets
3726+
path = []
3727+
while v_int in pred:
3728+
path.append(self.vertex_label(v_int))
3729+
v_int = pred[v_int]
3730+
path.append(source)
3731+
path.reverse()
3732+
return (d, path) if report_weight else path
3733+
3734+
if d > dist.get(v_int, float('inf')):
3735+
continue # already found a better path
3736+
3737+
for _, u, label in self.iterator_out_edges([v], labels=True):
3738+
if u in exclude_vertices:
3739+
continue
3740+
if edge_weight:
3741+
e_weight = edge_weight[(v, u)]
3742+
elif by_weight:
3743+
e_weight = label
3744+
else:
3745+
e_weight = 1
3746+
new_dist = d + e_weight
3747+
u_int = self.get_vertex(u)
3748+
if new_dist < dist.get(u_int, float('inf')):
3749+
dist[u_int] = new_dist
3750+
pred[u_int] = v_int
3751+
if pq.contains(u_int):
3752+
if pq.value(u_int) > new_dist:
3753+
pq.decrease(u_int, new_dist)
3754+
else:
3755+
pq.push(u_int, new_dist)
3756+
3757+
# no path found
3758+
raise ValueError(f"no path found from source to targets.")
3759+
36313760
def bidirectional_dijkstra_special(self, x, y, weight_function=None,
36323761
exclude_vertices=None, exclude_edges=None,
36333762
include_vertices=None, distance_flag=False,

src/sage/graphs/path_enumeration.pyx

Lines changed: 106 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,11 @@ from itertools import product
3636
from sage.misc.misc_c import prod
3737
from libcpp.queue cimport priority_queue
3838
from libcpp.pair cimport pair
39+
from libcpp.vector cimport vector
40+
41+
from sage.data_structures.pairing_heap cimport PairingHeap
3942
from sage.rings.integer_ring import ZZ
43+
4044
import copy
4145

4246

@@ -1515,49 +1519,56 @@ def pnc_k_shortest_simple_paths(self, source, target, weight_function=None,
15151519
G.delete_edges(G.incoming_edges(source, labels=False))
15161520
G.delete_edges(G.outgoing_edges(target, labels=False))
15171521

1522+
# relabel the graph so that vertices are named with integers
1523+
cdef list int_to_vertex = list(G)
1524+
cdef dict vertex_to_int = {u: i for i, u in enumerate(int_to_vertex)}
1525+
G.relabel(perm=vertex_to_int, inplace=True)
1526+
cdef int id_source = vertex_to_int[source]
1527+
cdef int id_target = vertex_to_int[target]
1528+
1529+
def relabeled_weight_function(e, wf=weight_function):
1530+
return wf((int_to_vertex[e[0]], int_to_vertex[e[1]], e[2]))
1531+
15181532
by_weight, weight_function = G._get_weight_function(by_weight=by_weight,
1519-
weight_function=weight_function,
1533+
weight_function=(relabeled_weight_function if weight_function else None),
15201534
check_weight=check_weight)
15211535

15221536
def reverse_weight_function(e):
15231537
return weight_function((e[1], e[0], e[2]))
15241538

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()}
1539+
cdef dict original_edge_labels = {(u, v): (int_to_vertex[u], int_to_vertex[v], label)
1540+
for u, v, label in G.edge_iterator()}
1541+
cdef dict original_edges = {(u, v): (int_to_vertex[u], int_to_vertex[v])
1542+
for u, v in G.edge_iterator(labels=False)}
1543+
cdef dict edge_wt = {(e[0], e[1]): weight_function(e) for e in G.edge_iterator()}
15301544

15311545
# The first shortest path tree T_0
15321546
from sage.graphs.base.boost_graph import shortest_paths
15331547
cdef dict dist
15341548
cdef dict successor
15351549
reverse_graph = G.reverse()
1536-
dist, successor = shortest_paths(reverse_graph, target, weight_function=reverse_weight_function,
1550+
dist, successor = shortest_paths(reverse_graph, id_target, weight_function=reverse_weight_function,
15371551
algorithm='Dijkstra_Boost')
15381552
cdef set unnecessary_vertices = set(G) - set(dist) # no path to target
1539-
if source in unnecessary_vertices: # no path from source to target
1553+
if id_source in unnecessary_vertices: # no path from source to target
15401554
return
1555+
G.delete_vertices(unnecessary_vertices)
15411556

15421557
# sidetrack cost
15431558
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:]))
1559+
for e in G.edge_iterator() if e[0] in dist and e[1] in dist}
15491560

15501561
# v-t path in the first shortest path tree T_0
15511562
def tree_path(v):
15521563
path = [v]
1553-
while v != target:
1564+
while v != id_target:
15541565
v = successor[v]
15551566
path.append(v)
15561567
return path
15571568

15581569
# shortest path
1559-
shortest_path = tree_path(source)
1560-
cdef double shortest_path_length = dist[source]
1570+
shortest_path = tree_path(id_source)
1571+
cdef double shortest_path_length = dist[id_source]
15611572

15621573
# idx of paths
15631574
cdef dict idx_to_path = {0: shortest_path}
@@ -1568,9 +1579,66 @@ def pnc_k_shortest_simple_paths(self, source, target, weight_function=None,
15681579
# (i.e. real length = cost + shortest_path_length in T_0)
15691580
cdef priority_queue[pair[pair[double, bint], pair[int, int]]] candidate_paths
15701581

1571-
# shortest path function for weighted/unweighted graph using reduced weights
1572-
shortest_path_func = G._backend.bidirectional_dijkstra_special
1582+
# ancestor_idx_vec[v] := the first vertex of ``path[:t+1]`` or ``id_target`` reachable by
1583+
# edges of first shortest path tree from v.
1584+
cdef vector[int] ancestor_idx_vec = [-1 for _ in range(len(G))]
1585+
1586+
def ancestor_idx_func(v, t, target_idx):
1587+
if ancestor_idx_vec[v] != -1:
1588+
if ancestor_idx_vec[v] <= t or ancestor_idx_vec[v] == target_idx:
1589+
return ancestor_idx_vec[v]
1590+
ancestor_idx_vec[v] = ancestor_idx_func(successor[v], t, target_idx)
1591+
return ancestor_idx_vec[v]
1592+
1593+
# used inside shortest_path_to_green
1594+
cdef PairingHeap[int, double] pq = PairingHeap[int, double]()
1595+
cdef dict dist_in_func = {}
1596+
cdef dict pred = {}
1597+
1598+
# calculate shortest path from dev to one of green vertices
1599+
def shortest_path_to_green(dev, exclude_vertices):
1600+
t = len(exclude_vertices)
1601+
ancestor_idx_vec[id_target] = t + 1
1602+
# clear
1603+
while not pq.empty():
1604+
pq.pop()
1605+
dist_in_func.clear()
1606+
pred.clear()
1607+
1608+
pq.push(dev, 0)
1609+
dist_in_func[dev] = 0
1610+
1611+
while not pq.empty():
1612+
v, d = pq.top()
1613+
pq.pop()
1614+
1615+
if ancestor_idx_func(v, t, t + 1) == t + 1: # green
1616+
path = []
1617+
while v in pred:
1618+
path.append(v)
1619+
v = pred[v]
1620+
path.append(dev)
1621+
path.reverse()
1622+
return (d, path)
1623+
1624+
if d > dist_in_func.get(v, float('inf')):
1625+
continue # already found a better path
1626+
1627+
for u in G.neighbor_out_iterator(v):
1628+
if u in exclude_vertices:
1629+
continue
1630+
new_dist = d + sidetrack_cost[(v, u)]
1631+
if new_dist < dist_in_func.get(u, float('inf')):
1632+
dist_in_func[u] = new_dist
1633+
pred[u] = v
1634+
if pq.contains(u):
1635+
if pq.value(u) > new_dist:
1636+
pq.decrease(u, new_dist)
1637+
else:
1638+
pq.push(u, new_dist)
1639+
return
15731640

1641+
cdef int i, deviation_i
15741642
candidate_paths.push(((0, True), (0, 0)))
15751643
while candidate_paths.size():
15761644
(negative_cost, is_simple), (path_idx, dev_idx) = candidate_paths.top()
@@ -1580,65 +1648,54 @@ def pnc_k_shortest_simple_paths(self, source, target, weight_function=None,
15801648
path = idx_to_path[path_idx]
15811649
del idx_to_path[path_idx]
15821650

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]
1651+
for i in range(ancestor_idx_vec.size()):
1652+
ancestor_idx_vec[i] = -1
1653+
for i, v in enumerate(path):
1654+
ancestor_idx_vec[v] = i
15971655

15981656
if is_simple:
15991657
# output
16001658
if report_edges and labels:
1601-
P = [edge_labels[e] for e in zip(path, path[1:])]
1659+
P = [original_edge_labels[e] for e in zip(path, path[1:])]
16021660
elif report_edges:
1603-
P = list(zip(path, path[1:]))
1661+
P = [original_edges[e] for e in zip(path, path[1:])]
16041662
else:
1605-
P = path
1663+
P = [int_to_vertex[v] for v in path]
16061664
if report_weight:
16071665
yield (shortest_path_length + cost, P)
16081666
else:
16091667
yield P
16101668

16111669
# GET DEVIATION PATHS
16121670
original_cost = cost
1613-
for deviation_i in range(len(path) - 1, dev_idx - 1, -1):
1671+
former_part = set(path)
1672+
for deviation_i in range(len(path) - 2, dev_idx - 1, -1):
16141673
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:
1674+
if e[1] in former_part: # e[1] is red or e in path
16191675
continue
1620-
new_path = path[:deviation_i + 1] + tree_path(e[1])
1676+
ancestor_idx = ancestor_idx_func(e[1], deviation_i, len(path) - 1)
1677+
new_is_simple = ancestor_idx > deviation_i
1678+
# no need to compute tree_path if new_is_simple is False
1679+
new_path = path[:deviation_i + 1] + (tree_path(e[1]) if new_is_simple else [e[1]])
16211680
new_path_idx = idx
16221681
idx_to_path[new_path_idx] = new_path
16231682
idx += 1
16241683
new_cost = original_cost + sidetrack_cost[(e[0], e[1])]
1625-
new_is_simple = ancestor_idx > deviation_i
16261684
candidate_paths.push(((-new_cost, new_is_simple), (new_path_idx, deviation_i + 1)))
16271685
if deviation_i == dev_idx:
16281686
continue
16291687
original_cost -= sidetrack_cost[(path[deviation_i - 1], path[deviation_i])]
1688+
former_part.remove(path[deviation_i + 1])
16301689
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:
1690+
deviations = shortest_path_to_green(path[dev_idx], set(path[:dev_idx]))
1691+
if not deviations:
16361692
continue # no path to target in G \ path[:dev_idx]
1637-
new_path = path[:dev_idx] + deviation
1693+
deviation_weight, deviation = deviations
1694+
new_path = path[:dev_idx] + deviation[:-1] + tree_path(deviation[-1])
16381695
new_path_idx = idx
16391696
idx_to_path[new_path_idx] = new_path
16401697
idx += 1
1641-
new_cost = sidetrack_length(new_path)
1698+
new_cost = cost + deviation_weight
16421699
candidate_paths.push(((-new_cost, True), (new_path_idx, dev_idx)))
16431700

16441701

0 commit comments

Comments
 (0)