From d40ebdabd2f7769496f47609a46c9012001a81fd Mon Sep 17 00:00:00 2001 From: Brychan Dempsey Date: Thu, 21 Oct 2021 11:32:08 +1300 Subject: [PATCH] Added solution files --- ShortestTotalPath.sln | 25 ++ ShortestTotalPath/Program.cs | 277 +++++++++++++++++++++ ShortestTotalPath/ShortestTotalPath.csproj | 8 + 3 files changed, 310 insertions(+) create mode 100644 ShortestTotalPath.sln create mode 100644 ShortestTotalPath/Program.cs create mode 100644 ShortestTotalPath/ShortestTotalPath.csproj diff --git a/ShortestTotalPath.sln b/ShortestTotalPath.sln new file mode 100644 index 0000000..2685f4b --- /dev/null +++ b/ShortestTotalPath.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31808.319 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ShortestTotalPath", "ShortestTotalPath\ShortestTotalPath.csproj", "{BC8B178D-A382-4AC9-9A96-BF1636B6D181}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {BC8B178D-A382-4AC9-9A96-BF1636B6D181}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BC8B178D-A382-4AC9-9A96-BF1636B6D181}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BC8B178D-A382-4AC9-9A96-BF1636B6D181}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BC8B178D-A382-4AC9-9A96-BF1636B6D181}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {22471F52-251B-4504-9154-B4FF6B414C97} + EndGlobalSection +EndGlobal diff --git a/ShortestTotalPath/Program.cs b/ShortestTotalPath/Program.cs new file mode 100644 index 0000000..97ce6cd --- /dev/null +++ b/ShortestTotalPath/Program.cs @@ -0,0 +1,277 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace ShortestTotalPath +{ + internal class Program + { + static void Main(string[] args) + { + Graph g = new(); + + const int V = 10; + List nodes = new(); + // Use random numbers, but with a controlled seed (tests are repeatable) + Random rand = new(2); + // Create the nodes + for (int i = 0; i < V; i++) + { + Node newNode = new(i.ToString()); + g.Nodes.Add(newNode); + nodes.Add(newNode); + } + // Create the links between nodes + for (int i = 0; i < V; i++) + { + for (int j = 0; j < V; j++) + { + if (i != j) + { + if (nodes[j].Children.TryGetValue(nodes[i], out double value)) + { + nodes[i].Children.Add(nodes[j], value); + } + else + { + nodes[i].Children.Add(nodes[j], 0.5 + rand.NextDouble() * 10); + } + } + } + } + // Take an approximate start time + DateTime start = DateTime.Now; + Algorithm a = new Algorithm(); + Node n = a.Run(nodes[0], g); + DateTime end = DateTime.Now; + Console.WriteLine((end - start).TotalSeconds.ToString("N3")); + Console.Write("Path: "); + for (int i = 0; i < n.TraversedNodes.Count; i++) + { + Console.Write(n.TraversedNodes[i].Name); + if (i == n.TraversedNodes.Count - 1) + { + Console.WriteLine(); + } + else + { + Console.Write(" -> "); + } + } + Console.ReadLine(); + } + + + class Algorithm + { + /// + /// Takes a graph, and the node, and returns the resulting node that contains the shortest path + /// through every node in ( must be undirected)
+ /// The resulting node contains an ordered list of traversed nodes and the total cost + ///
+ /// + /// + /// + public Node Run(Node source, Graph g) + { + // Keep track of the node to return + Node shortestVisitors = null; + // Create a queue of nodes. + // As we must always select the lowest cost node (+ heuristic) + // This must be O(n) traversed every iteration + Dictionary queue = new(); + // Add our source node to the queue + queue.Add(source, 0); + // While the queue still has nodes, expand them + while (queue.Count > 0) + { + // Pop the smallest node, including the heuristic from the queue + Node poppedNode = PopShortest(queue); + // If the node has visted nodes + if (poppedNode.TraversedNodes != null) + { + // See if the node's traversed nodes contains all nodes in the graph + bool trueForAll = true; + foreach (var item in g.Nodes) + { + if (!poppedNode.TraversedNodes.Contains(item)) + { + // A node was missing, so don't bother expanding any further + trueForAll = false; + break; + } + } + if (trueForAll) + { + // The popped node traverses every node, + // Don't bother expanding any more nodes in the queue + shortestVisitors = poppedNode; + break; + } + } + // Add children to the queue + foreach (Node child in poppedNode.Children.Keys) + { + // Check if the last four traversed nodes form a pattern, and our target node is one of those two nodes + // If so, don't queue this node for further expansion, as we're stuck in a local loop until the cost exceeds + // other calculated costs + if (poppedNode.IsInPattern() && (poppedNode.TraversedNodes[^2] == child || poppedNode.TraversedNodes[^1] == child)) + { + // If the node would cause us to enter a local loop, disregard it + continue; + } + // Skip the parent node, unless it would be the last node we can expand (i.e., don't do a two-node loop) + if (child != poppedNode.ParentNode || (queue.Count == 0 && poppedNode.Children.Count == 1)) + { + // Create a child node that is a copy of the current child, and set the total distance to the + // current total length of n + the length to the child + double length = poppedNode.TotalTraversedLength + poppedNode.Children[child]; + Node qNode = new(child, poppedNode, length); + // A admissible (never over-estimates) heuristic is largest distance to an un-traversed node from our current node. + qNode.LocalHeuristic = poppedNode.FarthestUnvisitedNodeDistance(); + // Add the node to the queue + queue.Add(qNode, length); + } + } + } + return shortestVisitors; + } + /// + /// Searches the dictionary for the with the shortest path-length + the heuristic estimate to the end goal. + /// + /// + /// + Node PopShortest(Dictionary queue) + { + Node shortest = null; + double length = double.MaxValue; + // Most application processing time is spent in this loop, although it is only O(n); + // This is because the saturated n^2 graph (every node has n connections) results in O(b^d) items in the queue + // We could reduce this to O(1), at the cost of longer insertion times, when sorting added items immediately. + // Another approach is to run simultaneous comparisons on chunks of data, i.e. break the queue into two-or-more + // sections and find the lowest of each section, then grab the overall lowest, but this requires additional + // time to establish the required threads. For graphs > rank of 10, that may be a better approach + foreach (Node n in queue.Keys) + { + // Compare Length plus heuristic to the current shortest length + if (shortest is null || n.TotalTraversedLength + n.LocalHeuristic < length) + { + // new shortest found, replace the current shortest values + shortest = n; + length = n.TotalTraversedLength; + } + } + // Remove the node from the queue + queue.Remove(shortest); + return shortest; + } + } + + /// + /// A graph is just a collection of Nodes (and any helper functions) + /// As nodes are usually only read, keep them in a Set, which provides O(1) lookup time + /// (We can find a specific node nearly instantly)
+ /// See . + ///
+ class Graph + { + public HashSet Nodes { get; set; } = new HashSet(); + } + + /// + /// A node represents a vertex in a graph.
+ /// At a minimum, the node only requires the Children to be set (which is the nodes this node is connected to, and their cost)
+ /// For this algorithm, we have additional requirements:
+ /// + /// + /// A node must keep track of its traversed nodes, and the traversed cost + /// + /// + /// It must provide a means to store a heuristic value + /// + /// + /// Each duplicated node must be unique + /// + /// + /// A reference to the fundamental (i.e., non-duplicant) parent is required to do loop checks + /// + /// + /// The heuristic function used is the distance to the farthest non-traversed node + ///
+ class Node + { + public string Name { get; set; } + public Node ParentNode { get; set; } + public double TotalTraversedLength { get; set; } = 0; + public double LocalHeuristic { get; set; } = 0; + public Dictionary Children { get; set; } = new(); + public List TraversedNodes { get; set; } = new List(); + + /// + /// Creates a new node, based on the provided name + /// + /// + public Node(string name) + { + Name = name; + TraversedNodes.Add(this); + } + /// + /// Create a deep-copy (excluding children) of the provided source node, using the provided cost + /// + /// + /// + /// + public Node(Node source, Node parent, double cost) + { + ParentNode = parent; + Name = source.Name; + TotalTraversedLength = cost; + TraversedNodes = new(parent.TraversedNodes); + TraversedNodes.Add(source); + Children = source.Children; + } + /// + /// Pretty-ify the debug output + /// + /// + public override string ToString() + { + return $"{Name} : {GetHashCode()}"; + } + /// + /// Get the farthest distance to a node that has not been traversed (this is the minimal solution distance, and is always admissible) + /// + /// + public double FarthestUnvisitedNodeDistance() + { + double max = 0; + foreach (KeyValuePair child in Children) + { + if (child.Value > max && !TraversedNodes.Contains(child.Key)) + { + max = child.Value; + } + } + return max; + } + + /// + /// Detect loop patterns, so that an affected tree is expanded no further + /// + /// + public bool IsInPattern() + { + if (TraversedNodes.Count < 4) + { + return false; + } + if (TraversedNodes[^1] == TraversedNodes[^3] && TraversedNodes[^2] == TraversedNodes[^4]) + { + return true; + } + else return false; + } + } + } +} diff --git a/ShortestTotalPath/ShortestTotalPath.csproj b/ShortestTotalPath/ShortestTotalPath.csproj new file mode 100644 index 0000000..2082704 --- /dev/null +++ b/ShortestTotalPath/ShortestTotalPath.csproj @@ -0,0 +1,8 @@ + + + + Exe + net5.0 + + +