The objective of the post is to improve design and improve command on C++. Given a map, start point and end point, the shortest path to end point from the start point has to be found out using Astar (A*) algorithm. The algorithm uses an evaluation function f for each point or node in the map. The f depends on g and h. The g is distance from start point to the point under consideration(current point in the path). The h is a heuristic which provides an (under)estimate of the distance from current point to end point. The frontier is a set that contains all candidate points to form the path. The pseudocode/ algorithm:
1) The values of f, g and h for all points are initially set to infinity (a very high value);
2) The start point is assigned a g of 0 and its h is calculated using Manhattan distance. This is then inserted to the frontier. The end point is assigned an h of 0.
3) Until frontier is empty, do:
a) Extract the point (current point) with lowest f value from the frontier.
b) Check if it is the end point. If yes, report success. Else, continue.
c) Collect all the eligible neighbors of the current point and add them to the frontier. During this addition, their f, g, h and parent are updated.
d) Remove the current point from frontier.
4) If success is not reported in (3), report failure.
Here is the commented code:
#include <iostream>
#include <vector>
#include <stdexcept>
#include <set>
#include <algorithm>
// Class to handle individual nodes/ points/ location in the environment map
class Point
{
// The x coordinate of the point
int x_v = {-1};
// The y coordinate of the point
int y_v = {-1};
// The value at the point; either 1 or 0
int val_v = {0};
// The total estimated cost of a point; A star uses this value
double f_v = {100000};
// The cost to reach a point; A star uses this value
double g_v = {100000};
// The estimate of cost (heuristic) to reach end from current point; A star uses this value
double h_v = {100000};
// The parent of Point set by Astar so that path from start to end can be retrieved
Point* parent_v = nullptr;
public:
Point()
{}
Point(int xx, int yy, int vv) : x_v{xx}, y_v{yy}, val_v{vv}
{}
// Copy constructor
Point(const Point& p1)
{
x_v = p1.x();
y_v = p1.y();
val_v = p1.val();
f_v = p1.f();
g_v = p1.g();
h_v = p1.h();
parent_v = p1.parent();
}
~Point(){}
int val() const
{
return val_v;
}
int x() const
{
return x_v;
}
int y() const
{
return y_v;
}
double f() const
{
return f_v;
}
double g() const
{
return g_v;
}
double h() const
{
return h_v;
}
Point* parent() const
{
return parent_v;
}
void set_g(double g)
{
g_v = g;
f_v = g_v + h_v;
}
void set_h(double h)
{
h_v = h;
f_v = g_v + h_v;
}
void set_parent(Point* p)
{
parent_v = p;
}
// Assignment operator
Point& operator=(const Point& p1)
{
x_v = p1.x();
y_v = p1.y();
val_v = p1.val();
f_v = p1.f();
g_v = p1.g();
h_v = p1.h();
parent_v = p1.parent();
return *this;
}
//This operator has been defined so that std::set can use it as comparison object
friend bool operator<(const Point& p1, const Point& p2)
{
if(p1.x() < p2.x())
{
return true;
}
if(p1.x() == p2.x() && p1.y() < p2.y())
{
return true;
}
return false;
}
friend bool operator==(const Point& p1, const Point& p2)
{
return (p1.x() == p2.x()) && (p1.y() == p2.y());
}
friend bool operator!=(const Point& p1, const Point& p2)
{
return !(p1 == p2);
}
};
// Class to perform A star
class Astar
{
// The map of the environment
std::vector<std::vector<Point>> map_v;
// The size of the map
int map_x = {0};
int map_y = {0};
// The start and end points
Point* start_v;
Point* end_v;
// The variable to store path from start to end
std::vector<Point*> path_v;
public:
Astar(std::vector<std::vector<int>>&, std::pair<int, int>&, std::pair<int, int>&);
bool is_valid(int, int);
double manhattan(Point*);
bool search();
std::vector<Point*> path();
};
// Constructor that takes in map, start and end from the user/ main and converts it into variables of the class
Astar::Astar(std::vector<std::vector<int>>& map, std::pair<int, int>& start, std::pair<int, int>& end)
{
// Check and note down sizes
map_y = map.size();
if(map_y)
{
map_x = map[0].size();
}
if(map_x == 0 || map_y == 0)
{
throw std::invalid_argument{"The map is invalid!\n"};
}
// Create a map of Points
for(int i = 0; i < map_y; i++)
{
map_v.push_back(std::vector<Point>(map_x));
for(int j = 0; j < map_x; j++)
{
map_v[i][j] = Point(j, i, map[i][j]);
}
}
// Assign start and end
start_v = &map_v[start.first][start.second];
end_v = &map_v[end.first][end.second];
if(!is_valid(start_v -> x(), start_v -> y()))
{
throw std::invalid_argument{"Start point is invalid!\n"};
}
if(!is_valid(end_v -> x(), end_v -> y()))
{
throw std::invalid_argument{"End point is invalid!\n"};
}
}
// Check if a Point lies within boundaries of the map and if it is free space
bool Astar::is_valid(int x, int y)
{
if(x >= 0 && x < map_x && y >= 0 && y < map_y && map_v[y][x].val() == 0)
{
return true;
}
return false;
}
// Calculate Manhattan distance as a hueristic
double Astar::manhattan(Point* p)
{
return std::abs(p -> x() - end_v -> x()) + std::abs(p -> y() - end_v -> y());
}
// Perform the actual search
bool Astar::search()
{
// Create a frontier and insert the start node
std::set<Point*> frontier;
end_v -> set_h(0);
start_v -> set_g(0);
start_v -> set_h(this -> manhattan(start_v));
frontier.insert(start_v);
// As long as there are points in the frontier or until the end point is reached, the search continues
while(!frontier.empty())
{
// Find the Point with minimum value of f_v
auto curr_point = *(std::min_element(frontier.begin(), frontier.end(), [](const Point* p1, const Point* p2){return p1 -> f() < p2 -> f();}));
// If it is the end point, return success
if(*curr_point == *end_v)
{
std::cout << "Success!\n";
return true;
}
// Otherwise, find the eligible neighbors and insert them into frontier
int x = curr_point -> x();
int y = curr_point -> y();
std::vector<Point*> neighbors;
if(this -> is_valid(x, y - 1))
{
neighbors.push_back(&map_v[y - 1][x]);
}
if(this -> is_valid(x, y + 1))
{
neighbors.push_back(&map_v[y + 1][x]);
}
if(this -> is_valid(x + 1, y))
{
neighbors.push_back(&map_v[y][x + 1]);
}
if(this -> is_valid(x - 1, y))
{
neighbors.push_back(&map_v[y][x - 1]);
}
// Add neighbors to frontier if their g value is higher than necessary
// Update g, h (and f). Also set their parent.
for(auto& neighbor : neighbors)
{
if(neighbor -> g() > curr_point -> g() + 1)
{
neighbor -> set_g(curr_point -> g() + 1);
neighbor -> set_h(this -> manhattan(neighbor));
neighbor -> set_parent(curr_point);
frontier.insert(neighbor);
}
}
// Remove the current Point
frontier.erase(curr_point);
}
// If end point is not reached, report failure
std::cout << "Failure!\n";
return false;
}
// Retrieve the path and return it
std::vector<Point*> Astar::path()
{
auto p1 = end_v;
while(p1 != nullptr)
{
path_v.insert(path_v.begin(), p1);
p1 = p1 -> parent();
}
return path_v;
}
int main()
{
// Map of the environment to navigate
std::vector<std::vector<int>> mv = {{1, 0, 0, 1, 0, 0, 0, 0},
{0, 1, 0, 1, 1, 0, 0, 1},
{1, 0, 0, 0, 0, 0, 1, 0},
{0, 0, 1, 0, 0, 0, 0, 0},
{1, 0, 1, 0, 1, 1, 1, 1},
{0, 0, 0, 0, 0, 1, 0, 1},
{0, 0, 0, 0, 0, 1, 1, 0},
{0, 1, 0, 1, 0, 0, 0, 1}};
// The start and end points
std::pair<int, int> start = {5, 2};
std::pair<int, int> end = {2, 5};
// Create search object and perform search
Astar astar1{mv, start, end};
auto success = astar1.search();
// If search is successful, print the path
if(success)
{
auto path = astar1.path();
std::cout << "The result path: \n";
for(auto p : path)
{
std::cout << "{ " << p -> y() << ", " << p -> x() << "}" << "\t";
}
std::cout << "\n";
}
return 0;
}
1) In the above code, the user/ main provides input map via a std::vector<std::vector<int>> and it is converted to std::vector<std::vector<Point>>. The Point is a user defined class capable of handling a lot of operations. In the above code, the class Astar is responsible for conversion of the map into required type. Is this bad design? How can I can do it more elegantly without loosing the advantages of Point class? Suggestions for better data structures are welcome.
2) Comments on operator overloading, copy constructor etc. used in Point.
3) Are there any parts that are very inefficient? Any other suggestions, advice or improvements?