2

I implemented A* for the 8 puzzle problem pretty much word for word to the pseudo code in the wiki article but even though I have closed I still get infinite runtime on boards that should fail.

It seems that the rate of opening new nodes is larger than closing them, since for each node about 1-4 new nodes are added to the open but only one node is moved from open to closed.

So what can be done to recognize that a given starting board has no path to the goal without waiting forever?

This is the code:

public List<KeyValuePair<GameBoard, GameBoard.MoveDirection>> Search(GameBoard startBoard, GameBoard goal)
{
    InitSearch(startBoard, goal);
    while (_open.Count > 0)
    {
        GameBoard current = LowestFScoreInOpen();
        if (current.Equals(_goal))
        {
            return ReconstructPath(current);
        }

        _open.Remove(current);
        _closed.Add(current);

        foreach (GameBoard.MoveDirection direction in Enum.GetValues(typeof(GameBoard.MoveDirection)))
        {
            GameBoard neighbor = current.MoveAndCopy(direction);
            if(neighbor == null) continue;  // invalid direction 
            if(_closed.Contains(neighbor)) continue;

            int tentativeScore = _gScore[current] + 1;
            if (!_open.Contains(neighbor))
            {
                _open.Add(neighbor);
            }
            else if (tentativeScore >= _gScore[neighbor])
            {
                continue; // already in _open and this is not a better path 
            }

            // best path so far
            _cameFrom[neighbor] = new KeyValuePair<GameBoard, GameBoard.MoveDirection>(current, direction);
            _gScore[neighbor] = tentativeScore;
            int fscore = tentativeScore + Heuristic(neighbor);
            _fScore[neighbor] = fscore;
            _sortedFScore.Enqueue(new PriorityQueueItem<GameBoard, int>(neighbor, fscore));
        }
    }

    return null; // no path found
}

Example for start and goal board with no path between them:

Start: enter image description here Goal: enter image description here

Full code: https://pastebin.com/v10U2J2Z

shinzou
  • 5,850
  • 10
  • 60
  • 124
  • 1
    have you tested it and got infinite loops ? The code actually seems good and to not run forever, thanks to the line if(_closed.Contains(neighbor)) continue; . Because of it, after a while you should never add anything new to the opened nodes, so you end up adding 0 elements to open and moving 1 from open to close – gdelab Apr 11 '17 at 15:39
  • You aren't using Dijkstra's algorithm since you don't have a property visited that is set initially to not visited. – jdweng Apr 11 '17 at 15:41
  • Yes in the boards I mentioned, after more than 12 hours `open` had about 2 million entries and `closed` about 700k entries. @gdelab – shinzou Apr 11 '17 at 15:42
  • I don't know if it would settle your problem, but it's probably good practice and a gain of time if you start by checking if current is already in _closed before doing any more work. Otherwise you can do the work on one node several times uselessly -- EDIT: forget it it's probably in function LowestFScoreInOpen()) already... – gdelab Apr 11 '17 at 15:42
  • @jdweng what do you mean? All the initializations happen in `initsearch()`. – shinzou Apr 11 '17 at 15:47
  • 2 million entries seems way too much! If my calculations are correct, this puzzle should have no more than 362880 possible states. My guess is that your `GameBoard` doesn't implement the [`IEquatable Interface`](https://msdn.microsoft.com/en-us/library/ms131187(v=vs.110).aspx), but it's only a guess because you don't show your implementation of `GameBoard`. – PJvG Apr 11 '17 at 15:48
  • It's 10! combinations, 0 to 9 is 10 digits, so 10*362880 combinations. I triple checked the equality and `GetHashCode()` of gameboard. @PJvG – shinzou Apr 11 '17 at 15:51
  • @gdelab I tried to add `if (_closed.Contains(current)){ ...` it never entered this condition even after a few minutes. – shinzou Apr 11 '17 at 15:59
  • 2
    When posting problems like this it's better to include full code (as minimal as required), because then people can just copy paste, run, and find a problem MUCH faster than just by looking at code. – Evk Apr 11 '17 at 16:03
  • 1
    Start with the standard techniques. First: does the code work properly with a zero heuristic? If it works with a zero heuristic but not with your heuristic, *is the heuristic actually admissible*? If the heuristic is not the problem, then can you produce a *much smaller* unsolvable example that does not terminate? – Eric Lippert Apr 11 '17 at 16:07
  • Now, if your question is "how can I quickly discover that there are no solutions to the 8-puzzle without trying them all?" that's easy. Read the Wikipedia article on the 15-puzzle for the math. – Eric Lippert Apr 11 '17 at 16:10
  • All nodes are set to Open. Then when you enter each node you set it closed. Eventually you reach the end location or all nodes are closed and you exit the while loop. The code will fail if you have unreachable nodes. Suppose your game has two paths that don't intersect in one graph. – jdweng Apr 11 '17 at 16:12
  • I use manhattan distance heuristics, I didn't build this for a smaller board, but there are other boards that this does work on them and find a solution in a few seconds. @EricLippert – shinzou Apr 11 '17 at 16:13
  • @Evk I added the full code. – shinzou Apr 11 '17 at 16:14
  • @jdweng nodes in A* don't start in Open, apart from the start node. The rest starts by not existing. – harold Apr 11 '17 at 16:17
  • 1
    _"I added the full code"_ -- no, you didn't. You added a link to an external web site that purportedly contains your code. Stack Overflow questions should be self-contained, immune to any changes elsewhere on the Internet. See [mcve] and [ask]. – Peter Duniho Apr 11 '17 at 16:44
  • @PeterDuniho uh the code there is immutable and you don't expect me to paste this much code here right? – shinzou Apr 11 '17 at 16:48
  • _"you don't expect me to paste this much code here right?"_ -- a basic A* implementation isn't very much code. So if there's too much code in your full example, it's too much to expect Stack Overflow users to try to read through. Your job is to distill the basic problem into a small enough example to be appropriate on Stack Overflow. Again, please read [mcve]. See also [ask], and especially read the articles that are linked at the bottom of that page. – Peter Duniho Apr 11 '17 at 17:21
  • That's what I did at first but was asked for the full code... @PeterDuniho – shinzou Apr 11 '17 at 17:30
  • _"That's what I did at first"_ -- no, you did not. That you think you did tells me that if you have in fact read [mcve] and the other links I've recommended, you did not really understand them. It's probably worth your time to go read them again. – Peter Duniho Apr 11 '17 at 17:42

