Code Review Stack Exchange is a question and answer site for peer programmer code reviews. Join them; it only takes a minute:

Sign up
Here's how it works:
  1. Anybody can ask a question
  2. Anybody can answer
  3. The best answers are voted up and rise to the top

I wanted to know if my A* algorithm is well-implemented and if it could be optimized in any way.

#include <iostream>
#include <cmath>
#include <list>
#include <vector>
#include <algorithm>

class Vector2
{
    int x, y;
public:
    Vector2(int _x, int _y) : x(_x), y(_y) {}
    Vector2() = default;
    Vector2 operator +(const Vector2& other) {
        Vector2 temp;
        temp.x = this->x + other.x;
        temp.y = this->y + other.y;
        return temp;
    }
    int getX() const { return x; }
    int getY() const { return y; }

    friend class Map;
};

struct Node
{
    Vector2 position;
    int G, H, F;
    Node* parent = nullptr;

    Node() = default;
    Node(const Node& other) = default;
    Node(Vector2 pos):position(pos) {};

    void calc(const Vector2& endPos) {
        H = static_cast<int>((abs(static_cast<double>(position.getX() - endPos.getX())) + abs(static_cast<double>(position.getY() - endPos.getY()))));
        G = parent ? parent->G + 1 : 1;
        F = G + H;
    }

    bool operator==(const Node& other) const {
        return (position.getX() == other.position.getX() && position.getY() == other.position.getY()); 
    }
    bool operator!=(const Node& other) const {
        return !(*this == other);
    }
    bool operator<(const Node& other)  const {
        return(F < other.F);
    }
};

class Map
{
    std::vector<char> data;
    int size;
public:
    Map() = default;
    Map(int _size) : size(_size) {
        data.resize(size * size);
        for (int i = 0; i < size * size; ++i) data[i] = '.';
    }
    void display() const{
        for (int i = 1; i <= size * size; ++i) {
            std::cout << data[i - 1] << " ";
            if (!(i % size)) std::cout << "\n";
        }
    }
    bool getIfInDanger(Vector2 position) const {
        if (position.y < 0) position.y = 0;
        if (position.x < 0) position.x = 0;
        if (position.y >= 20) position.y = size - 1;
        if (position.x >= 20) position.x = size - 1;
        return(data[position.getX() + (position.getY() * size)] == 'X');
    }
    void setElement(char&& asda, Vector2 position) {
        data[position.getX() + (position.getY() * size)] = asda;
    }
};

class Solver
{
    Vector2 startPos, endPos;
    std::vector<Vector2> directions;
    Map map;
public:
    Solver(Vector2 _startPos, Vector2 _endPos, int size) : startPos(_startPos), endPos(_endPos){
        Map temp(size);
        map = temp;

        map.setElement('X', Vector2(14, 15));
        map.setElement('X',Vector2(15,15));
        map.setElement('X', Vector2(16, 15));
        map.setElement('X', Vector2(16, 14));
        map.setElement('X', Vector2(16, 13));

        directions.resize(8);
        directions[0] = Vector2(-1, 1);
        directions[1] = Vector2(-1, 0);
        directions[2] = Vector2(-1, -1);
        directions[3] = Vector2(0, 1);
        directions[4] = Vector2(0, -1);
        directions[5] = Vector2(1, 1);
        directions[6] = Vector2(1, 0);
        directions[7] = Vector2(1, -1);
    }
    bool aStar() {
        Node startNode(startPos);
        Node goalNode(Vector2(endPos.getX(), endPos.getY()));

        if (map.getIfInDanger(startNode.position) || map.getIfInDanger(goalNode.position)) {
            std::cout << "Either the start of this map is obstructed or so is the end.";
            return false;
        }

        std::list<Node> openList;
        std::list<Node> closedList;

        startNode.calc(endPos);
        openList.push_back(startNode);

        while (!openList.empty()) {
            auto current = Node(*std::min_element(openList.begin(), openList.end()));

            current.calc(endPos);

            closedList.push_back(current);
            openList.remove(current);
            if (current == goalNode) break;

            for (auto& direction : directions) {
                Node successor(direction + current.position);

                if (map.getIfInDanger(successor.position) || successor.position.getX() > 20 - 1 || 
                    successor.position.getY() > 20 - 1 || successor.position.getX() < 0 || 
                    successor.position.getY() < 0 ||
                    std::find(closedList.begin(), closedList.end(), successor) != closedList.end()) {
                    continue;
                }

                successor.calc(endPos);

                auto inOpen = std::find(openList.begin(), openList.end(), successor);
                if (inOpen == openList.end()) {
                    successor.parent = &closedList.back();
                    successor.calc(endPos);

                    openList.push_back(successor);
                }
                else
                    if (successor.G < inOpen->G) successor.parent = &closedList.back();
            }
        }

        if (!openList.size()) {
            std::cout << "No path has been found\n";
            return false;
        }

        auto inClosed = std::find(closedList.begin(), closedList.end(), goalNode);
        if (inClosed != closedList.end()) {
            while (*inClosed != startNode) {
                map.setElement('Y',inClosed->position);
                *inClosed = *inClosed->parent;
            }
        }

        map.display();
        return true;
    }
};

