6

I am trying to solve Peg Solitaire with a depth-first search algorithm – it should be possible to solve the game since "modern computers can easily examine all game positions in a reasonable time". Even after 23 hours, the algorithm didn't find any solution. I did a web search and found the paper "Depth-first search solves peg solitaire". I tried the c-program from the paper and the first solution was found immediately after the program started. I compared the algorithms. The main difference between the algorithms is the way they find possible peg-jumps. While my algorithm searches the board for holes from top left to bottom right (each hole is checked if there are possible jumps), the paper-algorithm searches the board for pegs from top left to bottom right (each peg is checked if there are possible jumps). Both algorithms are trying the jumps in the order they are found. To underline the difference:

  • Analyzing holes: Runtime 23 hours no solution
  • Analyzing pegs: Runtime 10 seconds, 2940 solutions

Question: Why is there such a giant (not solvable / immediately solved) difference on how to search the board for jumps? Why is it better to check the pegs instead of checking the holes for possible jumps?

You can try the algorithm with following C++ program. To keep it compact I have removed the lesser important parts (printing the board, generating the initial bitboard, ...).

#include <iostream>
#include <ctime>
#include <vector>
#include <utility>
#include <ctime>

typedef unsigned __int64 ui64;
typedef std::vector<std::pair<int, int> > vecjumps; // first=from, second=to
ui64 moves = 0;    // Number of moves made so far
int solutions = 0; // Number of solutions found so far
clock_t start;     // To measure time

//          Bitboard
//   Board            Bits         
//  ------------------------------
//    ***           02 03 04      
//    ***           10 11 12      
//  *******   16 17 18 19 20 21 22
//  ***o***   24 25 26 27 28 29 30
//  *******   32 33 34 35 36 37 38
//    ***           42 43 44      
//    ***           50 51 52      
const ui64 bitboard = 0x001c1c7f7f7f1c1c; // 1ULL << 2 | 1ULL << 3 | ...
ui64 board = bitboard & ~(1ULL << 27);    // Initial Board: Bit 27 <=> Hole

// To try the hole-version: Swap Commented and Uncommented parts
vecjumps getMoves(ui64 b)
{
    // Find the possible jumps through bit-shift operations. mr <=> right jump
    // possibilities. The inverted Board represents the Holes. Shifting the
    // board by 16 right/left --> moving all pegs up/down. Shifting the board
    // by 1 --> moving all pegs left/right
    //ui64 mr = (((b >> 1) & b) <<  2) & ~b & bitboard;
    //ui64 md = (((b >> 8) & b) << 16) & ~b & bitboard;
    //ui64 ml = (((b >> 1) & b) >>  1) & ~b & bitboard;
    //ui64 mu = (((b >> 8) & b) >>  8) & ~b & bitboard;
    ui64 mr = ((((b >> 1) & b) <<  2) & ~b & bitboard) >>  2;
    ui64 md = ((((b >> 8) & b) << 16) & ~b & bitboard) >> 16;
    ui64 ml = ((((b >> 1) & b) >>  1) & ~b & bitboard) <<  2;
    ui64 mu = ((((b >> 8) & b) >>  8) & ~b & bitboard) << 16;

    vecjumps jumps;
    jumps.reserve(12);
    for (int i = 2; i < 53; i++)
    {
        //if (mr & (1ULL << i)) jumps.push_back(std::make_pair(i -  2, i));
        //if (md & (1ULL << i)) jumps.push_back(std::make_pair(i - 16, i));
        //if (ml & (1ULL << i)) jumps.push_back(std::make_pair(i +  2, i));
        //if (mu & (1ULL << i)) jumps.push_back(std::make_pair(i + 16, i));
        if (mr & (1ULL << i)) jumps.push_back(std::make_pair(i, i + 2));
        if (md & (1ULL << i)) jumps.push_back(std::make_pair(i, i + 16));
        if (ml & (1ULL << i)) jumps.push_back(std::make_pair(i, i - 2));
        if (mu & (1ULL << i)) jumps.push_back(std::make_pair(i, i - 16));
    }
    return jumps;
}

