Click here to Skip to main content
15,892,059 members
Articles / Web Development / HTML
Article

Pathfinding Algorithms in C#

Rate me:
Please Sign up or sign in to vote.
4.99/5 (120 votes)
3 Apr 2023CPOL4 min read 243.7K   9.8K   232   77
A comparison of Dijkstra and Astar

Unzip and open solution in Visual Studio 2022

Image

Introduction

Have you ever wondered how GPS applications calculate the fastest way to a chosen destination? As you will see, it is actually quite simple.

This article explains this and provides sample code that you are free to use as you like. The article also compares two common basic algorithms, Dijkstra and A*.

The Problem

Let’s say you have a map. You know where you are and where you want to go. The map has roads (they are called edges) that connect the nodes (places with coordinates).

From every node, you can go to one or many edges. An edge has a cost (e.g. length or time it takes to travel it).
For small maps, one could perhaps calculate all possible routes to the destination and select the shortest. But that is not very practical for maps with many nodes as the combinations grow exponentially.

Dijkstra

The Dijkstra algorithm was discovered in 1959 by Edsger Dijkstra. This is how it works:

  1. From the start node, add all connected nodes to a priority queue.
  2. Sort the priority queue by lowest cost and make the first node the current node.
    For every child node, select the best that leads to the shortest path to start.
    When all edges have been investigated from a node, that node is "Visited" and you don´t need to go there again.
  3. Add each child node connected to the current node to the priority queue.
  4. Go to step 2 until the queue is empty.
  5. Recursively create a list of each nodes node that leads the shortest path from end to start.
  6. Reverse the list and you have found the shortest path

In other words, recursively for every child of a node, measure its distance to the start. Store the distance and what node led to the shortest path to start. When you reach the end node, recursively go back to the start the shortest way, reverse that list and you have the shortest path.

Below is my Dijkstra Algorithm implementation in C# code. It might be easier to understand than the above.

C#
public List<Node> GetShortestPathDijkstra()
{
    DijkstraSearch();
    var shortestPath = new List<Node>();
    shortestPath.Add(End);
    BuildShortestPath(shortestPath, End);
    shortestPath.Reverse();
    return shortestPath;
}

private void BuildShortestPath(List<Node> list, Node node)
{
    if (node.NearestToStart == null)
        return;
    list.Add(node.NearestToStart);
    BuildShortestPath(list, node.NearestToStart);
}

private void DijkstraSearch()
{
    Start.MinCostToStart = 0;
    var prioQueue = new List<Node>();
    prioQueue.Add(Start);
    do {
        prioQueue = prioQueue.OrderBy(x => x.MinCostToStart).ToList();
        var node = prioQueue.First();
        prioQueue.Remove(node);
        foreach (var cnn in node.Connections.OrderBy(x => x.Cost))
        {
            var childNode = cnn.ConnectedNode;
            if (childNode.Visited)
                continue;
            if (childNode.MinCostToStart == null ||
                node.MinCostToStart + cnn.Cost < childNode.MinCostToStart)
            {
                childNode.MinCostToStart = node.MinCostToStart + cnn.Cost;
                childNode.NearestToStart = node;
                if (!prioQueue.Contains(childNode))
                    prioQueue.Add(childNode);
            }
        }
        node.Visited = true;
        if (node == End)
            return;
    } while (prioQueue.Any());
}

Dijkstra

This is a randomly generated map in my test program. The dots are nodes and between them are lines which represent edges. This map consists of 5000 nodes and 15000 edges.

Lighter colored dots are visited by the search algorithm and the best path is drawn in green.

A* Algorithm

There are many improvements of Dijkstra’s algorithm. One of the most common is called A*. It is basically the same as Dijkstra with one simple modification.

Edges are prioritized also with respect to how much closer that edge leads to a straight-line distance to the goal. So before running an A* search, the straight-line distance to the final destination has to be measured for every node, which is easy if you know each nodes coordinate. This is the simplest form of A* and its definition also allows for improvments of the heuristics function. (In this case StraightLineDistanceToEnd)

This algorithm has a big performance advantage since it does not need to visit as many nodes when the direction of the path end is known.

See my implementation below. In bold what is added to Dijkstra’s Algorithm.

C#
public List<Node> GetShortestPathAstar()
{
    foreach (var node in Map.Nodes)
        node.StraightLineDistanceToEnd = node.StraightLineDistanceTo(End);
    AstarSearch();
    var shortestPath = new List<Node>();
    shortestPath.Add(End);
    BuildShortestPath(shortestPath, End);
    shortestPath.Reverse();
    return shortestPath;
}

