Peg棋盘游戏 - 在深度优先搜索中检查棋子还是检查孔位

6
我正在尝试使用深度优先搜索算法解决Peg Solitaire问题——这个游戏应该是可以被解决的,因为“现代计算机可以在合理的时间内轻松检查所有游戏位置”。即使经过23小时,算法仍未找到任何解决方案。我进行了网络搜索,并找到了论文"Depth-first search solves peg solitaire"。我尝试了该论文中的C程序,程序刚开始就立即找到了第一个解决方案。我比较了两种算法。两种算法的主要区别在于它们如何寻找可能的跳棋。我的算法从左上角到右下角“搜索空洞”(检查每个空洞是否有可能的跳棋),而论文算法从左上角到右下角“搜索棋子”(检查每个棋子是否有可能的跳棋)。两种算法都按照它们发现的顺序尝试跳棋。为了强调差异:
  • 分析空洞:运行时间23小时无解
  • 分析钉子:运行时间10秒,2940个解决方案

问题:为什么在搜索跳跃方式时会有如此巨大的(无法解决/立即解决)差异?为什么检查钉子而不是检查空洞以寻找可能的跳跃更好?

您可以尝试使用以下C ++程序进行算法。为了使其紧凑,我已删除了较不重要的部分(打印板,生成初始位板,...)。

#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;
}

作为一个猜测,你的算法似乎广泛地使用位。因此,考虑两种方法的“位复杂度”是有意义的。也许,工作时间取决于板上设置(未设置)位的数量。我没有进行分析,但也许更快的方法会生成较少的设置(或未设置)位,因此更快? - Victor Sorokin
@Victor:我认为这不是位复杂度的问题,更快的方法需要更多的位操作。如果我们比较找到第一个解所需的步骤数:针版本只需要约20300步,而孔版本即使经过亿万次移动也没有找到任何解。我认为原因必须在于尝试移动的顺序。如果是这样,为什么针排序比孔排序更好呢? - Christian Ammer
1个回答

1
如果在任何一种方法中,结果树中没有循环,并且结果树相同,则应用相同搜索算法时产生巨大差异的原因必须是节点扩展的顺序(启发式)。我仍在检查您的实现,但一切似乎都没问题,所以我看不出其他速度差异的原因。
一段时间以前,我在这个问题上有一个任务,我在我的书签中找到了这个:您可以在这里阅读更多信息,深度搜索与广度搜索以及几种启发式来改进搜索:http://www.durangobill.com/Peg33.html

哎呀,这真是个非常有趣的问题!如果你的算法是正确的,且得到的树是相同的,那么我之前的想法就错了。在考虑空隙时,节点扩展的顺序会有所不同,从而成为更好的启发式算法。(很抱歉没有给出更好的帮助,我会和枕头讨论一下,也许明天可以想出更好的方法。) - NotGaeL
谢谢提供链接,很有趣。也许它会带我找到答案。 - Christian Ammer
我今天与我的老师进行了评论,他同意算法必须是正确的,巨大的差异是由于节点扩展的顺序造成的。他说:“在深度优先搜索中,如果你从左到右开始查找,而解决方案却在右边,那么你就绝对没有运气。”扩展一个随机节点直到搜索完成应该会给出更或多或少相同的结果,无论你移动孔还是桩,树都是相同的,这没有任何影响,他也同意这一点... - NotGaeL
骑士巡游和Sprout游戏也很有趣,从非确定性算法解决方案的角度来看,有一些令人惊讶的启发式方法可以帮助您快速地进行深度优先探索,找到许多解决方案... - NotGaeL
但我不知道为什么Peg版本首先尝试正确的移动。没有复杂的启发式或其他东西(只是运气吗?)。也许有人能找出原因,因此我将保持问题一段时间。 - Christian Ammer
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接