views:

280

answers:

2

I traverse a 16x16 maze using my own A* implementation.

All is well. However, after the traversal, I would like to find out what wall would give me the best alternative path. Apart from removing every block and re-running A* on the maze, what's a more clever and elegant solution?

I was thinking give every wall node (ignored by A*), a tentative F-value, and change the node structure to also have a n-sized list of node *tentative_parent where n is the number of walls in the maze. Could this be viable?

+1  A: 

Finding candidate areas for wall removal:

All along your original A* found path, retain the previous distance, and whenever the previous distance is larger than the current distance, note the previous node as having a potential for a wall removal.

I submit that it will catch cases of most impact:

Example 1:

    012
   *****
 0 *R*G*
 1 * * *
 2 * * *
 3 * * *
 4 * * *
 5 * * *
 6 *   *
   *****

Where:

R (0,0) is your rabbit-looking course runner G (2,0) is the goal

In this case, we start at (0,0), with a distance of 2. The next available move is (0,1), with a distance of 2.23 (sq root of 5). Your distance just grew, so that your previous location had potential for a wall tear down.

Example 2:

    0123456
   *********
 0 *R*     *
 1 ** *    *
 2 * * *   *
 3 *  * *  *
 4 *   * * *
 5 *    * **
 6 *     *G*
   *********

Start: (0,0) End: (6,6) A* course: (0,0), (1,1), (2,2), (3,3), (4,4), (5,5), (6,6) Distances: 8.5, 7.1, 5.7, 4.2, 2.8, 1.4 (or sq root of 72, 50, 32, 18, 8, 2) No optimal wall to remove.

Determining which wall to remove:

Draw a line between your marked node and your goal node. The first wall along that line (closest to the marked node) goes down. Give it some fuzz in order to allow removing your wall along a straight diagonal that would only hit corners. Compare your alternative paths to find the shortest.