private void AstarSearch()
{
    Start.MinCostToStart = 0;
    var prioQueue = new List<Node>();
    prioQueue.Add(Start);
    do {
        prioQueue = prioQueue.OrderBy(x => x.MinCostToStart + x.StraightLineDistanceToEnd).ToList();
        var node = prioQueue.First();
        prioQueue.Remove(node);
        NodeVisits++;
        foreach (var cnn in node.Connections.OrderBy(x => x.Cost))
        {
            var childNode = cnn.ConnectedNode;
            if (childNode.Visited)
                continue;
            if (childNode.MinCostToStart == null ||
                node.MinCostToStart + cnn.Cost < childNode.MinCostToStart)
            {
                childNode.MinCostToStart = node.MinCostToStart + cnn.Cost;
                childNode.NearestToStart = node;
                if (!prioQueue.Contains(childNode))
                    prioQueue.Add(childNode);
            }
        }
        node.Visited = true;
        if (node == End)
            return;
    } while (prioQueue.Any());
}

Astar

This is the same map as above, but the path is calculated with A* algorithm. As you can see, there are much less nodes that needs to be visited.

Results

When running both algorithms on the same map of 500,000 nodes, I get these results.

  Dijkstra A*
Visited nodes 330,871 19,410
Time to calculate (ms) 850 127
Cost of best path 14.3 14.3
Distance of shortest path 0.824 0.824

As you can see in the table above, A* algorithm is about 7 times faster than Dijkstra, and they both find the shortest path and same lowest cost. In any case the A* algorith should be the best choice.

On a real map, the shortest path isn’t always the best. Driving on roads with higher speed limit will probably take you to your destination sooner. That is why adding a random number to the cost of an edge makes this experiment more realistic.

The larger the map is compared to how many nodes lay between start and finish the more the Time to calculate shortest path will decrease using A*.

Conclusion

So what algorithm is the best path finding algorithm of Dijkstra and A*?
I’d say it is A*. It will always give a faster search, and it gives the same result as Dijkstra.

Thanks for reading and I hope you find path finding algorithms are as much fun as I do by now.

Have a nice day!

History

  • December 10, 2017 - Version 1.0
  • December 20, 2017 - Version 1.0.1
    • Minor spelling fixes
  • Januari 13, 2018 - Version 1.0.2
    • Clarified that A* can be improved.
  • April 3, 2023 - Version 1.1
    • Changed my conclusion. Thanks readers.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
Sweden Sweden
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralMy vote of 5 Pin
GerVenson19-Dec-17 11:38
professionalGerVenson19-Dec-17 11:38 
GeneralRe: My vote of 5 Pin
KristianEkman20-Dec-17 6:52
KristianEkman20-Dec-17 6:52 
GeneralRe: My vote of 5 Pin
GerVenson20-Dec-17 23:06
professionalGerVenson20-Dec-17 23:06 
GeneralMy vote of 5 Pin
Dennis Dykstra19-Dec-17 8:53
Dennis Dykstra19-Dec-17 8:53 
GeneralRe: My vote of 5 Pin
KristianEkman19-Dec-17 9:04
KristianEkman19-Dec-17 9:04 
QuestionImprove A* heuristic to find lowest cost path Pin
Member 1046459619-Dec-17 8:36
Member 1046459619-Dec-17 8:36 
AnswerRe: Improve A* heuristic to find lowest cost path Pin
KristianEkman19-Dec-17 9:03
KristianEkman19-Dec-17 9:03 
GeneralRe: Improve A* heuristic to find lowest cost path Pin
Member 1046459619-Dec-17 21:35
Member 1046459619-Dec-17 21:35 
Navigation software was one of the projects I had to do in one of my minors in college. I happened to be the one responsible for implementing A* in that project Smile | :)

Well, as far as I see it, there are basically two things to consider in your code. One is as I mentioned the fact that you base a lot of what you do on the physical distance. In order to always determine the fastest path (least cost), we need to assign a weight to the end distance. Your GetShortestPathAstart method could be adjusted to do this:

C#
public List GetShortestPathAstart(Func<Node, Node, float> weightToEnd)
{
    foreach (Node node in Map.Nodes)
        node.WeightToEnd = weightToEnd(node, End);
    AstarSearch();
    var shortestPath = new List();
    shortestPath.Add(End);
    BuildShortestPath(shortestPath, End);
    shortestPath.Reverse();
    return shortestPath;
}


Then you could call this method with:
C#
GetShortestPathAstart((nodeToCalculate, endNode) => nodeToCalculate.StraightLineDistanceTo(endNode));

This would yield the same results that you've had thusfar, still using the straightline distance. But now, you could also do:
C#
GetShortestPathAstart((nodeToCalculate, endNode) => nodeToCalculate.HeuristicCostTo(endNode));

This will allow you to calculate based on the preferred priority. Of course, a similar adjustment would then be required in AstarSearch, so that the WeightFromStart and WeightToEnd both reflect the same unit and are thus proper to combine.

So that is the first thing I'd consider when looking at possible improvements here.

