using System; using System.Collections.Generic; using System.Threading.Tasks; namespace ShortestTotalPath { internal class Program { static void Main(string[] args) { // Take an approximate start time for (int k = 0; k < 10; k++) { Graph g = new(); const int V = 10; List nodes = new(); // Use random numbers, but with a controlled seed (tests are repeatable) Random rand = new(); // double[] doubles = new double[] { 1.54, 1.37, 3.78, 8.54, 4.656, 7.334, 6.4643, 3.342, 3.456, 4.567 }; // 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.0); } } } } 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") + " seconds"); Console.WriteLine(n.TotalTraversedLength); 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.WriteLine("Finished"); 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 (Node 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 = qNode.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 if (queue.Keys.Count == 1) { foreach (Node n in queue.Keys) { shortest = n; } } else { foreach (Node n in queue.Keys) { // Compare Length plus heuristic to the current shortest length if (n.TotalTraversedLength + n.LocalHeuristic < length) { // new shortest found, replace the current shortest values shortest = n; length = n.TotalTraversedLength + n.LocalHeuristic; } } } // 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; } } } }