MPelletier
That's an interesting concept. I'm not sure I understand it 100%, but I'll try to implement it and see what happens :P
David Titarenco
The only issue I'm having with this, is how do I store the "alternative" parents of a certain node that "could have" come through a wall?
David Titarenco
Hmmm, a stack or file? I don't understand exactly how keeping the alternative parent is a problem.
MPelletier
A stack, because you could (theoretically) have n alternative parents (if an empty square is surrounded by blocks on all sides, for example) where n is pretty much up in the air.
David Titarenco
Well, you could theoretically have n alternative parents, like you say, but you might not want to get there. Otherwise, you'd have n graphs to go through. Wouldn't that mean O(n^n), if not worse?
MPelletier
Yeah exactly, so how do you know where to go (because you want the BEST path through a wall, not an alternative path through a wall). I'm also stuck at an O(n^n) implementation
David Titarenco
You can only take down one wall per alternative, correct? The idea behind A* (and other path finding algorithms like Dijkstra's) is that you don't want to try all the paths, to do better than O(n^3) time. Likewise, you want an algorithm that does NOT explore all alternative paths, only the ones with best potential. The idea behind my representation is: find the best path, note the increases, and remove a wall there. I'm trying to figure out how "there" is determined.
MPelletier
Above, when I wrote "stack or file", please read as "stack or queue".
MPelletier
I'm not done with this. Will do a proof of concept over the weekend.
MPelletier
+3  A: 

When you add a node to the list of nodes to consider, also add a flag whether the path through that node has already gone through a wall.

possibleNode.heuristic = currentNode.distance + heuristicEstimate(possibleNode)
possibleNode.goneThroughWall = currentNode.goneThroughWall || possibleNode.isAWall
allPossiblePaths.insert(possibleNode) // Priority queue sorted on Node.heuristic

Then when considering possible paths from a node, if it hasn't gone through a wall, consider adjacent walls to be fair paths.

foreach neighborNode in neighbors(someNode)
    if !(someNode.goneThroughWall && neighborNode.isAWall)
        addToPossiblePaths(neighborNode)

You already have to maintain distance from the start node to the current node being processed, and it uses what you already have in place.

Full proof of concept:

We see that operator== is defined to also consider whether or not the path has hit a wall yet. This allows us to consider the a node twice if needed, when we look in the closed set to see if we have already encountered this node. This is the case in the center hallway in the example maze in the source below.

The portions of code marked with #ifdef I_AM_ALLOWED_TO_GO_THROUGH_A_WALL show the portions needed to augment a normal A* algorithm to be able to go through a single wall.

#include <cassert>
#include <algorithm>
#include <cstdlib>
#include <iostream>
#include <set>
#include <vector>

#define I_AM_ALLOWED_TO_GO_THROUGH_A_WALL

const int width  = 5;
const int height = 5;

// Define maze
const int maze[height][width] =
  { { 0, 1, 1, 0, 0 },
    { 0, 1, 0, 1, 0 },
    { 0, 1, 0, 1, 0 },
    { 0, 1, 0, 1, 0 },
    { 0, 0, 0, 0, 0 } };

// Square represents the actual structure of the maze
// Here, we only care about where it is, and whether it is a wall
struct Square {
  Square(int _x, int _y)
    : x(_x), y(_y) {}

  bool operator==(const Square& rhs) const {
    return x == rhs.x && y == rhs.y;
  }

  bool isAWall() const {
    return maze[y][x];
  }

  int distance(const Square& goal) const {
    return std::abs(x - goal.x) + std::abs(y - goal.y);
  }

  int x;
  int y;
};

// Define start and end of maze
const Square goalSquare(3, 0);
const Square startSquare(0, 0);

// A PathNode describes the A* algorithm's path through the maze
// It keeps track of its associated Square
//                   its previous PathNode in the path
//                   the length of the path up to the current PathNode
//                   whether the path so far has goneThroughWall
struct PathNode {
  explicit PathNode(const Square& s, int length = 0, const PathNode* _prev = NULL)
    : square(s),
      prev(_prev),
      pathLength(length) {
    heuristic = pathLength + square.distance(goalSquare);

#ifdef I_AM_ALLOWED_TO_GO_THROUGH_A_WALL
    if(prev)
      goneThroughWall = prev->goneThroughWall || square.isAWall();
    else
      goneThroughWall = false;

    // Sanity check, these should be the same
    assert(goneThroughWall == hasGoneThroughAWall());
#endif
  }

  bool operator<(const PathNode& rhs) const {
    return heuristic < rhs.heuristic;
  }

  // This is very important. When examining the closed set to see
  // if we've already evaulated this node, we want to differentiate
  // from whether we got to that node using a path through a wall.
  //
  // This is especially important in the given example maze.
  // Without this differentiation, paths going up column 3 will hit
  // old, closed paths going through the walls in column 2, and not
  // find the best path.
  bool operator==(const PathNode& rhs) const {
    return square == rhs.square
#ifdef I_AM_ALLOWED_TO_GO_THROUGH_A_WALL
      && goneThroughWall == rhs.goneThroughWall
#endif
      ;
  }

  bool weakEquals(const PathNode& rhs) const {
    return square == rhs.square;
  }

  bool weakEquals(const Square& rhs) const {
    return square == rhs;
  }

  // Sanity checker
  bool hasGoneThroughAWall() const {
    if(square.isAWall()) return true;

    const PathNode* p = prev;
    while(p) {
      if(p->square.isAWall())
        return true;
      p = p->prev;
    }

    return false;
  }

  Square square;
  const PathNode* prev;
  int heuristic, pathLength;
#ifdef I_AM_ALLOWED_TO_GO_THROUGH_A_WALL
  bool goneThroughWall;
#endif
};

std::ostream& operator<<(std::ostream& ostr, const PathNode& p) {
  ostr << "(" << p.square.x << ", " << p.square.y << ")";
  if(p.square.isAWall())
    ostr << " <- Wall!";
  return ostr;
}

std::vector<Square> getNeighbors(const Square& s) {
  std::vector<Square> neighbors;

  if(s.x > 0)
    neighbors.push_back(Square(s.x - 1, s.y));
  if(s.x < width - 1)
    neighbors.push_back(Square(s.x + 1, s.y));
  if(s.y > 0)
    neighbors.push_back(Square(s.x, s.y - 1));
  if(s.y < height - 1)
    neighbors.push_back(Square(s.x, s.y + 1));

  return neighbors;
}

void printList(const PathNode* p, int i = 0) {
  if(p) {
    printList(p->prev, i + 1);
    std::cout << *p << std::endl;
  } else {
    std::cout << "Length: " << i << std::endl;
  }
}

typedef std::multiset<PathNode> Set;

int main(int argc, char** argv) {
  // Print out maze
  for(int j = 0; j < height; ++j) {
    for(int i = 0; i < width; ++i) {
      std::cout << " " << maze[j][i];
    }
    std::cout << std::endl;
  }
  std::cout << std::endl;

  Set closedSet;
  Set openSet;
  openSet.insert(PathNode(startSquare)); // Start node (defined at line ~42)

  int processedCount = 0;
  while(!openSet.empty()) {
    Set::iterator currentNode = openSet.begin();
    ++processedCount;

    // We've found the goal, so print solution.
    if(currentNode->weakEquals(goalSquare)) {
      std::cout << "Processed nodes: " << processedCount << std::endl;
      printList(&*currentNode);
      return 0;
    }

    {
      // Move from open set to closed set
      Set::iterator del = currentNode;
      currentNode = closedSet.insert(*currentNode);
      openSet.erase(del);
    }

    std::vector<Square> neighbors = getNeighbors(currentNode->square);
    for(unsigned int i = 0; i < neighbors.size(); ++i) {
      PathNode currentNeighbor(neighbors[i], currentNode->pathLength + 1, &*currentNode);

      // Skip if it is 2nd wall
      if(
#ifdef I_AM_ALLOWED_TO_GO_THROUGH_A_WALL
        currentNode->goneThroughWall &&
#endif
        currentNeighbor.square.isAWall()
      )
        continue;

      // Skip if it is already in the closed set
      // Note: Using std::find here instead of std::multiset::find because
      // std::multiset::find uses the definition !(a < b) && !(b < a) for
      // searching. I want it to use my overloaded a == b.
      if(find(closedSet.begin(), closedSet.end(), currentNeighbor) != closedSet.end())
        continue;

      // Skip if we were just there
      const PathNode* p = currentNode->prev;
      if(p && p->weakEquals(currentNeighbor))
        continue;

      // See if there is a better alternative in the open set
      // Note: Using std::find here instead of std::multiset::find. See above.
      Set::iterator contender = find(openSet.begin(), openSet.end(), currentNeighbor);
      if(contender == openSet.end())
        openSet.insert(currentNeighbor);
      else if(currentNeighbor.pathLength < contender->pathLength) {
        // We've found a better alternative, so replace
        openSet.erase(contender);
        openSet.insert(currentNeighbor);
      }
    }
  }

  std::cout << "Failure." << std::endl;
  return 1;
}
tJener
Thanks for this. I think this may make the most sense, and I was already developing a similar implementation. However, this would only work for the "first" wall encountered I think. Should I store a list<possible> of list<paths>?
David Titarenco
@David Titarenco, each item in your priority queue/sorted list in the open set represents a possible path used to get to a certain node. The question of whether a wall has destroyed to get to a certain node in the open set is independent of the other items, unless I'm mistaken.
tJener
Hey, great proof of concept! But I'm having some issues with my implementation. I implemented your pseudocode, and just as I said, it's only counting the "first" wall, I'll post my code in the main post maybe you can point me in the right direction :)
David Titarenco
What I pasted there should actually compile i.e., pasting into `astar.cpp` and `make astar` should work. Either way, in looking at your code, the problem may be the function `should_ignore_walls_allowed`. It seems that you're making it a property of the actual square, as opposed to the path through the squares. (However, I'm also not completely sure I understand what `should_ignore_walls_allowed` means.) If I have some time tomorrow, I'll try to make the code a little more clear to demonstrate what I mean, but is it analogous to my `PathNode::goneThroughWall` field?
tJener
I've added some more comments and moved stuff around to help better explain what I mean when I want to differentiate the path from the actual node/square.
tJener