The other thing is that, in a way, you are still visiting every node at least once. You are assigning the weightToEnd for every node in the map. You've already shown that the algorithm will never visit most of the nodes on the map. As such, I would calculate the weightToEnd in the AstarSearch method instead. I also wouldn't put any calculations in the OrderBy for the List. I understand why you would simply use the List that C# provides, but for quite a few nodes in that List you are going to be doing the same calculation over and over again. Performance-wise, not great.

So, taking into account performance in the sort, enabling different priorities in calculation and performance in the weightcalculations, I would come up with this:

C#
private void AstarSearch(Func<Node, Connection, float> weightFromStart, Func<Node, Node, float> weightToEnd)
{
    Start.MinimalWeight = weightToEnd(Start, End);
	Start.WeightFromStart = 0;
	
    var prioQueue = new List<Node>();
    prioQueue.Add(Start);
    do
    {
		// don't use the linq functions, you don't need them
		prioQueue.Sort((node1, node2) => node1.MinimalWeight.CompareTo(node2.MinimalWeight));
		// now that the queue is resorted, get the first item (using Linq now for safety, will get null if queue is empty)
		var node = FirstOrDefault();
		// if we don't get an item, then there is no route (start or end is in isolated part of the graph)
		if(node != null)
		{
			// removing the position will eliminate the need to search for the item before removal, we already know where it is
			prioQueue.RemoveAt(0);;
		
			NodeVisits++;
			// no need to order the connections by cost, we want all of them anyway
			foreach (var cnn in node.Connections)
			{
				var childNode = cnn.ConnectedNode;
				if (childNode.Visited)
					continue;
				// calculate both required values now, you need them anyway, this calculates them only once
				float weightTillNode = weightFromStart(node, cnn);
				float minimalweight = weightTillNode + weightToEnd(childNode, End);
				if (childNode.MinimalWeight == null ||
					minimalweight < childNode.MinimalWeight)
				{
					childNode.MinimalWeight = minimalweight;
					childNode.WeightFromStart = weightTillNode;
					childNode.NearestToStart = node;
					if (!prioQueue.Contains(childNode))
						prioQueue.Add(childNode);
				}
			}
			node.Visited = true;
			if (node == End)
				return;
		}
    } while (prioQueue.Any());
}


Now the weightToEnd is considered only for nodes that you want to evaluate altogether and you are capable of determining shortest path and fastest path, using the same algorithm.

One final thing to consider (though that is not in the code you present), in order to speed things up, you do not need to calculate the real distance in your weightToEnd. Calculating the square root to get the real distance is expensive. Instead, use the square of the distance you are already having and just square the distance you get from the connection instead, it's a lot lighter.
GeneralRe: Improve A* heuristic to find lowest cost path Pin
KristianEkman19-Dec-17 21:56
KristianEkman19-Dec-17 21:56 
GeneralMy vote of 5 Pin
Balachandar Jeganathan19-Dec-17 6:55
professionalBalachandar Jeganathan19-Dec-17 6:55 
GeneralRe: My vote of 5 Pin
KristianEkman19-Dec-17 7:46
KristianEkman19-Dec-17 7:46 
GeneralPractical realization Pin
Dmitriy Gakh18-Dec-17 7:09
professionalDmitriy Gakh18-Dec-17 7:09 
GeneralRe: Practical realization Pin
KristianEkman18-Dec-17 7:21
KristianEkman18-Dec-17 7:21 
GeneralMy vote of 5 Pin
Dmitriy Gakh18-Dec-17 6:53
professionalDmitriy Gakh18-Dec-17 6:53 
GeneralRe: My vote of 5 Pin
KristianEkman18-Dec-17 7:21
KristianEkman18-Dec-17 7:21 
Generalnice information Pin
antony philip17-Dec-17 22:31
professionalantony philip17-Dec-17 22:31 
GeneralRe: nice information Pin
KristianEkman18-Dec-17 22:47
KristianEkman18-Dec-17 22:47 
QuestionCould you do a comparison? Pin
Member 1307923015-Dec-17 2:59
Member 1307923015-Dec-17 2:59 
AnswerRe: Could you do a comparison? Pin
KristianEkman15-Dec-17 3:10
KristianEkman15-Dec-17 3:10 
GeneralMy vote of 5 Pin
Paw Jershauge14-Dec-17 20:12
Paw Jershauge14-Dec-17 20:12 
GeneralRe: My vote of 5 Pin
KristianEkman15-Dec-17 3:12
KristianEkman15-Dec-17 3:12 
GeneralMy vote of 5 Pin
Robert_Dyball14-Dec-17 11:00
professionalRobert_Dyball14-Dec-17 11:00 
GeneralRe: My vote of 5 Pin
KristianEkman15-Dec-17 3:13
KristianEkman15-Dec-17 3:13 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.