@@ -13,6 +13,7 @@ This module is meant for all functions related to path enumeration in graphs.
13
13
:func:`all_paths` | Return the list of all paths between a pair of vertices.
14
14
:func:`yen_k_shortest_simple_paths` | Return an iterator over the simple paths between a pair of vertices in increasing order of weights.
15
15
: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.
16
17
:func:`all_paths_iterator` | Return an iterator over the paths of ``self``.
17
18
:func:`all_simple_paths` | Return a list of all the simple paths of ``self`` starting with one of the given vertices.
18
19
: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,
1383
1384
reduced_cost[e[0 ], e[1 ]] = temp_dict[e[0 ], e[1 ]]
1384
1385
1385
1386
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
+
1386
1645
def _all_paths_iterator (self , vertex , ending_vertices = None ,
1387
1646
simple = False , max_length = None , trivial = False ,
1388
1647
use_multiedges = False , report_edges = False ,
0 commit comments