I've been assigned to use Dijkstra's algorithm in order to find the shortest path on real data, more specifically on Luxembourg's map.
The map is an .xml file with the following structure.
<nodes>
<node id="0" longitude="4963454" latitude="621476"/>
<node id="1" longitude="4959493" latitude="614350"/>
<node id="2" longitude="4959247" latitude="612096"/>
...
</nodes>
<arcs>
<arc from="0" to="40115" length="57"/>
<arc from="0" to="40114" length="13"/>
<arc from="1" to="16852" length="49"/>
...
</arcs>
I've got this .xml file alongside all the source files on this repository.
The purpose of the assignment is: given the drawn map, you have to click on 2 different screen locations and get the shortest path between the closest Nodes to those clicks.
Because the assignment wasn't that hard, I focused on elegance, performance, and reusability.
Here's a snippet of the final program.
Now let's dive in the code. I've come up with 4 interface-like classes which would let you make a truly custom Node class.
Earth.hpp
#pragma once
#include <cstdint>
#include "Point2D.hpp"
namespace Localizable
{
template <typename LatLongType = int32_t>
class Earth
{
public:
constexpr Earth(LatLongType latitude, LatLongType longitude) noexcept;
constexpr LatLongType latitude() const noexcept;
constexpr LatLongType longitutde() const noexcept;
protected:
Point2D<LatLongType> _location;
};
template <typename LatLongType>
inline constexpr Earth<LatLongType>::Earth(LatLongType latitude, LatLongType longitude) noexcept
: _location(latitude, longitude)
{}
template <typename LatLongType>
inline constexpr LatLongType Earth<LatLongType>::latitude() const noexcept
{
return _location.x;
}
template <typename LatLongType>
inline constexpr LatLongType Earth<LatLongType>::longitutde() const noexcept
{
return _location.y;
}
}
Screen.hpp
#pragma once
#include <cstdint>
#include "Point2D.hpp"
namespace Localizable
{
class Screen
{
public:
constexpr Screen(uint16_t x, uint16_t y) noexcept;
constexpr uint16_t x() const noexcept;
constexpr uint16_t y() const noexcept;
protected:
Point2D<uint16_t> _location;
};
inline constexpr Screen::Screen(uint16_t x, uint16_t y) noexcept
: _location(x, y)
{}
inline constexpr uint16_t Screen::x() const noexcept
{
return _location.x;
}
inline constexpr uint16_t Screen::y() const noexcept
{
return _location.y;
}
}
Associable.hpp
#pragma once
#include <vector>
#include <memory>
template <typename KeyType>
class Node;
template <typename KeyType>
class Associable
{
protected:
std::vector<std::weak_ptr<Node<KeyType>>> _neighbours;
using NeighboursIterator = decltype(std::cbegin(_neighbours));
public:
void add(std::weak_ptr<Node<KeyType>> neighbour);
void remove(std::weak_ptr<Node<KeyType>> neighbour);
constexpr std::pair<NeighboursIterator, NeighboursIterator> getNeighbours() const noexcept
{
return std::make_pair(std::cbegin(_neighbours), std::cend(_neighbours));
}
};
template<typename KeyType>
inline void Associable<KeyType>::add(std::weak_ptr<Node<KeyType>> neighbour)
{
_neighbours.push_back(neighbour);
}
template<typename KeyType>
inline void Associable<KeyType>::remove(std::weak_ptr<Node<KeyType>> neighbour)
{
auto it = std::find(std::begin(_neighbours), std::end(_neighbours), neighbour);
if (it != std::end(_neighbours))
_neighbours.erase(it);
}
Identifiable.hpp
#pragma once
#include <cstdint>
template <typename IdentifierType = uint16_t>
class Identifiable
{
public:
constexpr explicit Identifiable(IdentifierType identifier) noexcept;
constexpr IdentifierType key() const noexcept;
protected:
IdentifierType _key;
};
template<typename IdentifierType>
inline constexpr Identifiable<IdentifierType>::Identifiable(IdentifierType identifier) noexcept :
_key(identifier)
{
}
template<typename IdentifierType>
inline constexpr IdentifierType Identifiable<IdentifierType>::key() const noexcept
{
return _key;
}
Having all of these defined, making the custom Node class for the assignment was easy:
Node.hpp
#pragma once
#include "Earth.hpp"
#include "Identifiable.hpp"
#include "Associable.hpp"
#include "Screen.hpp"
#include "Utilities.hpp"
template <typename KeyType = uint16_t>
class Node : public Localizable::Earth<int32_t>, public Identifiable<KeyType>, public Associable<KeyType>, public Localizable::Screen
{
public:
constexpr Node(KeyType key, int32_t latitude, int32_t longitutde) noexcept;
};
template<typename KeyType>
inline constexpr Node<KeyType>::Node(KeyType key, int32_t latitude, int32_t longitutde) noexcept :
Identifiable<KeyType>(key),
Localizable::Earth<int32_t>(latitude, longitutde),
Localizable::Screen(Utilities::getScreenCoordinates(1280, 1020, latitude, longitutde)),
Associable<KeyType>()
{
}
namespace std
{
template<typename KeyType> struct hash<Node<KeyType>>
{
std::size_t operator()(const Node<KeyType>& node) const noexcept
{
return std::hash<KeyType>{}(node.key());
}
};
template <typename KeyType> struct equal_to<Node<KeyType>>
{
constexpr bool operator()(const Node<KeyType>& lhs, const Node<KeyType>& rhs) const noexcept
{
return lhs.key() == rhs.key();
}
};
}
template <typename KeyType>
constexpr bool operator == (const Node<KeyType>& lhs, const Node<KeyType>& rhs) noexcept
{
return lhs.key() == rhs.key();
}
template <typename KeyType>
constexpr bool operator != (const Node<KeyType>& lhs, const Node<KeyType>& rhs) noexcept
{
return lhs.key() != rhs.key();
}
Then I've integrated my Node class in the Graph class.
Graph.hpp
#pragma once
#include <unordered_map>
#include <vector>
#include "Node.hpp"
#include "Point2D.hpp"
class GraphFactory;
namespace std
{
template <> struct hash<std::pair<uint16_t, uint16_t>>
{
std::size_t operator ()(const std::pair<uint16_t, uint16_t>& pair) const noexcept
{
return std::hash<uint16_t>{}(pair.first) + std::hash<uint16_t>{}(pair.second);
}
};
}
namespace ShortestPath
{
template <typename KeyType>
class Dijkstra;
}
/**
* This Graph implements the Node class using an uint16_t KeyType.
* Feel free to use any other data type for KeyType on your graph.
*/
template <typename KeyType = uint16_t>
class Graph
{
private:
std::unordered_map<KeyType, std::shared_ptr<Node<KeyType>>> _keyToNode;
std::unordered_map<std::pair<KeyType, KeyType>, uint16_t> _weights;
std::unordered_map<Point2D<uint16_t>, std::weak_ptr<Node<KeyType>>> _coordinatesToNode;
using WeightsIterator = decltype(std::cbegin(_weights));
using NodesIterator = decltype(std::cbegin(_keyToNode));
public:
friend class GraphFactory;
template <typename KeyType>
friend class ShortestPath::Dijkstra;
public:
constexpr std::pair<NodesIterator, NodesIterator> nodes() const noexcept
{
return std::make_pair(std::cbegin(_keyToNode), std::cend(_keyToNode));
}
constexpr std::pair<WeightsIterator, WeightsIterator> arcs() const noexcept
{
return std::make_pair(std::cbegin(_weights), std::cend(_weights));
}
std::shared_ptr<Node<KeyType>> operator [](KeyType key) const noexcept;
std::weak_ptr<Node<KeyType>> operator [](Point2D<uint16_t> coordinates) const noexcept;
uint16_t cost(const std::pair<KeyType, KeyType>& keys) const;
protected:
template <typename InputKeyMappingIterator, typename InputWeightingIterator, typename CoordinatesMapping>
Graph(InputKeyMappingIterator keysFirst, InputKeyMappingIterator keysLast,
InputWeightingIterator weightsFirst, InputWeightingIterator weightsLast,
CoordinatesMapping coordsFirst, CoordinatesMapping coordsLast);
};
template<typename KeyType>
std::shared_ptr<Node<KeyType>> Graph<KeyType>::operator[](KeyType key) const noexcept
{
auto it = _keyToNode.find(key);
return it != std::end(_keyToNode) ? it->second : nullptr;
}
template<typename KeyType>
inline std::weak_ptr<Node<KeyType>> Graph<KeyType>::operator[](Point2D<uint16_t> coordinates) const noexcept
{
auto it = _coordinatesToNode.find(coordinates);
return it != std::end(_coordinatesToNode) ? it->second : std::weak_ptr<Node<KeyType>>();
}
template<typename KeyType>
inline uint16_t Graph<KeyType>::cost(const std::pair<KeyType, KeyType>& keys) const
{
const auto&[firstKey, secondKey] = keys;
if (auto it = _weights.find(std::make_pair(firstKey, secondKey)); it != std::end(_weights))
return it->second;
if (auto it = _weights.find(std::make_pair(secondKey, firstKey)); it != std::end(_weights))
return it->second;
static constexpr uint16_t kDefaultValue = 0;
return kDefaultValue;
}
template<typename KeyType>
template<typename InputKeyMappingIterator, typename InputWeightingIterator, typename CoordinatesMapping>
inline Graph<KeyType>::Graph(InputKeyMappingIterator keysFirst, InputKeyMappingIterator keysLast,
InputWeightingIterator weightsFirst, InputWeightingIterator weightsLast,
CoordinatesMapping coordsFirst, CoordinatesMapping coordsLast) :
_keyToNode(keysFirst, keysLast),
_weights(weightsFirst, weightsLast),
_coordinatesToNode(coordsFirst, coordsLast)
{
}
The reason I'm returning a pair of iterators instead of the whole container is because of encapsulation and abstraction reasons. Why would anyone care that the Graph class is using a HashTable to retain the nodes?
The final code snippet I'm exposing is the trivial Dijkstra implementation.
#pragma once
#include "Graph.hpp"
#include <queue>
#include <stdexcept>
namespace ShortestPath
{
template <typename KeyType = uint16_t>
class Dijkstra
{
private:
std::vector<Node<KeyType>> _path;
public:
explicit Dijkstra(const Graph<KeyType>& graph, const std::shared_ptr<Node<KeyType>>& source,
const std::shared_ptr<Node<KeyType>>& destination);
constexpr std::pair<decltype(std::cbegin(_path)), decltype(std::cend(_path))> path() const noexcept
{
return std::make_pair(std::cbegin(_path), std::cend(_path));
}
private:
void computeShortestPath(const Graph<KeyType>& graph);
void populatePath() noexcept;
const std::weak_ptr<Node<KeyType>> _source;
const std::weak_ptr<Node<KeyType>> _destination;
std::unordered_map<Node<KeyType>, Node<KeyType>> _cameFrom;
std::unordered_map<Node<KeyType>, uint16_t> _cost;
};
template<typename KeyType>
inline Dijkstra<KeyType>::Dijkstra(const Graph<KeyType>& graph,
const std::shared_ptr<Node<KeyType>>& source,
const std::shared_ptr<Node<KeyType>>& destination) :
_source(source),
_destination(destination)
{
computeShortestPath(graph);
populatePath();
}
template<typename KeyType>
inline void Dijkstra<KeyType>::computeShortestPath(const Graph<KeyType> & graph)
{
using Location = std::pair<Node<KeyType>, uint16_t>;
auto compareLocations = [](const Location& lhs, const Location& rhs)
{
return lhs.second > rhs.second;
};
std::priority_queue<Location, std::vector<Location>, decltype(compareLocations)> discoveredNodes(compareLocations);
static constexpr uint16_t kDefaultDistanceCost = 0;
discoveredNodes.push(std::make_pair(*_source.lock(), kDefaultDistanceCost));
_cameFrom.insert(std::make_pair(*_source.lock(), *_source.lock()));
_cost[*_source.lock()] = kDefaultDistanceCost;
while (!discoveredNodes.empty())
{
Location currentLocation = discoveredNodes.top();
discoveredNodes.pop();
if (auto destination = _destination.lock();
currentLocation.first.key() == destination->key())
return;
auto locationFromGraph = graph[currentLocation.first.key()];
auto[first, last] = locationFromGraph->getNeighbours();
for (; first != last; ++first)
{
auto neighbour = *((*first).lock());
uint16_t newCost = _cost[currentLocation.first] +
graph.cost(std::make_pair(currentLocation.first.key(), neighbour.key()));
if (_cost.find(neighbour) == std::end(_cost) || newCost < _cost[neighbour])
{
_cost[neighbour] = newCost;
_cameFrom.insert(std::make_pair(neighbour, currentLocation.first));
discoveredNodes.push(std::make_pair(neighbour, newCost));
}
}
}
throw std::domain_error("No path was found.\n");
}
template<typename KeyType>
inline void Dijkstra<KeyType>::populatePath() noexcept
{
Node<KeyType> currentNode = *_destination.lock();
const Node<KeyType> sourceNode = *_source.lock();
while (currentNode != sourceNode)
{
_path.push_back(currentNode);
currentNode = _cameFrom.at(currentNode);
}
_path.push_back(sourceNode);
}
}
All in all, what do you think?
Did I use smart pointers properly?
std::pair
. Only Earth and Screen seem to be in a namespace; why only them? Weak pointer is usually used to break circular dependencies, is that why you are using it here? What's the point ofIdentifiable
? I think you can simplify this to Graph and Node. \$\endgroup\$