void makeMove(ui64& b, int from, int to)
{
    // Through a xor-instruction, a jump from 11 to 27 includes 19
    // 19 = (11 + 27) / 2
    b ^= 1ULL << from | 1ULL << (from + to) / 2 | 1ULL << to;
    moves++;
    if (moves % 10000000 == 0)
        printf("(%8d, %14llu moves, %lf)\n", 
            solutions, moves, (double)(clock() - start) / CLOCKS_PER_SEC);
}

// Depth First Search Algorithm
bool findSolution(int depth)
{
    if (!depth) return ((1ULL << 27) & board);
    vecjumps jumps = getMoves(board);
    for (vecjumps::const_iterator cit = jumps.begin(); cit != jumps.end();
         ++cit)
    {
        ui64 copy = board;
        makeMove(board, (*cit).first, (*cit).second);
        if (findSolution(depth - 1)) solutions++;
        board = copy;
    }
    return false;
}

int main()
{
    start = clock();
    findSolution(31);   
    printf("(%8d, %14llu moves, %lf)\n", 
        solutions, moves, (double)(clock() - start) / CLOCKS_PER_SEC);
    return 0;
}
animuson
  • 53,861
  • 28
  • 137
  • 147
Christian Ammer
  • 7,464
  • 6
  • 51
  • 108
  • As a wild guess, your alg seems to work extensively with bits. So, it makes sense to consider 'Bit complexity' of both approaches. May be, working time depends on the number of set(unset) bits on board. I didn't do analysis, but maybe, faster approach generates less set (or unset) bits, and, thus, faster? – Victor Sorokin Nov 28 '11 at 10:54
  • @Victor: I don't think its the bit-complexity, there are more bit-operations on the faster approach. If we compare the number of moves needed to find the first solution: The peg-version only needs ~20300 moves, the hole-version didn't find any solution, even after hundred-millions of moves. I think, the reason must lie in the order the moves are tried. If so, why is the peg-ordering better then the hole ordering? – Christian Ammer Nov 28 '11 at 11:58

1 Answers1

1

If there are no loops in the resulting tree in either method, and the resulting tree is the same, the reason for the hudge difference when aplying the same search algorithm must be the order in which the nodes are expanded (heuristic). I'm still checking your implementation but everything seems right on it, so I can't see any other reason for the speed difference.

Some time ago I had an assigment on this problem, and I found this on my bookmarks: You can read more info, depth search vs breadth search and a couple of heuristics to improve the search here: http://www.durangobill.com/Peg33.html

NotGaeL
  • 8,344
  • 5
  • 40
  • 70
  • Considering gaps or pegs on the same board gives the same jump-possibilities, the only difference is the ordering. The tree is not broader. – Christian Ammer Nov 27 '11 at 22:49
  • Mmh, this is a very interesting question! If your algorithm is right and the resulting tree is the same, then I was wrong and there is a decision on the order the nodes are being expanded which is different when considering the gaps, making that a (better) heuristic. (Sorry for not being of better help, I'll discuss it with the pillow, maybe tomorrow I will come up with something better) – NotGaeL Nov 27 '11 at 23:00
  • Thanks for the link, it is quite interesting. Maybe it brings me to an answer. – Christian Ammer Nov 28 '11 at 19:46
  • I commented with my teacher today, he agrees the algoritm must be right and the hudge difference is due to the order of expansion of the nodes. He said "in depth firsth search if you start looking from left to right and the solution turns to be in the right you're definetly out of luck". Expanding a random node until the search is complete should give more or less the same results no matter if you move holes or pegs, the tree is the same and it makes no difference, he also agreed with that... – NotGaeL Nov 29 '11 at 17:08
  • Knight's tour and the game of Sprout are also interesting to look at from a non-deterministic algorithmic solution, there are quite surprising heuristics to help you find many solutions with depth first exploring really quick... – NotGaeL Nov 29 '11 at 17:12
  • Thanks for investigating. I have tried to expand the tree with a random node (called `random_shuffle` on the jumps returned by getMoves) and the program doesn't find any solution even not with the peg-version. The table on the page you linked brought me to the understanding that even after 4 wrong moves the board can not be brought to a solution. And the depth-first search would need years (maybe) to come back to the fourth move. – Christian Ammer Nov 29 '11 at 21:44
  • But I don't know why the peg-version tries the right moves first. There's no sophisticated heuristic or something else (just luck?). Maybe someone finds out what's the reason, thus I will keep the question open for some time. – Christian Ammer Nov 29 '11 at 21:57