From e72705fc4c9c21002dd026b80b6cba5ec37bcb2d Mon Sep 17 00:00:00 2001 From: Michael Schneider Date: Thu, 20 Jul 2023 10:51:58 +0200 Subject: [PATCH] Support dynamic path decisions within the YenShortestPathsAlgorithm - do not rely on a fixed k, but find as many path until a certain condition is met - decide whether to add a path to the final result set right after finding this path instead of having to wait until all paths are discovered --- .../ShortestPath/YenShortestPathsAlgorithm.cs | 138 +++++++++++++++++- .../YenShortestPathsAlgorithmTests.cs | 136 +++++++++++++++++ 2 files changed, 270 insertions(+), 4 deletions(-) diff --git a/src/QuikGraph/Algorithms/ShortestPath/YenShortestPathsAlgorithm.cs b/src/QuikGraph/Algorithms/ShortestPath/YenShortestPathsAlgorithm.cs index b2c30b51f..651178112 100644 --- a/src/QuikGraph/Algorithms/ShortestPath/YenShortestPathsAlgorithm.cs +++ b/src/QuikGraph/Algorithms/ShortestPath/YenShortestPathsAlgorithm.cs @@ -16,6 +16,95 @@ namespace QuikGraph.Algorithms.ShortestPath /// Vertex type. public class YenShortestPathsAlgorithm { + /// + /// Struct provides the parameters used for the path inspection. + /// + public struct InspectPathParameters + { + /// + /// Initializes a new instance of the struct. + /// + public InspectPathParameters(SortedPath path, IEnumerable shortestPaths, IEnumerable acceptedPaths) + { + Path = path; + ShortestPaths = shortestPaths; + AcceptedPaths = acceptedPaths; + } + + /// + /// The current found shortest path. + /// + public SortedPath Path { get; } + + /// + /// All shortest paths found so far. They might were accepted or not. Includes the current found path. + /// + public IEnumerable ShortestPaths { get; } + + /// + /// All shortest paths that were accepted so far. + /// + public IEnumerable AcceptedPaths { get; } + } + + /// + /// Struct provides the result of the path inspection. + /// + public struct InspectPathResult + { + /// + /// Initializes a new instance of the struct. + /// + public InspectPathResult(PathAcceptance pathAcceptance, SearchContinuation searchContinuation) + { + PathAcceptance = pathAcceptance; + SearchContinuation = searchContinuation; + } + + /// + /// Decides whether to add the found path to the result set. + /// + public PathAcceptance PathAcceptance { get; } + + /// + /// Decide whether to stop searching for more paths. + /// + public SearchContinuation SearchContinuation { get; } + } + + /// + /// After path inspection, decide whether to add the found path to the result set. + /// + public enum PathAcceptance + { + /// + /// The found shortest path will be added to the result list. + /// + Accept, + + /// + /// The found shortest path will not be added to the result list. + /// + Reject + } + + /// + /// After path inspection, decide whether to stop searching for more paths. + /// + [Flags] + public enum SearchContinuation + { + /// + /// Continue the search until the k shortest paths are found + /// + Continue, + + /// + /// The the search and return the found paths even if not k iterations have been done. + /// + StopSearch + } + /// /// Class representing a sorted path. /// @@ -109,6 +198,9 @@ IEnumerator IEnumerable.GetEnumerator() [NotNull] private readonly Func, IEnumerable> _filter; + [NotNull] + private readonly Func _inspectPath; + // Limit for the amount of paths private readonly int _k; @@ -127,6 +219,7 @@ IEnumerator IEnumerable.GetEnumerator() /// Maximum number of path to search. /// Optional function that computes the weight for a given edge. /// Optional filter of found paths. + /// Optional function to inspect a found shortest path right after it was found. /// is . /// is . /// is . @@ -139,7 +232,8 @@ public YenShortestPathsAlgorithm( [NotNull] TVertex target, int k, [CanBeNull] Func, double> edgeWeights = null, - [CanBeNull] Func, IEnumerable> filter = null) + [CanBeNull] Func, IEnumerable> filter = null, + [CanBeNull] Func inspectPath = null) { if (graph is null) throw new ArgumentNullException(nameof(graph)); @@ -160,6 +254,7 @@ public YenShortestPathsAlgorithm( _graph = graph.Clone(); _weights = edgeWeights ?? DefaultGetWeights; _filter = filter ?? DefaultFilter; + _inspectPath = inspectPath ?? DefaultInspectPath; } [Pure] @@ -175,6 +270,12 @@ private static double DefaultGetWeights([NotNull] EquatableTaggedEdge Execute() { SortedPath initialPath = GetInitialShortestPath(); var shortestPaths = new List { initialPath }; + var acceptedPaths = new List(); + + InspectPathResult inspectPathResult = _inspectPath(new InspectPathParameters(initialPath, shortestPaths, acceptedPaths)); + + if (inspectPathResult.PathAcceptance == PathAcceptance.Accept) + { + acceptedPaths.Add(initialPath); + } + + if (inspectPathResult.SearchContinuation == SearchContinuation.StopSearch) + { + return _filter(acceptedPaths); + } // Initialize the set to store the potential k-th shortest path var shortestPathCandidates = new BinaryQueue(GetPathDistance); + var previousPath = initialPath; for (int k = 1; k < _k; ++k) { - SortedPath previousPath = shortestPaths[k - 1]; - if (!SearchAndAddKthShortestPath(previousPath, shortestPaths, shortestPathCandidates)) + { break; + } + + SortedPath newPath = shortestPaths[k]; + inspectPathResult = _inspectPath(new InspectPathParameters(newPath, shortestPaths, acceptedPaths)); + + if (inspectPathResult.PathAcceptance == PathAcceptance.Accept) + { + acceptedPaths.Add(newPath); + } + + if (inspectPathResult.SearchContinuation == SearchContinuation.StopSearch) + { + break; + } + + previousPath = newPath; } - return _filter(shortestPaths); + return _filter(acceptedPaths); } [NotNull, ItemNotNull] diff --git a/tests/QuikGraph.Tests/Algorithms/ShortestPath/YenShortestPathsAlgorithmTests.cs b/tests/QuikGraph.Tests/Algorithms/ShortestPath/YenShortestPathsAlgorithmTests.cs index 895df5296..e55a74fd1 100644 --- a/tests/QuikGraph.Tests/Algorithms/ShortestPath/YenShortestPathsAlgorithmTests.cs +++ b/tests/QuikGraph.Tests/Algorithms/ShortestPath/YenShortestPathsAlgorithmTests.cs @@ -365,6 +365,142 @@ void RunYenAndCheck(YenShortestPathsAlgorithm yen) #endregion } + [Test] + public void InspectPath_RejectSomePaths() + { + var graph = new AdjacencyGraph>(false); + graph.AddVertexRange(new[] { "A", "B", "C", "D" }); + var edges = new[] + { + new EquatableTaggedEdge("A", "B", 5), + new EquatableTaggedEdge("A", "C", 6), + new EquatableTaggedEdge("B", "C", 7), + new EquatableTaggedEdge("B", "D", 8), + new EquatableTaggedEdge("C", "D", 9) + }; + graph.AddEdgeRange(edges); + + // ignore all paths where "B" comes in the 2nd position + var algorithm = new YenShortestPathsAlgorithm(graph, "A", "D", 5, inspectPath: InspectPath); + YenShortestPathsAlgorithm.SortedPath[] paths = algorithm.Execute().ToArray(); + + // Expecting to find 3 paths: + // * => A-B-D (gets ignored) + // 1 => A-C-D + // * => A-B-C-D (gets ignored) + // Consistently checking the result + Assert.AreEqual(1, paths.Length); + // 1 + EquatableTaggedEdge[] path0 = paths[0].ToArray(); + Assert.AreEqual(path0[0], edges[1]); + Assert.AreEqual(path0[1], edges[4]); + + #region Local functions + + YenShortestPathsAlgorithm.InspectPathResult InspectPath(YenShortestPathsAlgorithm.InspectPathParameters inspectPathParameters) + { + var pathAcceptance = inspectPathParameters.Path.GetVertex(1) == "B" + ? YenShortestPathsAlgorithm.PathAcceptance.Reject + : YenShortestPathsAlgorithm.PathAcceptance.Accept; + + return new YenShortestPathsAlgorithm.InspectPathResult(pathAcceptance, YenShortestPathsAlgorithm.SearchContinuation.Continue); + } + + #endregion + } + + [Test] + public void InspectPath_StopsSearchWhenConditionIsMet_ForInitialShortestPath() + { + var graph = new AdjacencyGraph>(false); + graph.AddVertexRange(new[] { "A", "B", "C", "D" }); + var edges = new[] + { + new EquatableTaggedEdge("A", "B", 5), + new EquatableTaggedEdge("A", "C", 6), + new EquatableTaggedEdge("B", "C", 7), + new EquatableTaggedEdge("B", "D", 8), + new EquatableTaggedEdge("C", "D", 9) + }; + graph.AddEdgeRange(edges); + + // stop when we have found a shortest path that exceeds some costs + var algorithm = new YenShortestPathsAlgorithm(graph, "A", "D", 10, inspectPath: InspectPath); + YenShortestPathsAlgorithm.SortedPath[] paths = algorithm.Execute().ToArray(); + + // Expecting to get 3 paths: + // 1 => A-B-D (cost = 13) + // * => A-C-D (path limit reached) + // * => A-B-C-D (path limit reached) + // Consistently checking the result + Assert.AreEqual(1, paths.Length); + // 1 + EquatableTaggedEdge[] path0 = paths[0].ToArray(); + Assert.AreEqual(path0[0], edges[0]); + Assert.AreEqual(path0[1], edges[3]); + + #region Local functions + + YenShortestPathsAlgorithm.InspectPathResult InspectPath(YenShortestPathsAlgorithm.InspectPathParameters inspectPathParameters) + { + var searchContinuation = inspectPathParameters.Path.Sum(x => x.Tag) >= 10 + ? YenShortestPathsAlgorithm.SearchContinuation.StopSearch + : YenShortestPathsAlgorithm.SearchContinuation.Continue; + + return new YenShortestPathsAlgorithm.InspectPathResult(YenShortestPathsAlgorithm.PathAcceptance.Accept, searchContinuation); + } + + #endregion + } + + [Test] + public void InspectPath_StopsSearchWhenConditionIsMet_ForNthShortestPath() + { + var graph = new AdjacencyGraph>(false); + graph.AddVertexRange(new[] { "A", "B", "C", "D" }); + var edges = new[] + { + new EquatableTaggedEdge("A", "B", 5), + new EquatableTaggedEdge("A", "C", 6), + new EquatableTaggedEdge("B", "C", 7), + new EquatableTaggedEdge("B", "D", 8), + new EquatableTaggedEdge("C", "D", 9) + }; + graph.AddEdgeRange(edges); + + // stop when we have found a shortest path that exceeds some costs + var algorithm = new YenShortestPathsAlgorithm(graph, "A", "D", 10, inspectPath: InspectPath); + YenShortestPathsAlgorithm.SortedPath[] paths = algorithm.Execute().ToArray(); + + // Expecting to get 3 paths: + // 1 => A-B-D (cost = 13) + // 2 => A-C-D (cost = 15) + // * => A-B-C-D (path limit reached) + // Consistently checking the result + Assert.AreEqual(2, paths.Length); + // 1 + EquatableTaggedEdge[] path0 = paths[0].ToArray(); + Assert.AreEqual(path0[0], edges[0]); + Assert.AreEqual(path0[1], edges[3]); + // 2 + EquatableTaggedEdge[] path1 = paths[1].ToArray(); + Assert.AreEqual(path1[0], edges[1]); + Assert.AreEqual(path1[1], edges[4]); + + #region Local functions + + YenShortestPathsAlgorithm.InspectPathResult InspectPath(YenShortestPathsAlgorithm.InspectPathParameters inspectPathParameters) + { + var searchContinuation = inspectPathParameters.Path.Sum(x => x.Tag) >= 15 + ? YenShortestPathsAlgorithm.SearchContinuation.StopSearch + : YenShortestPathsAlgorithm.SearchContinuation.Continue; + + return new YenShortestPathsAlgorithm.InspectPathResult(YenShortestPathsAlgorithm.PathAcceptance.Accept, searchContinuation); + } + + #endregion + } + [Test] public void SortedPathHashCode() {