1 Answers1

4

The problem here is not in algorithm implementation (well there might be problems with it too - did not check), but in wrong implementation of GetHashCode() for GameBoard class:

public int[,] Board { get; }
public override int GetHashCode()
{
    return Board.GetHashCode();
}

Board is array and array is a class (not struct) so it's default GetHashCode() implementation just returns some value which is guaranteed to be the same for this instance only. That means that:

var a = new int[] {1,2,3};
var b = new int[] {1,2,3};
Debug.Assert(a.GetHashCode() == b.GetHashCode());

Fails, because even though array contents are the same - those are different instances and GetHashCode returns completely different values.

That wrong GetHashCode implementation is used in critical places in your algorithm, most importantly (for this concrete problem) in closed set, which is:

HashSet<GameBoard> _closed = new HashSet<GameBoard>();

When you do

if (_closed.Contains(neighbor)) continue;

It fails to detect that closed set contains given board, even if it really does, because the only requiremet for a hash function (objects that are equal should return hash codes that are equal) is violated. So your closed set growth without bound because you add the same boards there again and again.

Easy to check with this code:

var set = new HashSet<GameBoard>();
set.Add(new GameBoard(new int[,]
{
    {1, 2, 3},
    {8, 0, 4},
    {7, 6, 5},
}));
bool contains = set.Contains(new GameBoard(new int[,]
{
    {1, 2, 3},
    {8, 0, 4},
    {7, 6, 5},
})); // false

To fix, do something like:

public override int GetHashCode()
{                          
     // make _flatten readonly by the way, now it's not
     return _flatten.GetHashCode();
}

And your program will terminate (will throw null reference exception, because you return null if there is no solution but don't check for this when printing solution).

Evk
  • 98,527
  • 8
  • 141
  • 191
  • Damn, I was sure I checked the data structures before doing the algorithm. Thanks for finding this. How long did you wait for it to finish? It's about a minute for me. – shinzou Apr 11 '17 at 21:27
  • Didn't measure but I think less than 15 minutes (was away). – Evk Apr 11 '17 at 21:30
  • 1
    @kuhaku: Now ask yourself the question **what test could I have written that would have found this problem without asking people on the internet to do my work for me**? There are a number of techniques you could have used; can you enumerate a few of them? – Eric Lippert Apr 12 '17 at 01:19
  • Testing this manually like the example in this answer but I was certain I checked this earlier... I heard about unit tests and TDD but I haven't had the time to learn those yet. @EricLippert – shinzou Apr 12 '17 at 07:29