int main()
{
    Solver solve(Vector2(0,0),Vector2(19,19), 20);
    solve.aStar();
}
share|improve this question

Avoid numbered names

class Vector2

What about this makes it a Vector2? It seems like it holds position data. Why not just call it Position? Alternately consider Vector2D, CartesianVector, or defining your own namespace, e.g. nonstandard::Vector to differentiate it (please find a better name than nonstandard though). Names should be descriptive if possible. The 2 doesn't tell me why you aren't just using a std::vector there.

Don't repeat yourself

        data.resize(size * size);
        for (int i = 0; i < size * size; ++i) data[i] = '.';

You calculate size * size on every iteration of the for loop and once previous. Why not just calculate it once? Even if the original would get optimized out by the compiler, this makes it clearer that the resize and the for loop are operating on the same value.

        std::size_t area = size * size;
        data.resize(area);
        for (std::size_t i = 0; i < area; ++i) {
            data[i] = '.';
        }

I also changed to size_t as being a better type for indexing an array. Note that this would make no functional difference in this case. It is a good habit though.

I changed from the statement form of the for loop to the block form. This can also be a good habit, both for readability and to avoid a bug where someone tries to put two statements into a for loop without the block notation.

        for (int i = 1; i <= size * size; ++i) {

Similar problem with a similar solution.

        for (std::size_t i = 1, area = size * size; i <= area; ++i) {

Another alternative is

        std::size_t i = 0;
        std::size_t area = size * size;
        while (i < area) {
            std::cout << data[i];
            ++i;
            std::cout << ((0 == i % size) ? "\n" : " ");
        }

This saves an unnecessary subtraction but may be less readable.

Note that it also saves adding an unnecessary space at the end of each line. I find that neater, but it's not a big deal either way.

A third version would be

        for (std::size_t i = 0, area = size * size; i < area; ++i) {
            for (std::size_t endOfRow = i + size - 1; i < endOfRow; ++i) {
                 std::cout << data[i] << " ";
            }

            std::cout << data[i] << "\n";
        }

This saves calculating the modulus on each iteration. Instead it calculates a sum of three values every size iterations. I'll leave it up to you whether this is more or less readable than the previous versions. It should be more efficient but not necessarily enough to matter.

Avoid magic values

        if (position.y < 0) position.y = 0;
        if (position.x < 0) position.x = 0;
        if (position.y >= 20) position.y = size - 1;
        if (position.x >= 20) position.x = size - 1;
        return(data[position.getX() + (position.getY() * size)] == 'X');

Why 20? Looking through the code, it's because you set size to 20. So why not say that?

        if (position.y < 0) {
            position.y = 0;
        }
        if (position.x < 0) {
            position.x = 0;
        }
        if (position.y >= size) {
            position.y = size - 1;
        }
        if (position.x >= size) {
            position.x = size - 1;
        }

        return data[getIndexFrom(position)] == 'X';

And now we need a getIndexFrom

    std::size_t getIndexFrom(Position p) const {
        return p.getX() + p.getY() * size;
    }

This not only reads easier to me, it avoids accidentally doing the conversion differently somewhere.

Consider a Priority Queue

        std::list<Node> openList;

Rather than store this in a std::list and use the \$O(n)\$ std::min every iteration, consider a std::priority_queue.

        std::priority_queue<Node> unexploredNodes;

Note that you may need to put the indexes in the queue rather than the Node values themselves to get a performance improvement.

        std::priority_queue<size_t, std::vector<size_t>, CompareNodes> unexploredNodes;

You'd have to benchmark all three versions to see which gives the best performance.

You also may want to consider writing your own priority queue implementation.

I also changed the name from openList to unexploredNodes as being more descriptive.

Save visited nodes in an array rather than a list

You store each visited node in closedList and use std::find to check if it's there. Instead, consider using a boolean array:

        bool *explored = new bool[size * size];
        for (auto& e : explored) {
            e = false;
        }
        explored[map.getIndexFrom(current.position)] = true;
            || explored[map.getIndexFrom(successor.position)]) {
        delete(explored);

This turns an \$O(n)\$ std::find into a constant time array access.

Bug

        if (!openList.size()) {
            std::cout << "No path has been found\n";
            return false;
        }

I'd prefer

        if (openList.empty()) {

as more readable, but that's not the bug.

What if the last open path is the correct one? Then openList will have a size of zero and it will say that no path has been found. You can fix this by changing

            openList.remove(current);
            if (current == goalNode) break;

to

            if (current == goalNode) {
                goalReached == true;
                break;
            }
            openList.remove(current);

Now the openList is no longer empty.

Or you can test on goalReached instead of the state of the list. This is less reliant on the mechanics of the search.

Consider using function returns to replace loop control

Rather than using a tracking variable or relying on the structure of the search, consider using another function to do the actual search. Then you can replace the loop with

            if (findPath(goal, unexploredNodes, explored)) {
                if (explored[map.getIndexFrom(goalNode)]) {
                    auto parent = &goalNode
                    while (*parent != startNode) {
                        map.setElement('Y', parent->position);
                        parent = parent->parent;
                    }
                }

                map.display();
                return true;
            }
            else {
                std::cout << "No path has been found\n";
                return false
            }

Note that I changed the incorrect inClosed to parent. This makes more sense for everything but the first element in the original algorithm.

And you have to create a function findPath

        template <typename T, typename U, typename V>
        bool findPath(const Position& goal, std::priority_queue<T, U, V>& unexploredNodes, bool *explored) {
            while (!unexploredNodes.empty()) {
                auto current = unexploredNodes.pop();
                current.calc(goal);
                explored[map.getIndexFrom(current.position)] = true;

                if (current == goal) {
                    return true;
                }

                for (auto& direction : directions) {
                    Node successor(direction + current.position);

                    if (map.getIfInDanger(successor.position)) 
                        || map.inBounds(successor.position)
                        || explored[map.getIndexFrom(successor.position)]
                       ) {
                        continue;
                    }

                    auto unexplored = std::find(unexploredNodes.begin(), unexploredNodes.end(), successor);
                    if (unexploredNodes.end() == unexplored) {
                        successor.parent = &current;
                        successor.calc(goal);

                        unexploredNodes.push(successor);
                    }
                    else {
                        successor.calc(goal);

                        if (successor.G < unexplored->G) {
                            successor.parent = &current;
                        }
                    }
                }

                return false;
            }

And then you need an inBounds function

    bool inBounds(Position p) {
        return p.getX() >=0 && p.getY() >= 0 && p.getX() < size && p.getY() < size;
    }

I find this version to read more naturally.

share|improve this answer
4  
Using the term Vector is customary for coordinate tuples (or triplets). Vector is a direction which has the same components as a position. It's just a matter of interpretation. Moving the calculation of size*size out of the loop body will not affect the generated binary code. The compiler will deduce that size*size is loop-invariant and do that optimization for you. – Emily L. Jul 24 '15 at 7:09
    
Thanks for answering!. Well, I got a problem, and it's that since goalNode's parent has never been set to anything I get access violation reading location in parent->parent->position – Levon Jul 26 '15 at 0:41
    
Oh, and also in auto current = unexploredNodes.pop(); auto cannot be deduced since unexploredNodes.pop() returns void – Levon Jul 26 '15 at 0:50

@mdfst13 has already done a good job reviewing your code, but there are still a few things left that you could change. It's mostly nitpicking, but there's no harm in being a bit pedantic in a code review:

  • When you use a standard library function, don't forget to fully qualify it with std::. That way, it makes it clear what you're doing and it also makes it easier to look for standard library features in your code:

    H = static_cast<int>((std::abs(static_cast<double>(position.getX() - endPos.getX())) + std::abs(static_cast<double>(position.getY() - endPos.getY()))));
                          ^^^^^                                                            ^^^^^
    
  • There's a reason I picked the line above: it seems that you're doing some static_casts in order to provide the good types to std::abs, the ones that match its signature the best. That said, all these conversions would have been done implicitly and I doubt that your static_casts make the code more readable. Even better: the header <cstdlib> has integer overloads for std::abs (while <cmath> contains the floating point overloads); you can simply include <cstdlib> and drop the static_casts altogether:

    H = std::abs(position.getX() - endPos.getX()) + std::abs(position.getY() - endPos.getY());
    
  • When you can, try to define your comparison and relational operators (and the other ones too by the way, apart from a few ones) as free functions outside of the class. Most of the time they are implemented in function of accessible class properties anyway, so it should be easy.

  • In C++, it's not idiomatic for classes to have a display or print method. The idiomatic way to display a class is to overload operator<< between your class and std::ostream& so that it can be used with every compliant stream:

    std::ostream& operator<<(std::ostream& stream, const Map& map) {
        for (int i = 0; i < map.size * map.size; ++i) {
            stream << map.data[i] << ' ';
            if (!(i % map.size)) {
                stream << '\n';
            }
        }
        return stream;
    }
    

    Note that you will probably need to make this overloaded operator<< a friend of Map or provide an equivalent of a Map::getSize method in order for this function to work.

  • You could use some more standard library features to avoid some conditions. For example consider these lines:

    if (position.y < 0) position.y = 0;
    if (position.x < 0) position.x = 0;
    

    You can use std::max instead:

    position.y = std::max(0, position.y);
    position.x = std::max(0, position.x);
    

    While it doesn't change many things, it lessens the number of visible conditionals and can thus reduce the cognitive burden when you read the algorithm.

share|improve this answer
    
Its not uncommon for classes to have display/print method. BUT they normally also have an operator<< overload (which simply calls the display/print method if they exist). – Loki Astari Jul 24 '15 at 15:13
    
Thanks for answering! I'll take all the points into account – Levon Jul 26 '15 at 1:26

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.