2021-10-21 11:32:08 +13:00
|
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
|
|
|
|
|
namespace ShortestTotalPath
|
|
|
|
|
{
|
|
|
|
|
internal class Program
|
|
|
|
|
{
|
|
|
|
|
static void Main(string[] args)
|
|
|
|
|
{
|
2021-10-21 22:16:53 +13:00
|
|
|
|
// Take an approximate start time
|
|
|
|
|
for (int k = 0; k < 10; k++)
|
2021-10-21 11:32:08 +13:00
|
|
|
|
{
|
2021-10-21 22:16:53 +13:00
|
|
|
|
Graph g = new();
|
|
|
|
|
|
|
|
|
|
const int V = 10;
|
|
|
|
|
List<Node> 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++)
|
2021-10-21 11:32:08 +13:00
|
|
|
|
{
|
2021-10-21 22:16:53 +13:00
|
|
|
|
for (int j = 0; j < V; j++)
|
2021-10-21 11:32:08 +13:00
|
|
|
|
{
|
2021-10-21 22:16:53 +13:00
|
|
|
|
if (i != j)
|
2021-10-21 11:32:08 +13:00
|
|
|
|
{
|
2021-10-21 22:16:53 +13:00
|
|
|
|
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);
|
|
|
|
|
}
|
2021-10-21 11:32:08 +13:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-10-21 22:16:53 +13:00
|
|
|
|
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++)
|
2021-10-21 11:32:08 +13:00
|
|
|
|
{
|
2021-10-21 22:16:53 +13:00
|
|
|
|
Console.Write(n.TraversedNodes[i].Name);
|
|
|
|
|
if (i == n.TraversedNodes.Count - 1)
|
|
|
|
|
{
|
|
|
|
|
Console.WriteLine();
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
Console.Write(" -> ");
|
|
|
|
|
}
|
2021-10-21 11:32:08 +13:00
|
|
|
|
}
|
|
|
|
|
}
|
2021-10-21 22:16:53 +13:00
|
|
|
|
|
2021-10-21 11:32:08 +13:00
|
|
|
|
Console.ReadLine();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Algorithm
|
|
|
|
|
{
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Takes a graph, and the <paramref name="source"/> node, and returns the resulting node that contains the shortest path
|
|
|
|
|
/// through every node in <paramref name="g"/> (<paramref name="g"/> must be undirected)<br />
|
|
|
|
|
/// The resulting node contains an ordered list of traversed nodes and the total cost
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="source"></param>
|
|
|
|
|
/// <param name="g"></param>
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
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<Node, double> 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;
|
2021-10-21 22:16:53 +13:00
|
|
|
|
foreach (Node item in g.Nodes)
|
2021-10-21 11:32:08 +13:00
|
|
|
|
{
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Searches the dictionary for the <see cref="Node"/> with the shortest path-length + the heuristic estimate to the end goal.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="queue"></param>
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
Node PopShortest(Dictionary<Node, double> 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
|
2021-10-21 22:16:53 +13:00
|
|
|
|
if (queue.Keys.Count == 1)
|
2021-10-21 11:32:08 +13:00
|
|
|
|
{
|
2021-10-21 22:16:53 +13:00
|
|
|
|
foreach (Node n in queue.Keys)
|
2021-10-21 11:32:08 +13:00
|
|
|
|
{
|
|
|
|
|
shortest = n;
|
2021-10-21 22:16:53 +13:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
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;
|
|
|
|
|
}
|
2021-10-21 11:32:08 +13:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Remove the node from the queue
|
|
|
|
|
queue.Remove(shortest);
|
|
|
|
|
return shortest;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 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)<br />
|
|
|
|
|
/// See <seealso cref="Node"/>.
|
|
|
|
|
/// </summary>
|
|
|
|
|
class Graph
|
|
|
|
|
{
|
|
|
|
|
public HashSet<Node> Nodes { get; set; } = new HashSet<Node>();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// A node represents a vertex in a graph.<br />
|
|
|
|
|
/// At a minimum, the node only requires the Children to be set (which is the nodes this node is connected to, and their cost)<br />
|
|
|
|
|
/// For this algorithm, we have additional requirements:<br />
|
|
|
|
|
/// <list type="bullet">
|
|
|
|
|
/// <item>
|
|
|
|
|
/// <description>A node must keep track of its traversed nodes, and the traversed cost</description>
|
|
|
|
|
/// </item>
|
|
|
|
|
/// <item>
|
|
|
|
|
/// <description>It must provide a means to store a heuristic value</description>
|
|
|
|
|
/// </item>
|
|
|
|
|
/// <item>
|
|
|
|
|
/// <description>Each duplicated node <b>must be</b> unique</description>
|
|
|
|
|
/// </item>
|
|
|
|
|
/// <item>
|
|
|
|
|
/// <description>A reference to the fundamental (i.e., non-duplicant) parent is required to do loop checks</description>
|
|
|
|
|
/// </item>
|
|
|
|
|
/// </list>
|
|
|
|
|
/// The heuristic function used is the distance to the farthest non-traversed node
|
|
|
|
|
/// </summary>
|
|
|
|
|
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<Node, double> Children { get; set; } = new();
|
|
|
|
|
public List<Node> TraversedNodes { get; set; } = new List<Node>();
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Creates a new node, based on the provided name
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="name"></param>
|
|
|
|
|
public Node(string name)
|
|
|
|
|
{
|
|
|
|
|
Name = name;
|
|
|
|
|
TraversedNodes.Add(this);
|
|
|
|
|
}
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Create a deep-copy (excluding children) of the provided source node, using the provided cost
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="source"></param>
|
|
|
|
|
/// <param name="parent"></param>
|
|
|
|
|
/// <param name="cost"></param>
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Pretty-ify the debug output
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
public override string ToString()
|
|
|
|
|
{
|
|
|
|
|
return $"{Name} : {GetHashCode()}";
|
|
|
|
|
}
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Get the farthest distance to a node that has not been traversed (this is the minimal solution distance, and is always admissible)
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
public double FarthestUnvisitedNodeDistance()
|
|
|
|
|
{
|
|
|
|
|
double max = 0;
|
|
|
|
|
foreach (KeyValuePair<Node, double> child in Children)
|
|
|
|
|
{
|
|
|
|
|
if (child.Value > max && !TraversedNodes.Contains(child.Key))
|
|
|
|
|
{
|
|
|
|
|
max = child.Value;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return max;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Detect loop patterns, so that an affected tree is expanded no further
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
public bool IsInPattern()
|
|
|
|
|
{
|
|
|
|
|
if (TraversedNodes.Count < 4)
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
if (TraversedNodes[^1] == TraversedNodes[^3] && TraversedNodes[^2] == TraversedNodes[^4])
|
|
|
|
|
{
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
else return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|