I recently needed to to implement a shortest-path algorithm (to identify preferred domain controllers using site link costs) and I found Dijkstra’s Algorithm
Path class
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace DijkstraAlgorithm { public class Path<T> { public T Source { get; set; } public T Destination { get; set; } /// <summary> /// Cost of using this path from Source to Destination /// </summary> ////// Lower costs are preferable to higher costs /// </remarks> public int Cost { get; set; } } }
ExtensionMethods class
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace DijkstraAlgorithm { public static class ExtensionMethods { /// <summary> /// Adds or Updates the dictionary to include the destination and its associated cost /// and complete path (and param arrays make paths easier to work with) /// </summary> public static void Set<T>(this Dictionary<T, KeyValuePair<int, LinkedList<Path<T>>>> Dictionary, T destination, int Cost, params Path<T>[] paths) { var CompletePath = paths == null ? new LinkedList<Path<T>>() : new LinkedList<Path<T>>(paths); Dictionary[destination] = new KeyValuePair<int, LinkedList<Path<T>>>(Cost, CompletePath); } } }
Engine class
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace DijkstraAlgorithm { /// <summary> /// Calculates the best route between various paths, using Dijkstra's algorithm /// </summary> public static class Engine { public static LinkedList<Path<T>> CalculateShortestPathBetween<T>(T source, T destination, IEnumerable<Path<T>> Paths) { return CalculateFrom(source, Paths)[destination]; } public static Dictionary<T, LinkedList<Path<T>>> CalculateShortestFrom<T>(T source, IEnumerable<Path<T>> Paths) { return CalculateFrom(source, Paths); } private static Dictionary<T, LinkedList<Path<T>>> CalculateFrom<T>(T source, IEnumerable<Path<T>> Paths) { // validate the paths if (Paths.Any(p => p.Source.Equals(p.Destination))) throw new ArgumentException("No path can have the same source and destination"); // keep track of the shortest paths identified thus far Dictionary<T, KeyValuePair<int, LinkedList<Path<T>>>> ShortestPaths = new Dictionary<T, KeyValuePair<int, LinkedList<Path<T>>>>(); // keep track of the locations which have been completely processed List<T> LocationsProcessed = new List<T>(); // include all possible steps, with Int.MaxValue cost Paths.SelectMany(p => new T[] { p.Source, p.Destination }) // union source and destinations .Distinct() // remove duplicates .ToList() // ToList exposes ForEach .ForEach(s => ShortestPaths.Set(s, Int32.MaxValue, null)); // add to ShortestPaths with MaxValue cost // update cost for self-to-self as 0; no path ShortestPaths.Set(source, 0, null); // keep this cached var LocationCount = ShortestPaths.Keys.Count; while (LocationsProcessed.Count < LocationCount) { T _locationToProcess = default(T); //Search for the nearest location that isn't handled foreach (T _location in ShortestPaths.OrderBy(p => p.Value.Key).Select(p => p.Key).ToList()) { if (!LocationsProcessed.Contains(_location)) { if (ShortestPaths[_location].Key == Int32.MaxValue) return ShortestPaths.ToDictionary(k => k.Key, v => v.Value.Value); //ShortestPaths[destination].Value; _locationToProcess = _location; break; } } // foreach var _selectedPaths = Paths.Where(p => p.Source.Equals(_locationToProcess)); foreach (Path<T> path in _selectedPaths) { if (ShortestPaths[path.Destination].Key > path.Cost + ShortestPaths[path.Source].Key) { ShortestPaths.Set( path.Destination, path.Cost + ShortestPaths[path.Source].Key, ShortestPaths[path.Source].Value.Union(new Path<T>[] { path }).ToArray()); } } //Add the location to the list of processed locations LocationsProcessed.Add(_locationToProcess); } // while return ShortestPaths.ToDictionary(k => k.Key, v => v.Value.Value); //return ShortestPaths[destination].Value; } } }
Test
using System.Linq; using DijkstraAlgorithm; using FluentAssertions; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace UnitTestProject1 { [TestClass] public class UnitTest1 { [TestMethod] public void Calculate_A_to_D_given_AB_BC_CD__should_be__ABCD() { var Results = Engine.CalculateShortestPathBetween( "A", "D", new Path<string>[] { new Path<string>() { Source = "A", Destination = "B", Cost = 3 }, new Path<string>() { Source = "B", Destination = "C", Cost = 3 }, new Path<string>() { Source = "C", Destination = "D", Cost = 3 } }); Results.Sum(r => r.Cost).Should().Be(9); Results.Count.Should().Be(3); Results.First().Cost.Should().Be(3); Results.First().Source.Should().Be("A"); Results.First().Destination.Should().Be("B"); Results.Skip(1).First().Cost.Should().Be(3); Results.Skip(1).First().Source.Should().Be("B"); Results.Skip(1).First().Destination.Should().Be("C"); Results.Skip(2).First().Cost.Should().Be(3); Results.Skip(2).First().Source.Should().Be("C"); Results.Skip(2).First().Destination.Should().Be("D"); } [TestMethod] public void Calculate_A_to_D_given_AB_BC_CD_DE__should_be__ABCD() { var Results = Engine.CalculateShortestPathBetween( "A", "D", new Path<string>[] { new Path<string>() { Source = "A", Destination = "B", Cost = 3 }, new Path<string>() { Source = "B", Destination = "C", Cost = 3 }, new Path<string>() { Source = "C", Destination = "D", Cost = 3 }, new Path<string>() { Source = "D", Destination = "E", Cost = 3 } }); Results.Sum(r => r.Cost).Should().Be(9); Results.Count.Should().Be(3); Results.First().Cost.Should().Be(3); Results.First().Source.Should().Be("A"); Results.First().Destination.Should().Be("B"); Results.Skip(1).First().Cost.Should().Be(3); Results.Skip(1).First().Source.Should().Be("B"); Results.Skip(1).First().Destination.Should().Be("C"); Results.Skip(2).First().Cost.Should().Be(3); Results.Skip(2).First().Source.Should().Be("C"); Results.Skip(2).First().Destination.Should().Be("D"); } [TestMethod] public void Calculate_A_to_D_given_AB_AC_AD_AE_BC_CD_DE__should_be__ACD() { var Results = Engine.CalculateShortestPathBetween( "A", "D", new Path<string>[] { new Path<string>() { Source = "A", Destination = "B", Cost = 3 }, new Path<string>() { Source = "A", Destination = "C", Cost = 3 }, new Path<string>() { Source = "A", Destination = "D", Cost = 7 }, // set this just above ABC (3+3=6) new Path<string>() { Source = "A", Destination = "E", Cost = 3 }, new Path<string>() { Source = "B", Destination = "C", Cost = 3 }, new Path<string>() { Source = "C", Destination = "D", Cost = 3 }, new Path<string>() { Source = "D", Destination = "E", Cost = 3 } }); Results.Sum(r => r.Cost).Should().Be(6); Results.Count.Should().Be(2); Results.First().Cost.Should().Be(3); Results.First().Source.Should().Be("A"); Results.First().Destination.Should().Be("C"); Results.Skip(1).First().Cost.Should().Be(3); Results.Skip(1).First().Source.Should().Be("C"); Results.Skip(1).First().Destination.Should().Be("D"); } [TestMethod] public void Calculate_A_to_D_given_AB_AC_AD_AE_BC_CD_DE__should_be__AD() { var Results = Engine.CalculateShortestPathBetween( "A", "D", new Path<string>[] { new Path<string>() { Source = "A", Destination = "B", Cost = 3 }, new Path<string>() { Source = "A", Destination = "C", Cost = 3 }, new Path<string>() { Source = "A", Destination = "D", Cost = 5 }, // set this just below ABC (3+3=6) new Path<string>() { Source = "A", Destination = "E", Cost = 3 }, new Path<string>() { Source = "B", Destination = "C", Cost = 3 }, new Path<string>() { Source = "C", Destination = "D", Cost = 3 }, new Path<string>() { Source = "D", Destination = "E", Cost = 3 } }); Results.Sum(r => r.Cost).Should().Be(5); Results.Count.Should().Be(1); Results.Single().Cost.Should().Be(5); Results.Single().Source.Should().Be("A"); Results.Single().Destination.Should().Be("D"); } } }
You can find the code on GitHub.
Happy coding!