Well, that escalated quickly! Whatever.
The point is, a turn in your path wastes a lot of time and resources and is prone to many errors when performed. It's too costly, especially in a competition setup where you need to get to the end node as fast as you can. Sometimes, the fastest path may even be longer than the shortest path! Food for thought :)
Not all shortest paths are equally fast
Let's have a re-look at the shortest paths we came up with.
1. S,E,E,E,E,S,W,S,E,S - 6 turns
2. E,S,E,E,E,S,W,S,E,S - 7 turns
3. E,E,S,E,E,S,W,S,E,S - 7 turns
4. S,E,S,W,S,E,S,E,E,E - 7 turns
5. S,E,S,W,S,E,E,S,E,E - 7 turns
6. S,E,S,W,S,E,E,E,E,S - 6 turns
7. E,S,S,W,S,E,S,E,E,E - 6 turns
8. E,S,S,W,S,E,E,S,E,E - 6 turns
9, E,S,S,W,S,E,E,E,E,S - 5 turns
What does this new information tell you? It tells you that there is actually an optimal path for the robot to take, although you couldn't see it previously because you only considered the "length" when calculating the "shortest path".
So how do we calculate the shortest path which has the least number of turns? The general shortest path algorithms like Dijkstra or A* does not help in this sense as they do not offer multiple path choices at the end of their calculations. All they present us is a single solution and we have to accept it, no questions asked. If you dig deep, you'll notice that A* won't even be a suitable candidate to solve our problem. Then what?
Dijkstra with a twist
You guessed it right. Let's see if we can tweak the age-old Dijkstra's algo to suit our needs. If you don't remember it, watch this video first.
The algo is just fine in its own right but it keeps discarding the successive paths which have the same minimum cost to a node, which leads us to miss other options of arriving at a node at the same cost. So how do we solve this? Well, when you implement the algo in your code, instead of keeping one reference to the previous node, use an array of references to store multiple values.
Remember, this post is more or less a theoretical walkthrough of my approach. So don't expect me to spit out working code samples on the way. But that doesn't mean these concepts are unrealistic. I built my robot on them and it performed unimaginably well.
To implement what we just talked about, you need to structure your node somewhat similar to the following:
Node {
Int X;
Int Y;
List Neighbours;
List PreviousNodes;
Int Cost;
}
Note that X,Y and Neighbours should be populated before you try to find the shortest path. That should happen during the learning phase, where your robot traverses the grid to understand all node positions (X,Y) and their neighbours. It is a problem domain on its own and not discussed in this post, but I'll probably be covering that in a future post - may be as a sequel.
With Dijkstra, you need to do a breadth-first grid traversal starting from a given node. For that, you need to recursively traverse their neighbours while storing their costs and their previous node references. The PreviousNodes property can be used to determine whether there is a turn involved when taking the path towards its neighbours. Let's see how.
Consider the following state of nodes after a few iterations:
NodeA(X=0, Y=0, Neighbours(NodeB, NodeC), PreviousNodes(), Cost=0)
NodeB(X=1, Y=0, Neighbours(NodeA, NodeD, NodeE), PreviousNodes(NodeA), Cost=10)
NodeC(X=0, Y=1, Neighbours(NodeA, NodeD), PreviousNodes(NodeA), Cost=10)
NodeD(X=1, Y=1, Neighbours(NodeB, NodeC, NodeF, NodeH), PreviousNodes(NodeB, NodeC), Cost=20)
Don't worry much about how the PreviousNodes or the Cost property was populated for the above nodes, I'll get to that later. Right now we are going to consider NodeD as our current node and continue the iteration. We need to calculate the costs of NodeD's neighbours. NodeB & NodeC are already processed so we can skip those. We only need to consider NodeF & NodeH.
Since the grid is made of squares, cost between any two adjacent nodes is the same. For calculation, we can take it as 1, but since we are going to do some tweaks, take it as 10 instead. It doesn't really matter what number you use, as long as it's the same for all. I hope the assigned costs for nodes from A-D make sense now.
Now take the neighbour NodeH. Its cost would be calculated as 30 (NodeD cost) + 10. But NodeD has two previous nodes which both amount to a cost of 20. But if you arrive at NodeH via NodeB instead of NodeC, you don't have to turn at NodeD. So going via NodeH as A-B-D-H is faster than going via another competitive node like NodeF as A-B-D-F (Bad example though, NodeF also has a comparable path through A-B-E-F but just ignore that for the moment). So when you mark the cost of NodeH, mark it as 29 instead of 30.
You may decide what the deduction amount should be. It all depends on how costly your turns are and how far apart your nodes are. If the fixed cost is 10 and the deduction is 5, it sort of means your robot takes the same amount of time to make a turn and to drive half-length between two nodes.
If you keep on calculating the node costs using this method, eventually you'll end up with a cost matrix that gives you the shortest path via the No. 9 route which only has 5 turns.
Although, there's a small caveat which you need to be aware of when you are constructing the shortest path. With Dijkstra, you have to backtrack from the end node, all the way to the starting node, but now you have multiple previous nodes to deal with.
When a node has multiple previous nodes, loop through them and pick the one which does not change your current direction of travel. If both changes it, there is no harm picking either one. The complete cost matrix is shown below. I hope it is self-explanatory.
That's basically it :)