From 07603768a784629a368ded35ff24864e19a47620 Mon Sep 17 00:00:00 2001 From: kappybar Date: Wed, 2 Jul 2025 22:31:42 +0900 Subject: [PATCH 01/14] improve pnc algorithm --- src/sage/graphs/path_enumeration.pyx | 36 +++++++++++++++------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/src/sage/graphs/path_enumeration.pyx b/src/sage/graphs/path_enumeration.pyx index a592d0c9b54..516f4e0c002 100644 --- a/src/sage/graphs/path_enumeration.pyx +++ b/src/sage/graphs/path_enumeration.pyx @@ -1538,6 +1538,7 @@ def pnc_k_shortest_simple_paths(self, source, target, weight_function=None, cdef set unnecessary_vertices = set(G) - set(dist) # no path to target if source in unnecessary_vertices: # no path from source to target return + G.delete_vertices(unnecessary_vertices) # sidetrack cost cdef dict sidetrack_cost = {(e[0], e[1]): weight_function(e) + dist[e[1]] - dist[e[0]] @@ -1571,6 +1572,17 @@ def pnc_k_shortest_simple_paths(self, source, target, weight_function=None, # shortest path function for weighted/unweighted graph using reduced weights shortest_path_func = G._backend.bidirectional_dijkstra_special + # See explanation of ancestor_idx_dict below + def ancestor_idx_func(v, t, len_path, ancestor_idx_dict): + if v not in successor: + # target vertex is not reachable from v + return -1 + if v in ancestor_idx_dict: + if ancestor_idx_dict[v] <= t or ancestor_idx_dict[v] == len_path - 1: + return ancestor_idx_dict[v] + ancestor_idx_dict[v] = ancestor_idx_func(successor[v], t, len_path, ancestor_idx_dict) + return ancestor_idx_dict[v] + candidate_paths.push(((0, True), (0, 0))) while candidate_paths.size(): (negative_cost, is_simple), (path_idx, dev_idx) = candidate_paths.top() @@ -1580,21 +1592,6 @@ def pnc_k_shortest_simple_paths(self, source, target, weight_function=None, path = idx_to_path[path_idx] del idx_to_path[path_idx] - # ancestor_idx_dict[v] := the first vertex of ``path[:t+1]`` or ``path[-1]`` reachable by - # edges of first shortest path tree from v when enumerating deviating edges - # from ``path[t]``. - ancestor_idx_dict = {v: i for i, v in enumerate(path)} - - def ancestor_idx_func(v, t, len_path): - if v not in successor: - # target vertex is not reachable from v - return -1 - if v in ancestor_idx_dict: - if ancestor_idx_dict[v] <= t or ancestor_idx_dict[v] == len_path - 1: - return ancestor_idx_dict[v] - ancestor_idx_dict[v] = ancestor_idx_func(successor[v], t, len_path) - return ancestor_idx_dict[v] - if is_simple: # output if report_edges and labels: @@ -1608,13 +1605,18 @@ def pnc_k_shortest_simple_paths(self, source, target, weight_function=None, else: yield P + # ancestor_idx_dict[v] := the first vertex of ``path[:t+1]`` or ``path[-1]`` reachable by + # edges of first shortest path tree from v when enumerating deviating edges + # from ``path[t]``. + ancestor_idx_dict = {v: i for i, v in enumerate(path)} + # GET DEVIATION PATHS original_cost = cost for deviation_i in range(len(path) - 1, dev_idx - 1, -1): for e in G.outgoing_edge_iterator(path[deviation_i]): if e[1] in path[:deviation_i + 2]: # e[1] is red or e in path continue - ancestor_idx = ancestor_idx_func(e[1], deviation_i, len(path)) + ancestor_idx = ancestor_idx_func(e[1], deviation_i, len(path), ancestor_idx_dict) if ancestor_idx == -1: continue new_path = path[:deviation_i + 1] + tree_path(e[1]) @@ -1630,7 +1632,7 @@ def pnc_k_shortest_simple_paths(self, source, target, weight_function=None, else: # get a path to target in G \ path[:dev_idx] deviation = shortest_path_func(path[dev_idx], target, - exclude_vertices=unnecessary_vertices.union(path[:dev_idx]), + exclude_vertices=path[:dev_idx], reduced_weight=sidetrack_cost) if not deviation: continue # no path to target in G \ path[:dev_idx] From a84f579c22a10666b07f52a826f90419ca269e71 Mon Sep 17 00:00:00 2001 From: kappybar Date: Wed, 2 Jul 2025 22:59:09 +0900 Subject: [PATCH 02/14] remove unnecessary calculation of non-simple path --- src/sage/graphs/path_enumeration.pyx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/sage/graphs/path_enumeration.pyx b/src/sage/graphs/path_enumeration.pyx index 516f4e0c002..91b5b3cffc5 100644 --- a/src/sage/graphs/path_enumeration.pyx +++ b/src/sage/graphs/path_enumeration.pyx @@ -1619,12 +1619,13 @@ def pnc_k_shortest_simple_paths(self, source, target, weight_function=None, ancestor_idx = ancestor_idx_func(e[1], deviation_i, len(path), ancestor_idx_dict) if ancestor_idx == -1: continue - new_path = path[:deviation_i + 1] + tree_path(e[1]) + new_is_simple = ancestor_idx > deviation_i + # no need to compute tree_path if new_is_simple is False + new_path = path[:deviation_i + 1] + (tree_path(e[1]) if new_is_simple else [e[1]]) new_path_idx = idx idx_to_path[new_path_idx] = new_path idx += 1 new_cost = original_cost + sidetrack_cost[(e[0], e[1])] - new_is_simple = ancestor_idx > deviation_i candidate_paths.push(((-new_cost, new_is_simple), (new_path_idx, deviation_i + 1))) if deviation_i == dev_idx: continue From 511ef38c573dc5455a7888715e75e50005098708 Mon Sep 17 00:00:00 2001 From: kappybar Date: Thu, 3 Jul 2025 22:18:27 +0900 Subject: [PATCH 03/14] replace list with set --- src/sage/graphs/path_enumeration.pyx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/sage/graphs/path_enumeration.pyx b/src/sage/graphs/path_enumeration.pyx index 91b5b3cffc5..e3009e789b8 100644 --- a/src/sage/graphs/path_enumeration.pyx +++ b/src/sage/graphs/path_enumeration.pyx @@ -1612,9 +1612,10 @@ def pnc_k_shortest_simple_paths(self, source, target, weight_function=None, # GET DEVIATION PATHS original_cost = cost - for deviation_i in range(len(path) - 1, dev_idx - 1, -1): + former_part = set(path) + for deviation_i in range(len(path) - 2, dev_idx - 1, -1): for e in G.outgoing_edge_iterator(path[deviation_i]): - if e[1] in path[:deviation_i + 2]: # e[1] is red or e in path + if e[1] in former_part: # e[1] is red or e in path continue ancestor_idx = ancestor_idx_func(e[1], deviation_i, len(path), ancestor_idx_dict) if ancestor_idx == -1: @@ -1630,6 +1631,7 @@ def pnc_k_shortest_simple_paths(self, source, target, weight_function=None, if deviation_i == dev_idx: continue original_cost -= sidetrack_cost[(path[deviation_i - 1], path[deviation_i])] + former_part.remove(path[deviation_i + 1]) else: # get a path to target in G \ path[:dev_idx] deviation = shortest_path_func(path[dev_idx], target, From 4ae3801cd029481dff39f77bf68959be76c8286b Mon Sep 17 00:00:00 2001 From: kappybar Date: Sat, 5 Jul 2025 11:46:41 +0900 Subject: [PATCH 04/14] relabel vertices to numbers to improve --- src/sage/graphs/path_enumeration.pyx | 70 +++++++++++++++++----------- 1 file changed, 44 insertions(+), 26 deletions(-) diff --git a/src/sage/graphs/path_enumeration.pyx b/src/sage/graphs/path_enumeration.pyx index e3009e789b8..1b0b3d0a05f 100644 --- a/src/sage/graphs/path_enumeration.pyx +++ b/src/sage/graphs/path_enumeration.pyx @@ -36,6 +36,8 @@ from itertools import product from sage.misc.misc_c import prod from libcpp.queue cimport priority_queue from libcpp.pair cimport pair +from libcpp.vector cimport vector +from libcpp.map cimport map as cmap from sage.rings.integer_ring import ZZ import copy @@ -1515,35 +1517,46 @@ def pnc_k_shortest_simple_paths(self, source, target, weight_function=None, G.delete_edges(G.incoming_edges(source, labels=False)) G.delete_edges(G.outgoing_edges(target, labels=False)) + # relabel the graph so that vertices are named with integers + cdef list int_to_vertex = list(G) + cdef dict vertex_to_int = {u: i for i, u in enumerate(int_to_vertex)} + G.relabel(perm=vertex_to_int, inplace=True) + cdef int id_source = vertex_to_int[source] + cdef int id_target = vertex_to_int[target] + + def relabeled_weight_function(e, wf=weight_function): + return wf((int_to_vertex[e[0]], int_to_vertex[e[1]], e[2])) + by_weight, weight_function = G._get_weight_function(by_weight=by_weight, - weight_function=weight_function, + weight_function=(relabeled_weight_function if weight_function else None), check_weight=check_weight) def reverse_weight_function(e): return weight_function((e[1], e[0], e[2])) - cdef dict edge_labels - edge_labels = {(e[0], e[1]): e for e in G.edge_iterator()} + cdef dict original_edge_labels = {(e[0], e[1]): (int_to_vertex[e[0]], int_to_vertex[e[1]], e[2]) + for e in G.edge_iterator()} + cdef dict original_edges = {(e[0], e[1]): (int_to_vertex[e[0]], int_to_vertex[e[1]]) + for e in G.edge_iterator(labels=False)} + cdef dict original_vertices = {i: int_to_vertex[i] for i in range(len(int_to_vertex))} - cdef dict edge_wt - edge_wt = {(e[0], e[1]): weight_function(e) for e in G.edge_iterator()} + cdef dict edge_wt = {(e[0], e[1]): weight_function(e) for e in G.edge_iterator()} # The first shortest path tree T_0 from sage.graphs.base.boost_graph import shortest_paths cdef dict dist cdef dict successor reverse_graph = G.reverse() - dist, successor = shortest_paths(reverse_graph, target, weight_function=reverse_weight_function, + dist, successor = shortest_paths(reverse_graph, id_target, weight_function=reverse_weight_function, algorithm='Dijkstra_Boost') cdef set unnecessary_vertices = set(G) - set(dist) # no path to target - if source in unnecessary_vertices: # no path from source to target + if id_source in unnecessary_vertices: # no path from source to target return G.delete_vertices(unnecessary_vertices) # sidetrack cost cdef dict sidetrack_cost = {(e[0], e[1]): weight_function(e) + dist[e[1]] - dist[e[0]] - for e in G.edge_iterator() - if e[0] in dist and e[1] in dist} + for e in G.edge_iterator() if e[0] in dist and e[1] in dist} def sidetrack_length(path): return sum(sidetrack_cost[e] for e in zip(path, path[1:])) @@ -1551,14 +1564,14 @@ def pnc_k_shortest_simple_paths(self, source, target, weight_function=None, # v-t path in the first shortest path tree T_0 def tree_path(v): path = [v] - while v != target: + while v != id_target: v = successor[v] path.append(v) return path # shortest path - shortest_path = tree_path(source) - cdef double shortest_path_length = dist[source] + shortest_path = tree_path(id_source) + cdef double shortest_path_length = dist[id_source] # idx of paths cdef dict idx_to_path = {0: shortest_path} @@ -1572,16 +1585,18 @@ def pnc_k_shortest_simple_paths(self, source, target, weight_function=None, # shortest path function for weighted/unweighted graph using reduced weights shortest_path_func = G._backend.bidirectional_dijkstra_special - # See explanation of ancestor_idx_dict below - def ancestor_idx_func(v, t, len_path, ancestor_idx_dict): + # See explanation of ancestor_idx_vec below + cdef vector[int] ancestor_idx_vec = [-1 for _ in range(len(G))] + + def ancestor_idx_func(v, t, len_path): if v not in successor: # target vertex is not reachable from v return -1 - if v in ancestor_idx_dict: - if ancestor_idx_dict[v] <= t or ancestor_idx_dict[v] == len_path - 1: - return ancestor_idx_dict[v] - ancestor_idx_dict[v] = ancestor_idx_func(successor[v], t, len_path, ancestor_idx_dict) - return ancestor_idx_dict[v] + if ancestor_idx_vec[v] != -1: + if ancestor_idx_vec[v] <= t or ancestor_idx_vec[v] == len_path - 1: + return ancestor_idx_vec[v] + ancestor_idx_vec[v] = ancestor_idx_func(successor[v], t, len_path) + return ancestor_idx_vec[v] candidate_paths.push(((0, True), (0, 0))) while candidate_paths.size(): @@ -1595,20 +1610,23 @@ def pnc_k_shortest_simple_paths(self, source, target, weight_function=None, if is_simple: # output if report_edges and labels: - P = [edge_labels[e] for e in zip(path, path[1:])] + P = [original_edge_labels[e] for e in zip(path, path[1:])] elif report_edges: - P = list(zip(path, path[1:])) + P = [original_edges[e] for e in zip(path, path[1:])] else: - P = path + P = [original_vertices[v] for v in path] if report_weight: yield (shortest_path_length + cost, P) else: yield P - # ancestor_idx_dict[v] := the first vertex of ``path[:t+1]`` or ``path[-1]`` reachable by + # ancestor_idx_vec[v] := the first vertex of ``path[:t+1]`` or ``path[-1]`` reachable by # edges of first shortest path tree from v when enumerating deviating edges # from ``path[t]``. - ancestor_idx_dict = {v: i for i, v in enumerate(path)} + for i in range(ancestor_idx_vec.size()): + ancestor_idx_vec[i] = -1 + for i, v in enumerate(path): + ancestor_idx_vec[v] = i # GET DEVIATION PATHS original_cost = cost @@ -1617,7 +1635,7 @@ def pnc_k_shortest_simple_paths(self, source, target, weight_function=None, for e in G.outgoing_edge_iterator(path[deviation_i]): if e[1] in former_part: # e[1] is red or e in path continue - ancestor_idx = ancestor_idx_func(e[1], deviation_i, len(path), ancestor_idx_dict) + ancestor_idx = ancestor_idx_func(e[1], deviation_i, len(path)) if ancestor_idx == -1: continue new_is_simple = ancestor_idx > deviation_i @@ -1634,7 +1652,7 @@ def pnc_k_shortest_simple_paths(self, source, target, weight_function=None, former_part.remove(path[deviation_i + 1]) else: # get a path to target in G \ path[:dev_idx] - deviation = shortest_path_func(path[dev_idx], target, + deviation = shortest_path_func(path[dev_idx], id_target, exclude_vertices=path[:dev_idx], reduced_weight=sidetrack_cost) if not deviation: From d84959b9413e16bd407c6ba33fc3be832499cee3 Mon Sep 17 00:00:00 2001 From: kappybar Date: Thu, 10 Jul 2025 21:15:51 +0900 Subject: [PATCH 05/14] fix --- src/sage/graphs/path_enumeration.pyx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/sage/graphs/path_enumeration.pyx b/src/sage/graphs/path_enumeration.pyx index 1b0b3d0a05f..4785563d66b 100644 --- a/src/sage/graphs/path_enumeration.pyx +++ b/src/sage/graphs/path_enumeration.pyx @@ -1534,12 +1534,10 @@ def pnc_k_shortest_simple_paths(self, source, target, weight_function=None, def reverse_weight_function(e): return weight_function((e[1], e[0], e[2])) - cdef dict original_edge_labels = {(e[0], e[1]): (int_to_vertex[e[0]], int_to_vertex[e[1]], e[2]) - for e in G.edge_iterator()} - cdef dict original_edges = {(e[0], e[1]): (int_to_vertex[e[0]], int_to_vertex[e[1]]) - for e in G.edge_iterator(labels=False)} - cdef dict original_vertices = {i: int_to_vertex[i] for i in range(len(int_to_vertex))} - + cdef dict original_edge_labels = {(u, v): (int_to_vertex[u], int_to_vertex[v], label) + for u, v, label in G.edge_iterator()} + cdef dict original_edges = {(u, v): (int_to_vertex[u], int_to_vertex[v]) + for u, v in G.edge_iterator(labels=False)} cdef dict edge_wt = {(e[0], e[1]): weight_function(e) for e in G.edge_iterator()} # The first shortest path tree T_0 @@ -1614,7 +1612,7 @@ def pnc_k_shortest_simple_paths(self, source, target, weight_function=None, elif report_edges: P = [original_edges[e] for e in zip(path, path[1:])] else: - P = [original_vertices[v] for v in path] + P = [int_to_vertex[v] for v in path] if report_weight: yield (shortest_path_length + cost, P) else: From 2a848ffafabd852bf5bb630aa88883c4782bc9ba Mon Sep 17 00:00:00 2001 From: kappybar Date: Sun, 13 Jul 2025 21:35:36 +0900 Subject: [PATCH 06/14] implement shortest_path_to_set (for efficient shortest path computation in pnc algorithm) --- src/sage/graphs/base/c_graph.pyx | 90 ++++++++++++++++++++++++++++ src/sage/graphs/path_enumeration.pyx | 44 ++++++++++---- 2 files changed, 123 insertions(+), 11 deletions(-) diff --git a/src/sage/graphs/base/c_graph.pyx b/src/sage/graphs/base/c_graph.pyx index 7d8aae5e823..e4bc52ccf03 100644 --- a/src/sage/graphs/base/c_graph.pyx +++ b/src/sage/graphs/base/c_graph.pyx @@ -46,6 +46,7 @@ method :meth:`realloc `. from cysignals.memory cimport check_allocarray, sig_free from libcpp.pair cimport pair from libcpp.queue cimport queue +from libcpp.queue cimport priority_queue from libcpp.stack cimport stack from sage.arith.long cimport pyobject_to_long @@ -3628,6 +3629,95 @@ cdef class CGraphBackend(GenericGraphBackend): return Infinity return [] + def shortest_path_to_set(self, x, y_set, by_weight=False, edge_weight=None, + exclude_vertices=None, report_weight=False,): + r""" + Return the shortest path from ``x`` to any vertex in ``y_set``. + + INPUT: + + - ``x`` -- the starting vertex. + + - ``y_set`` -- iterable container; the set of end vertices. + + - ``edge_weight`` -- dictionary (default: ``None``); a dictionary + that takes as input an edge ``(u, v)`` and outputs its weight. + If not ``None``, ``by_weight`` is automatically set to ``True``. + If ``None`` and ``by_weight`` is ``True``, we use the edge + label ``l`` as a weight. + + - ``by_weight`` -- boolean (default: ``False``); if ``True``, the edges + in the graph are weighted, otherwise all edges have weight 1. + + - ``exclude_vertices`` -- iterable container (default: ``None``); + iterable of vertices to exclude from the graph while calculating the + shortest path from ``x`` to any vertex in ``y_set``. + + - ``report_weight`` -- boolean (default: ``False``); if ``False``, just + a path is returned. Otherwise a tuple of path length and path is + returned. + + OUTPUT: + + - A list of vertices in the shortest path from ``x`` to any vertex + in ``y_set`` or a tuple of path lengh and path is returned + depending upon the value of parameter ``report_weight``. + + EXAMPLES:: + + sage: g = Graph([(1, 2, 10), (1, 3, 20), (1, 4, 30)]) + sage: g._backend.shortest_path_to_set(1, {3, 4}, by_weight=True) + [1, 3] + sage: g = Graph([(1, 2, 10), (2, 3, 10), (1, 4, 20), (4, 5, 20), (1, 6, 30), (6, 7, 30)]) + sage: g._backend.shortest_path_to_set(1, {5, 7}, by_weight=True, exclude_vertices=[4], report_weight=True) + (60.0, [1, 6, 7]) + """ + if not exclude_vertices: + exclude_vertices = [] + cdef priority_queue[pair[double, int]] pq + cdef dict dist = {} + cdef dict pred = {} + cdef int x_int = self.get_vertex(x) + pq.push((0, x_int)) + dist[x_int] = 0 + + while not pq.empty(): + negative_d, v_int = pq.top() + d = -negative_d + pq.pop() + v = self.vertex_label(v_int) + + if v in y_set: + # found a vertex in y_set + path = [] + while v_int in pred: + path.append(self.vertex_label(v_int)) + v_int = pred[v_int] + path.append(x) + path.reverse() + return (d, path) if report_weight else path + + if d > dist.get(v_int, float('inf')): + continue # already found a better path + + for _, u, l in self.iterator_out_edges([v], labels=True): + if u in exclude_vertices: + continue + if edge_weight: + e_weight = edge_weight[(v, u)] + elif by_weight: + e_weight = l + else: + e_weight = 1 + new_dist = d + e_weight + u_int = self.get_vertex(u) + if new_dist < dist.get(u_int, float('inf')): + dist[u_int] = new_dist + pred[u_int] = v_int + pq.push((-new_dist, u_int)) + + return + def bidirectional_dijkstra_special(self, x, y, weight_function=None, exclude_vertices=None, exclude_edges=None, include_vertices=None, distance_flag=False, diff --git a/src/sage/graphs/path_enumeration.pyx b/src/sage/graphs/path_enumeration.pyx index 4785563d66b..0e44d3740e0 100644 --- a/src/sage/graphs/path_enumeration.pyx +++ b/src/sage/graphs/path_enumeration.pyx @@ -1547,6 +1547,10 @@ def pnc_k_shortest_simple_paths(self, source, target, weight_function=None, reverse_graph = G.reverse() dist, successor = shortest_paths(reverse_graph, id_target, weight_function=reverse_weight_function, algorithm='Dijkstra_Boost') + cdef dict predecessor = {v: [] for v in dist} + for v, u in successor.items(): + if u is not None: + predecessor[u].append(v) cdef set unnecessary_vertices = set(G) - set(dist) # no path to target if id_source in unnecessary_vertices: # no path from source to target return @@ -1556,9 +1560,6 @@ def pnc_k_shortest_simple_paths(self, source, target, weight_function=None, cdef dict sidetrack_cost = {(e[0], e[1]): weight_function(e) + dist[e[1]] - dist[e[0]] for e in G.edge_iterator() if e[0] in dist and e[1] in dist} - def sidetrack_length(path): - return sum(sidetrack_cost[e] for e in zip(path, path[1:])) - # v-t path in the first shortest path tree T_0 def tree_path(v): path = [v] @@ -1581,7 +1582,7 @@ def pnc_k_shortest_simple_paths(self, source, target, weight_function=None, cdef priority_queue[pair[pair[double, bint], pair[int, int]]] candidate_paths # shortest path function for weighted/unweighted graph using reduced weights - shortest_path_func = G._backend.bidirectional_dijkstra_special + shortest_path_func = G._backend.shortest_path_to_set # See explanation of ancestor_idx_vec below cdef vector[int] ancestor_idx_vec = [-1 for _ in range(len(G))] @@ -1596,6 +1597,24 @@ def pnc_k_shortest_simple_paths(self, source, target, weight_function=None, ancestor_idx_vec[v] = ancestor_idx_func(successor[v], t, len_path) return ancestor_idx_vec[v] + def green_vertices(path): + # Find all vertices that are reachable to target in T_0 + # without crossing vertices of path + green = set() + in_path = [False for _ in range(len(G))] + for v in path: + in_path[v] = True + + def dfs(v): + if in_path[v]: + return + green.add(v) + for u in predecessor[v]: + dfs(u) + return + dfs(id_target) + return green + candidate_paths.push(((0, True), (0, 0))) while candidate_paths.size(): (negative_cost, is_simple), (path_idx, dev_idx) = candidate_paths.top() @@ -1649,17 +1668,20 @@ def pnc_k_shortest_simple_paths(self, source, target, weight_function=None, original_cost -= sidetrack_cost[(path[deviation_i - 1], path[deviation_i])] former_part.remove(path[deviation_i + 1]) else: - # get a path to target in G \ path[:dev_idx] - deviation = shortest_path_func(path[dev_idx], id_target, - exclude_vertices=path[:dev_idx], - reduced_weight=sidetrack_cost) - if not deviation: + green = green_vertices(path[:dev_idx + 1]) + # get a path to target in G \ path[:dev_idx] to one of green vertices + path2green = shortest_path_func(path[dev_idx], green, + report_weight=True, + exclude_vertices=set(path[:dev_idx]), + edge_weight=sidetrack_cost) + if not path2green: continue # no path to target in G \ path[:dev_idx] - new_path = path[:dev_idx] + deviation + deviation_weight, deviation = path2green + new_path = path[:dev_idx] + deviation[:-1] + tree_path(deviation[-1]) new_path_idx = idx idx_to_path[new_path_idx] = new_path idx += 1 - new_cost = sidetrack_length(new_path) + new_cost = cost + deviation_weight candidate_paths.push(((-new_cost, True), (new_path_idx, dev_idx))) From 9b7b880889dad4f3178cc97af3e0de53cd9c50cf Mon Sep 17 00:00:00 2001 From: kappybar Date: Mon, 14 Jul 2025 21:13:07 +0900 Subject: [PATCH 07/14] fix and add tests of shortest_path_to_set --- src/sage/graphs/base/c_graph.pyx | 52 +++++++++++++++++++++++--------- 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/src/sage/graphs/base/c_graph.pyx b/src/sage/graphs/base/c_graph.pyx index e4bc52ccf03..006c53743cd 100644 --- a/src/sage/graphs/base/c_graph.pyx +++ b/src/sage/graphs/base/c_graph.pyx @@ -57,6 +57,7 @@ from sage.rings.integer cimport smallInteger from sage.rings.integer_ring import ZZ +from collections.abc import Iterable cdef extern from "Python.h": int unlikely(int) nogil # Defined by Cython @@ -3629,16 +3630,16 @@ cdef class CGraphBackend(GenericGraphBackend): return Infinity return [] - def shortest_path_to_set(self, x, y_set, by_weight=False, edge_weight=None, + def shortest_path_to_set(self, source, targets, by_weight=False, edge_weight=None, exclude_vertices=None, report_weight=False,): r""" - Return the shortest path from ``x`` to any vertex in ``y_set``. + Return the shortest path from ``source`` to any vertex in ``targets``. INPUT: - - ``x`` -- the starting vertex. + - ``source`` -- the starting vertex. - - ``y_set`` -- iterable container; the set of end vertices. + - ``targets`` -- iterable container; the set of end vertices. - ``edge_weight`` -- dictionary (default: ``None``); a dictionary that takes as input an edge ``(u, v)`` and outputs its weight. @@ -3651,7 +3652,7 @@ cdef class CGraphBackend(GenericGraphBackend): - ``exclude_vertices`` -- iterable container (default: ``None``); iterable of vertices to exclude from the graph while calculating the - shortest path from ``x`` to any vertex in ``y_set``. + shortest path from ``source`` to any vertex in ``targets``. - ``report_weight`` -- boolean (default: ``False``); if ``False``, just a path is returned. Otherwise a tuple of path length and path is @@ -3659,8 +3660,8 @@ cdef class CGraphBackend(GenericGraphBackend): OUTPUT: - - A list of vertices in the shortest path from ``x`` to any vertex - in ``y_set`` or a tuple of path lengh and path is returned + - A list of vertices in the shortest path from ``source`` to any vertex + in ``targets`` or a tuple of path lengh and path is returned depending upon the value of parameter ``report_weight``. EXAMPLES:: @@ -3671,13 +3672,36 @@ cdef class CGraphBackend(GenericGraphBackend): sage: g = Graph([(1, 2, 10), (2, 3, 10), (1, 4, 20), (4, 5, 20), (1, 6, 30), (6, 7, 30)]) sage: g._backend.shortest_path_to_set(1, {5, 7}, by_weight=True, exclude_vertices=[4], report_weight=True) (60.0, [1, 6, 7]) + + TESTS:: + + sage: g = Graph([(1, 2, 10), (1, 3, 20), (1, 4, 30)]) + sage: assert g._backend.shortest_path_to_set(1, {3, 4}, exclude_vertices=[1]) is None + sage: assert g._backend.shortest_path_to_set(1, {3, 4}, exclude_vertices=[3, 4]) is None + sage: g._backend.shortest_path_to_set(1, {3, 4}, exclude_vertices=[3], by_weight=True) + [1, 4] + sage: g._backend.shortest_path_to_set(1, {1, 3, 4}, by_weight=True) + [1] + + ``exclude_vertices`` must be iterable:: + + sage: g._backend.shortest_path_to_set(1, {1, 3, 4}, exclude_vertices=100) + Traceback (most recent call last): + ... + TypeError: exclude_vertices (100) are not iterable. """ if not exclude_vertices: - exclude_vertices = [] + exclude_vertices = set() + elif not isinstance(exclude_vertices, Iterable): + raise TypeError(f"exclude_vertices ({exclude_vertices}) are not iterable.") + elif not isinstance(exclude_vertices, set): + exclude_vertices = set(exclude_vertices) + if source in exclude_vertices: + return cdef priority_queue[pair[double, int]] pq cdef dict dist = {} cdef dict pred = {} - cdef int x_int = self.get_vertex(x) + cdef int x_int = self.get_vertex(source) pq.push((0, x_int)) dist[x_int] = 0 @@ -3687,26 +3711,26 @@ cdef class CGraphBackend(GenericGraphBackend): pq.pop() v = self.vertex_label(v_int) - if v in y_set: - # found a vertex in y_set + if v in targets: + # found a vertex in targets path = [] while v_int in pred: path.append(self.vertex_label(v_int)) v_int = pred[v_int] - path.append(x) + path.append(source) path.reverse() return (d, path) if report_weight else path if d > dist.get(v_int, float('inf')): continue # already found a better path - for _, u, l in self.iterator_out_edges([v], labels=True): + for _, u, label in self.iterator_out_edges([v], labels=True): if u in exclude_vertices: continue if edge_weight: e_weight = edge_weight[(v, u)] elif by_weight: - e_weight = l + e_weight = label else: e_weight = 1 new_dist = d + e_weight From 47e3d3a1e3620274f53ac2c1205ab7e37cbc38ac Mon Sep 17 00:00:00 2001 From: kappybar Date: Mon, 14 Jul 2025 21:27:44 +0900 Subject: [PATCH 08/14] change priority queue into paring heap in shortest_path_to_set --- src/sage/graphs/base/c_graph.pyx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/sage/graphs/base/c_graph.pyx b/src/sage/graphs/base/c_graph.pyx index 006c53743cd..69160e89364 100644 --- a/src/sage/graphs/base/c_graph.pyx +++ b/src/sage/graphs/base/c_graph.pyx @@ -3698,16 +3698,15 @@ cdef class CGraphBackend(GenericGraphBackend): exclude_vertices = set(exclude_vertices) if source in exclude_vertices: return - cdef priority_queue[pair[double, int]] pq + cdef PairingHeap[int, double] pq = PairingHeap[int, double]() cdef dict dist = {} cdef dict pred = {} cdef int x_int = self.get_vertex(source) - pq.push((0, x_int)) + pq.push(x_int, 0) dist[x_int] = 0 while not pq.empty(): - negative_d, v_int = pq.top() - d = -negative_d + v_int, d = pq.top() pq.pop() v = self.vertex_label(v_int) @@ -3738,7 +3737,11 @@ cdef class CGraphBackend(GenericGraphBackend): if new_dist < dist.get(u_int, float('inf')): dist[u_int] = new_dist pred[u_int] = v_int - pq.push((-new_dist, u_int)) + if pq.contains(u_int): + if pq.value(u_int) > new_dist: + pq.decrease(u_int, new_dist) + else: + pq.push(u_int, new_dist) return From 861ce00b9310031ee35179b1803e4722f2ea4ed4 Mon Sep 17 00:00:00 2001 From: kappybar Date: Mon, 14 Jul 2025 22:55:57 +0900 Subject: [PATCH 09/14] erase unnecessary import --- src/sage/graphs/base/c_graph.pyx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/sage/graphs/base/c_graph.pyx b/src/sage/graphs/base/c_graph.pyx index 69160e89364..6f8b72b599a 100644 --- a/src/sage/graphs/base/c_graph.pyx +++ b/src/sage/graphs/base/c_graph.pyx @@ -46,7 +46,6 @@ method :meth:`realloc `. from cysignals.memory cimport check_allocarray, sig_free from libcpp.pair cimport pair from libcpp.queue cimport queue -from libcpp.queue cimport priority_queue from libcpp.stack cimport stack from sage.arith.long cimport pyobject_to_long From bf2487a1be0ca5c282d7ee0600a55735296ddccf Mon Sep 17 00:00:00 2001 From: kappybar Date: Mon, 14 Jul 2025 22:58:04 +0900 Subject: [PATCH 10/14] erase unnecessary import --- src/sage/graphs/path_enumeration.pyx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/sage/graphs/path_enumeration.pyx b/src/sage/graphs/path_enumeration.pyx index 0e44d3740e0..7ad406c2de6 100644 --- a/src/sage/graphs/path_enumeration.pyx +++ b/src/sage/graphs/path_enumeration.pyx @@ -37,7 +37,6 @@ from sage.misc.misc_c import prod from libcpp.queue cimport priority_queue from libcpp.pair cimport pair from libcpp.vector cimport vector -from libcpp.map cimport map as cmap from sage.rings.integer_ring import ZZ import copy From b688c68735655c488a2b60f0d33bf7bf9e1dacd7 Mon Sep 17 00:00:00 2001 From: kappybar Date: Wed, 16 Jul 2025 21:41:32 +0900 Subject: [PATCH 11/14] raise an error when no path found from source to targets in shortest_path_to_set --- src/sage/graphs/base/c_graph.pyx | 21 +++++++++++++++++---- src/sage/graphs/path_enumeration.pyx | 17 ++++++++++------- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/src/sage/graphs/base/c_graph.pyx b/src/sage/graphs/base/c_graph.pyx index 6f8b72b599a..9d9653cccd0 100644 --- a/src/sage/graphs/base/c_graph.pyx +++ b/src/sage/graphs/base/c_graph.pyx @@ -3675,13 +3675,25 @@ cdef class CGraphBackend(GenericGraphBackend): TESTS:: sage: g = Graph([(1, 2, 10), (1, 3, 20), (1, 4, 30)]) - sage: assert g._backend.shortest_path_to_set(1, {3, 4}, exclude_vertices=[1]) is None - sage: assert g._backend.shortest_path_to_set(1, {3, 4}, exclude_vertices=[3, 4]) is None sage: g._backend.shortest_path_to_set(1, {3, 4}, exclude_vertices=[3], by_weight=True) [1, 4] sage: g._backend.shortest_path_to_set(1, {1, 3, 4}, by_weight=True) [1] + ``source`` must not be in ``exclude_vertices``:: + + sage: g._backend.shortest_path_to_set(1, {3, 4}, exclude_vertices=[1]) + Traceback (most recent call last): + ... + ValueError: source must not be in exclude_vertices. + + When no path exists from ``source`` to ``targets``, raise an error. + + sage: g._backend.shortest_path_to_set(1, {3, 4}, exclude_vertices=[3, 4]) + Traceback (most recent call last): + ... + ValueError: no path found from source to targets. + ``exclude_vertices`` must be iterable:: sage: g._backend.shortest_path_to_set(1, {1, 3, 4}, exclude_vertices=100) @@ -3696,7 +3708,7 @@ cdef class CGraphBackend(GenericGraphBackend): elif not isinstance(exclude_vertices, set): exclude_vertices = set(exclude_vertices) if source in exclude_vertices: - return + raise ValueError(f"source must not be in exclude_vertices.") cdef PairingHeap[int, double] pq = PairingHeap[int, double]() cdef dict dist = {} cdef dict pred = {} @@ -3742,7 +3754,8 @@ cdef class CGraphBackend(GenericGraphBackend): else: pq.push(u_int, new_dist) - return + # no path found + raise ValueError(f"no path found from source to targets.") def bidirectional_dijkstra_special(self, x, y, weight_function=None, exclude_vertices=None, exclude_edges=None, diff --git a/src/sage/graphs/path_enumeration.pyx b/src/sage/graphs/path_enumeration.pyx index 7ad406c2de6..0b20ffb8223 100644 --- a/src/sage/graphs/path_enumeration.pyx +++ b/src/sage/graphs/path_enumeration.pyx @@ -1669,13 +1669,16 @@ def pnc_k_shortest_simple_paths(self, source, target, weight_function=None, else: green = green_vertices(path[:dev_idx + 1]) # get a path to target in G \ path[:dev_idx] to one of green vertices - path2green = shortest_path_func(path[dev_idx], green, - report_weight=True, - exclude_vertices=set(path[:dev_idx]), - edge_weight=sidetrack_cost) - if not path2green: - continue # no path to target in G \ path[:dev_idx] - deviation_weight, deviation = path2green + try: + deviation_weight, deviation = shortest_path_func(path[dev_idx], green, + report_weight=True, + exclude_vertices=set(path[:dev_idx]), + edge_weight=sidetrack_cost) + except ValueError as e: + if str(e) == "no path found from source to targets.": + continue # no path to target in G \ path[:dev_idx] + else: + raise new_path = path[:dev_idx] + deviation[:-1] + tree_path(deviation[-1]) new_path_idx = idx idx_to_path[new_path_idx] = new_path From 72d59086e3ede26ad770968b150d9f5ec9118106 Mon Sep 17 00:00:00 2001 From: kappybar Date: Thu, 17 Jul 2025 21:49:07 +0900 Subject: [PATCH 12/14] change to get green vertices adaptively --- src/sage/graphs/path_enumeration.pyx | 119 +++++++++++++++------------ 1 file changed, 65 insertions(+), 54 deletions(-) diff --git a/src/sage/graphs/path_enumeration.pyx b/src/sage/graphs/path_enumeration.pyx index 0b20ffb8223..c4433cb4a56 100644 --- a/src/sage/graphs/path_enumeration.pyx +++ b/src/sage/graphs/path_enumeration.pyx @@ -37,7 +37,10 @@ from sage.misc.misc_c import prod from libcpp.queue cimport priority_queue from libcpp.pair cimport pair from libcpp.vector cimport vector + +from sage.data_structures.pairing_heap cimport PairingHeap from sage.rings.integer_ring import ZZ + import copy @@ -1546,10 +1549,6 @@ def pnc_k_shortest_simple_paths(self, source, target, weight_function=None, reverse_graph = G.reverse() dist, successor = shortest_paths(reverse_graph, id_target, weight_function=reverse_weight_function, algorithm='Dijkstra_Boost') - cdef dict predecessor = {v: [] for v in dist} - for v, u in successor.items(): - if u is not None: - predecessor[u].append(v) cdef set unnecessary_vertices = set(G) - set(dist) # no path to target if id_source in unnecessary_vertices: # no path from source to target return @@ -1580,40 +1579,65 @@ def pnc_k_shortest_simple_paths(self, source, target, weight_function=None, # (i.e. real length = cost + shortest_path_length in T_0) cdef priority_queue[pair[pair[double, bint], pair[int, int]]] candidate_paths - # shortest path function for weighted/unweighted graph using reduced weights - shortest_path_func = G._backend.shortest_path_to_set - - # See explanation of ancestor_idx_vec below + # ancestor_idx_vec[v] := the first vertex of ``path[:t+1]`` or ``id_target`` reachable by + # edges of first shortest path tree from v. cdef vector[int] ancestor_idx_vec = [-1 for _ in range(len(G))] - def ancestor_idx_func(v, t, len_path): - if v not in successor: - # target vertex is not reachable from v - return -1 + def ancestor_idx_func(v, t, target_idx): if ancestor_idx_vec[v] != -1: - if ancestor_idx_vec[v] <= t or ancestor_idx_vec[v] == len_path - 1: + if ancestor_idx_vec[v] <= t or ancestor_idx_vec[v] == target_idx: return ancestor_idx_vec[v] - ancestor_idx_vec[v] = ancestor_idx_func(successor[v], t, len_path) + ancestor_idx_vec[v] = ancestor_idx_func(successor[v], t, target_idx) return ancestor_idx_vec[v] - def green_vertices(path): - # Find all vertices that are reachable to target in T_0 - # without crossing vertices of path - green = set() - in_path = [False for _ in range(len(G))] - for v in path: - in_path[v] = True - - def dfs(v): - if in_path[v]: - return - green.add(v) - for u in predecessor[v]: - dfs(u) - return - dfs(id_target) - return green + # used inside shortest_path_func + cdef PairingHeap[int, double] pq = PairingHeap[int, double]() + cdef dict dist_in_func = {} + cdef dict pred = {} + + def shortest_path_func(dev, exclude_vertices): + t = len(exclude_vertices) + ancestor_idx_vec[id_target] = t + 1 + # clear + while not pq.empty(): + pq.pop() + dist_in_func.clear() + pred.clear() + + pq.push(dev, 0) + dist_in_func[dev] = 0 + + while not pq.empty(): + v, d = pq.top() + pq.pop() + + if ancestor_idx_func(v, t, t + 1) == t + 1: # green + path = [] + while v in pred: + path.append(v) + v = pred[v] + path.append(dev) + path.reverse() + return (d, path) + + if d > dist_in_func.get(v, float('inf')): + continue # already found a better path + + for _, u, _ in G.outgoing_edge_iterator(v): + if u in exclude_vertices: + continue + new_dist = d + sidetrack_cost[(v, u)] + if new_dist < dist_in_func.get(u, float('inf')): + dist_in_func[u] = new_dist + pred[u] = v + if pq.contains(u): + if pq.value(u) > new_dist: + pq.decrease(u, new_dist) + else: + pq.push(u, new_dist) + return + cdef int i, deviation_i candidate_paths.push(((0, True), (0, 0))) while candidate_paths.size(): (negative_cost, is_simple), (path_idx, dev_idx) = candidate_paths.top() @@ -1623,6 +1647,11 @@ def pnc_k_shortest_simple_paths(self, source, target, weight_function=None, path = idx_to_path[path_idx] del idx_to_path[path_idx] + for i in range(ancestor_idx_vec.size()): + ancestor_idx_vec[i] = -1 + for i, v in enumerate(path): + ancestor_idx_vec[v] = i + if is_simple: # output if report_edges and labels: @@ -1636,14 +1665,6 @@ def pnc_k_shortest_simple_paths(self, source, target, weight_function=None, else: yield P - # ancestor_idx_vec[v] := the first vertex of ``path[:t+1]`` or ``path[-1]`` reachable by - # edges of first shortest path tree from v when enumerating deviating edges - # from ``path[t]``. - for i in range(ancestor_idx_vec.size()): - ancestor_idx_vec[i] = -1 - for i, v in enumerate(path): - ancestor_idx_vec[v] = i - # GET DEVIATION PATHS original_cost = cost former_part = set(path) @@ -1651,9 +1672,7 @@ def pnc_k_shortest_simple_paths(self, source, target, weight_function=None, for e in G.outgoing_edge_iterator(path[deviation_i]): if e[1] in former_part: # e[1] is red or e in path continue - ancestor_idx = ancestor_idx_func(e[1], deviation_i, len(path)) - if ancestor_idx == -1: - continue + ancestor_idx = ancestor_idx_func(e[1], deviation_i, len(path) - 1) new_is_simple = ancestor_idx > deviation_i # no need to compute tree_path if new_is_simple is False new_path = path[:deviation_i + 1] + (tree_path(e[1]) if new_is_simple else [e[1]]) @@ -1667,18 +1686,10 @@ def pnc_k_shortest_simple_paths(self, source, target, weight_function=None, original_cost -= sidetrack_cost[(path[deviation_i - 1], path[deviation_i])] former_part.remove(path[deviation_i + 1]) else: - green = green_vertices(path[:dev_idx + 1]) - # get a path to target in G \ path[:dev_idx] to one of green vertices - try: - deviation_weight, deviation = shortest_path_func(path[dev_idx], green, - report_weight=True, - exclude_vertices=set(path[:dev_idx]), - edge_weight=sidetrack_cost) - except ValueError as e: - if str(e) == "no path found from source to targets.": - continue # no path to target in G \ path[:dev_idx] - else: - raise + deviations = shortest_path_func(path[dev_idx], set(path[:dev_idx])) + if not deviations: + continue # no path to target in G \ path[:dev_idx] + deviation_weight, deviation = deviations new_path = path[:dev_idx] + deviation[:-1] + tree_path(deviation[-1]) new_path_idx = idx idx_to_path[new_path_idx] = new_path From 46287420bb3ef5358c9147b8776ed191914c3871 Mon Sep 17 00:00:00 2001 From: kappybar Date: Thu, 17 Jul 2025 21:56:48 +0900 Subject: [PATCH 13/14] change function name --- src/sage/graphs/path_enumeration.pyx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/sage/graphs/path_enumeration.pyx b/src/sage/graphs/path_enumeration.pyx index c4433cb4a56..617a92147e8 100644 --- a/src/sage/graphs/path_enumeration.pyx +++ b/src/sage/graphs/path_enumeration.pyx @@ -1590,12 +1590,13 @@ def pnc_k_shortest_simple_paths(self, source, target, weight_function=None, ancestor_idx_vec[v] = ancestor_idx_func(successor[v], t, target_idx) return ancestor_idx_vec[v] - # used inside shortest_path_func + # used inside shortest_path_to_green cdef PairingHeap[int, double] pq = PairingHeap[int, double]() cdef dict dist_in_func = {} cdef dict pred = {} - def shortest_path_func(dev, exclude_vertices): + # calculate shortest path from dev to one of green vertices + def shortest_path_to_green(dev, exclude_vertices): t = len(exclude_vertices) ancestor_idx_vec[id_target] = t + 1 # clear @@ -1686,7 +1687,7 @@ def pnc_k_shortest_simple_paths(self, source, target, weight_function=None, original_cost -= sidetrack_cost[(path[deviation_i - 1], path[deviation_i])] former_part.remove(path[deviation_i + 1]) else: - deviations = shortest_path_func(path[dev_idx], set(path[:dev_idx])) + deviations = shortest_path_to_green(path[dev_idx], set(path[:dev_idx])) if not deviations: continue # no path to target in G \ path[:dev_idx] deviation_weight, deviation = deviations From 3f1ddc69722faabb6a30303496bd3db5276588f9 Mon Sep 17 00:00:00 2001 From: kappybar Date: Thu, 17 Jul 2025 22:07:25 +0900 Subject: [PATCH 14/14] mino fix --- src/sage/graphs/path_enumeration.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sage/graphs/path_enumeration.pyx b/src/sage/graphs/path_enumeration.pyx index 617a92147e8..34431100807 100644 --- a/src/sage/graphs/path_enumeration.pyx +++ b/src/sage/graphs/path_enumeration.pyx @@ -1624,7 +1624,7 @@ def pnc_k_shortest_simple_paths(self, source, target, weight_function=None, if d > dist_in_func.get(v, float('inf')): continue # already found a better path - for _, u, _ in G.outgoing_edge_iterator(v): + for u in G.neighbor_out_iterator(v): if u in exclude_vertices: continue new_dist = d + sidetrack_cost[(v, u)]