简体   繁体   English

如何优化骑士游算法?

[英]How to optimize Knight's tour algorithm?

I code the Knight's tour algorithm in c++ using Backtracking method.我使用回溯方法在 C++ 中编写了Knight's tour算法。 But it seems too slow or stuck in infinite loop for n > 7 (bigger than 7 by 7 chessboard).但是对于 n > 7(大于 7 x 7 棋盘),它似乎太慢或陷入无限循环。

The question is: What is the Time complexity for this algorithm and how can I optimize it?!问题是:这个算法的时间复杂度是多少,我该如何优化它?!


The Knight's Tour problem can be stated as follows:骑士之旅问题可以表述如下:

Given a chess board with n × n squares, find a path for the knight that visits every square exactly once.给定一个有 n × n 个方格的国际象棋棋盘,为骑士找到一条只访问每个方格一次的路径。

Here is my code:这是我的代码:

#include <iostream>
#include <iomanip>

using namespace std;

int counter = 1;
class horse {
  public:
    horse(int);
    bool backtrack(int, int);
    void print();
  private:
    int size;
    int arr[8][8];
    void mark(int &);
    void unmark(int &);
    bool unvisited(int &);
};

horse::horse(int s) {
    int i, j;
    size = s;
    for (i = 0; i <= s - 1; i++)
        for (j = 0; j <= s - 1; j++)
            arr[i][j] = 0;
}

void horse::mark(int &val) {
    val = counter;
    counter++;
}

void horse::unmark(int &val) {
    val = 0;
    counter--;
}

void horse::print() {
    cout << "\n - - - - - - - - - - - - - - - - - -\n";
    for (int i = 0; i <= size - 1; i++) {
        cout << "| ";
        for (int j = 0; j <= size - 1; j++)
            cout << setw(2) << setfill ('0') << arr[i][j] << " | ";
        cout << "\n - - - - - - - - - - - - - - - - - -\n";
    }
}

bool horse::backtrack(int x, int y) {
    if (counter > (size * size))
        return true;

    if (unvisited(arr[x][y])) {
        if ((x - 2 >= 0) && (y + 1 <= (size - 1))) {
            mark(arr[x][y]);
            if (backtrack(x - 2, y + 1))
                return true;
            else
                unmark(arr[x][y]);
        }
        if ((x - 2 >= 0) && (y - 1 >= 0)) {
            mark(arr[x][y]);
            if (backtrack(x - 2, y - 1))
                return true;
            else
                unmark(arr[x][y]);
        }
        if ((x - 1 >= 0) && (y + 2 <= (size - 1))) {
            mark(arr[x][y]);
            if (backtrack(x - 1, y + 2))
                return true;
            else
                unmark(arr[x][y]);
        }
        if ((x - 1 >= 0) && (y - 2 >= 0)) {
            mark(arr[x][y]);
            if (backtrack(x - 1, y - 2))
                return true;
            else
                unmark(arr[x][y]);
        }
        if ((x + 2 <= (size - 1)) && (y + 1 <= (size - 1))) {
            mark(arr[x][y]);
            if (backtrack(x + 2, y + 1))
                return true;
            else
                unmark(arr[x][y]);
        }
        if ((x + 2 <= (size - 1)) && (y - 1 >= 0)) {
            mark(arr[x][y]);
            if (backtrack(x + 2, y - 1))
                return true;
            else
                unmark(arr[x][y]);
        }
        if ((x + 1 <= (size - 1)) && (y + 2 <= (size - 1))) {
            mark(arr[x][y]);
            if (backtrack(x + 1, y + 2))
                return true;
            else
                unmark(arr[x][y]);
        }
        if ((x + 1 <= (size - 1)) && (y - 2 >= 0)) {
            mark(arr[x][y]);
            if (backtrack(x + 1, y - 2))
                return true;
            else
                unmark(arr[x][y]);
        }
    }
    return false;
}

bool horse::unvisited(int &val) {
    if (val == 0)
        return 1;
    else
        return 0;
}

int main() {
    horse example(7);
    if (example.backtrack(0, 0)) {
        cout << " >>> Successful! <<< " << endl;
        example.print();
    } else
        cout << " >>> Not possible! <<< " << endl;
}

output for the example (n = 7) above is like this:上面示例 (n = 7) 的输出如下所示:

在此处输入图片说明

Since at each step you have 8 possibilities to check and this has to be done for each cell (minus the last one) the time complexity of this algorithm is O(8^(n^2-1)) = O(8^(n^2)) where n is the number of squares on the edges of the checkboard.由于在每个步骤中您有 8 种可能性要检查,并且必须对每个单元格(减去最后一个)进行检查,因此该算法的时间复杂度为 O(8^(n^2-1)) = O(8^( n^2)) 其中 n 是棋盘边缘的方块数。 To be precise this is the worst case time complexity (time taken to explore all the possibilities if none is found or if it is the last one).准确地说,这是最坏情况下的时间复杂度(如果没有找到或者是最后一个,则探索所有可能性所花费的时间)。

As for the optimizations there can be 2 types of improvements:至于优化,可以有两种类型的改进:

Implementation improvements实施改进

You're calculating x-2, x-1, x+1, x+2 and the same for y at least the double of the times.您正在计算 x-2、x-1、x+1、x+2 以及 y 至少两倍的时间。 I can suggest to rewrite things like this:我可以建议重写这样的东西:

int sm1 = size - 1;
int xm2 = x - 2;
int yp1 = y + 1;
if((xm2 >= 0) && (yp1 <= (sm1))){
    mark(arr[x][y]);
    if(backtrack(xm2, yp1))
        return true;
    else
        unmark(arr[x][y]);
}

int ym1 = y-1;
if((xm2 >= 0) && (ym1 >= 0)){
    mark(arr[x][y]);
    if(backtrack(xm2, ym1))
        return true;
    else
        unmark(arr[x][y]);
}

note the reusing of precalculated values also in subsequent blocks.请注意在后续块中也重复使用预先计算的值。 I've found this to be more effective than what I was especting;我发现这比我预期的更有效; meaning that variable assignment and recall is faster than doing the operation again.这意味着变量赋值和调用比再次执行操作要快。 Also consider saving sm1 = s - 1;还要考虑节省sm1 = s - 1; and area = s * s; area = s * s; in the constructor instead of calculating each time.在构造函数中而不是每次都计算。

However this (being an implementation improvement and not an algorithm improvement) will not change the time complexity order but only divide the time by a certain factor.然而,这(是实现改进而不是算法改进)不会改变时间复杂度顺序,而只会将时间除以某个因素。 I mean time complexity O(8^(n^2)) = k*8^(n^2) and the difference will be in a lower k factor.我的意思是时间复杂度 O(8^(n^2)) = k*8^(n^2) 并且差异将在较低的 k 因子中。

Algorithm improvements算法改进

I can think this:我可以这样想:

  • for each tour starting on in a cell in the diagonals (like starting in (0,0) as in your example) you can consider only the first moves being on one of the two half checkboards created by the diagonal.对于从对角线中的一个单元格开始的每次游览(例如在您的示例中从 (0,0) 开始),您可以只考虑第一个移动是在对角线创建的两个半棋盘之一上。
    • This is beacouse of the simmetry or it exists 2 simmetric solutions or none.这是因为 simmetry 或者它存在 2 simmetric 解决方案或没有。
    • This gives O(4*8^(n^2-2)) for that cases but the same remains for non simmetric ones.对于那些情况,这给出了 O(4*8^(n^2-2)) 但对于非对称的情况仍然相同。
    • Note that again O(4*8^(n^2-2)) = O(8^(n^2))再次注意 O(4*8^(n^2-2)) = O(8^(n^2))
  • try to interrupt the rush early if some global condition suggests that a solution is impossible given the current markings.如果某些全局条件表明鉴于当前标记不可能解决方案,请尝试尽早中断匆忙。
    • for example the horse cannot jump two bulk columns or rows so if you have two bulk marked columns (or rows) and unmarked cells on both sides you're sure that there will be no solution.例如,马不能跳过两个大容量的列或行,所以如果你有两个大容量标记的列(或行)和两边未标记的单元格,你肯定不会有解决方案。 Consider that this can be checked in O(n) if you mantain number of marked cells per col/row updated.考虑到这可以在 O(n) 中检查,如果您保持每列/行更新的标记单元数。 Then if you check this after each marking you're adding O(n*8^(n^2)) time that is not bad if n < = 8. Workaround is simply not to check alwais but maybe every n/8 markings (checking counter % 8 == 4 for example or better counter > 2*n && counter % 8 == 4然后,如果你在每次标记后检查这个,你添加 O(n*8^(n^2)) 时间,如果 n < = 8 还不错。解决方法就是不检查 alwais,但可能每 n/8 个标记(例如检查counter % 8 == 4或更好的counter > 2*n && counter % 8 == 4
  • find other ideas to cleverly interrupt the search early but remember that the backtrack algorithm with 8 options will always have its nature of being O(8^(2^n)).寻找其他想法来提前巧妙地中断搜索,但请记住,具有 8 个选项的回溯算法将始终具有 O(8^(2^n)) 的性质。

Bye再见

Here is my 2 cents.这是我的 2 美分。 I started with the basic backtracking algorithm.我从基本的回溯算法开始。 It was waiting indefinitely for n > 7 as you mentioned.正如您提到的,它无限期地等待 n > 7。 I implemented warnsdorff rule and it works like a magic and gives result in less than a second for boards of sizes till n = 31. For n >31, it was giving stackoverflow error as recursion depth exceeded the limit.我实施了warnsdorff 规则,它就像魔术一样工作,对于 n = 31 之前的大小的板,它在不到一秒的时间内给出结果。对于 n > 31,由于递归深度超过限制,它会给出计算器溢出错误。 I could find a better discussion here which talks about problems with warnsdorff rule and possible further optimizations.我可以在这里找到更好的讨论它讨论了warnsdorff 规则的问题和可能的进一步优化。

Just for the reference, I am providing my python implementation of Knight's Tour problem with warnsdorff optimization仅供参考,我提供了带有warnsdorff优化的Knight's Tour问题的python实现



    def isValidMove(grid, x, y):
            maxL = len(grid)-1
            if x  maxL or y  maxL or grid[x][y] > -1 :
                    return False
            return True

    def getValidMoves(grid, x, y, validMoves):
            return [ (i,j) for i,j in validMoves if isValidMove(grid, x+i, y+j) ]

    def movesSortedbyNumNextValidMoves(grid, x, y, legalMoves):
            nextValidMoves = [ (i,j) for i,j in getValidMoves(grid,x,y,legalMoves) ]
            # find the number of valid moves for each of the possible valid mode from x,y
            withNumNextValidMoves = [ (len(getValidMoves(grid,x+i,y+j,legalMoves)),i,j) for i,j in nextValidMoves]
            # sort based on the number so that the one with smallest number of valid moves comes on the top
            return [ (t[1],t[2]) for t in sorted(withNumNextValidMoves) ]


    def _solveKnightsTour(grid, x, y, num, legalMoves):
            if num == pow(len(grid),2):
                    return True
            for i,j in movesSortedbyNumNextValidMoves(grid,x,y,legalMoves):
            #For testing the advantage of warnsdorff heuristics, comment the above line and uncomment the below line
            #for i,j in getValidMoves(grid,x,y,legalMoves):
                    xN,yN = x+i,y+j
                    if isValidMove(grid,xN,yN):
                            grid[xN][yN] = num
                            if _solveKnightsTour(grid, xN, yN, num+1, legalMoves):
                                    return True
                            grid[xN][yN] = -2
            return False

    def solveKnightsTour(gridSize, startX=0, startY=0):
            legalMoves = [(2,1),(2,-1),(-2,1),(-2,-1),(1,2),(1,-2),(-1,2),(-1,-2)]
            #Initializing the grid
            grid = [ x[:] for x in [[-1]*gridSize]*gridSize ]
            grid[startX][startY] = 0
            if _solveKnightsTour(grid,startX,startY,1,legalMoves):
                    for row in grid:
                            print '  '.join(str(e) for e in row)
            else:
                    print 'Could not solve the problem..'

Examine your algorithm.检查你的算法。 At each depth of recursion, you examine each of 8 possible moves, checking which are on the board, and then recursively process that position.在每个递归深度,您检查 8 个可能的移动中的每一个,检查哪些在棋盘上,然后递归处理该位置。 What mathematical formula best describes this expansion?什么数学公式最能描述这种扩展?

You have a fixed board size, int[8][8], maybe you should make it dynamic,你有一个固定的板尺寸,int[8][8],也许你应该让它动态,

class horse
{
    ...
    int** board; //[s][s];
    ...
};

horse::horse(int s)
{
    int i, j;
    size = s;
    board = (int**)malloc(sizeof(int*)*size);
    for(i = 0; i < size; i++)
    {
        board[i] = (int*)malloc(sizeof(int)*size);
        for(j = 0; j < size; j++)
        {
            board[i][j] = 0;
        }
    }
}

Changing your tests a little by adding a function to check that a board move is legal,通过添加一个功能来检查棋盘移动是否合法,稍微改变你的测试,

bool canmove(int mx, int my)
{
    if( (mx>=0) && (mx<size) && (my>=0) && (my<size) ) return true;
    return false;
}

Note that the mark() and unmark() are very repetitive, you really only need to mark() the board, check all legal moves, then unmark() the location if none of the backtrack() return true,注意mark()和unmark()是非常重复的,你真的只需要mark()棋盘,检查所有合法的动作,如果backtrack()没有返回true,然后unmark()位置,

And rewriting the function makes everything a bit clearer,重写函数让一切变得更清晰,

bool horse::backtrack(int x, int y)
{

    if(counter > (size * size))
        return true;

    if(unvisited(board[x][y]))
    {
        mark(board[x][y]);
        if( canmove(x-2,y+1) )
        {
            if(backtrack(x-2, y+1)) return true;
        }
        if( canmove(x-2,y-1) )
        {
            if(backtrack(x-2, y-1)) return true;
        }
        if( canmove(x-1,y+2) )
        {
            if(backtrack(x-1, y+2)) return true;
        }
        if( canmove(x-1,y-2) )
        {
            if(backtrack(x-1, y-2)) return true;
        }
        if( canmove(x+2,y+1) )
        {
            if(backtrack(x+2, y+1)) return true;
        }
        if( canmove(x+2,y-1) )
        {
            if(backtrack(x+2, y-1)) return true;
        }
        if( canmove(x+1,y+2) )
        {
            if(backtrack(x+1, y+2)) return true;
        }
        if( canmove(x+1,y-2) )
        {
            if(backtrack(x+1, y-2)) return true;
        }
        unmark(board[x][y]);
    }
    return false;
}

Now, think about how deep the recursion must be to visit every [x][y]?现在,想想访问每个 [x][y] 的递归必须有多深? Fairly deep, huh?相当深吧? So, you might want to think about a strategy that would be more efficient.因此,您可能需要考虑一种更有效的策略。 Adding these two printouts to the board display should show you how many backtrack steps occured,将这两个打印输出添加到板显示应该显示发生了多少回溯步骤,

int counter = 1; int stepcount=0;
...
void horse::print()
{
    cout<< "counter: "<<counter<<endl;
    cout<< "stepcount: "<<stepcount<<endl;
    ...
bool horse::backtrack(int x, int y)
{
    stepcount++;
    ...

Here is the costs for 5x5, 6x6, 7x6,这是 5x5、6x6、7x6 的成本,

./knightstour 5
 >>> Successful! <<< 
counter: 26
stepcount: 253283

./knightstour 6
 >>> Successful! <<< 
counter: 37
stepcount: 126229019

./knightstour 7
 >>> Successful! <<< 
counter: 50
stepcount: 56342

Why did it take fewer steps for 7 than 5?为什么 7 步比 5 步少? Think about the ordering of the moves in the backtrack - if you change the order, would the steps change?想想回溯中移动的顺序——如果你改变顺序,步骤会改变吗? What if you made a list of the possible moves [ {1,2},{-1,2},{1,-2},{-1,-2},{2,1},{2,1},{2,1},{2,1} ], and processed them in a different order?如果您列出可能的移动 [ {1,2},{-1,2},{1,-2},{-1,-2},{2,1},{2,1} ,{2,1},{2,1} ],并以不同的顺序处理它们? We can make reordering the moves easier,我们可以更容易地重新排列动作,

int moves[ ] =
{ -2,+1, -2,-1, -1,+2, -1,-2, +2,+1, +2,-1, +1,+2, +1,-2 };
...
        for(int mdx=0;mdx<8*2;mdx+=2)
        {
        if( canmove(x+moves[mdx],y+moves[mdx+1]) )
        {
            if(backtrack(x+moves[mdx], y+moves[mdx+1])) return true;
        }
        }

Changing the original move sequence to this one, and running for 7x7 gives different result,把原来的走法改成这个,7x7跑,结果不一样,

{ +2,+1, +2,-1, +1,+2, +1,-2, -2,+1, -2,-1, -1,+2, -1,-2 };


./knightstour 7
 >>> Successful! <<< 
counter: 50
stepcount: -556153603 //sheesh, overflow!

But your original question was,但你最初的问题是,

The question is: What is the Time complexity for this algorithm and how can I optimize it?!问题是:这个算法的时间复杂度是多少,我如何优化它?!

The backtracking algorithm is approximately 8^(n^2), though it may find the answer after as few as n^2 moves.回溯算法大约是 8^(n^2),尽管它可能会在 n^2 次移动后找到答案。 I'll let you convert that to O() complexity metric.我会让你把它转换成 O() 复杂度指标。

I think this guides you to the answer, without telling you the answer.我认为这会引导您找到答案,而不会告诉您答案。

this is a new solution:这是一个新的解决方案:

in this method, using the deadlock probability prediction at the next movement of the knight in the chessboard, a movement will be chose that it's tending to the deadlock probability is less than the other ones, we know at the first step this deadlock probability is zero for every cells and it will be changed gradually.在这种方法中,使用棋盘中骑士下一个动作的死锁概率预测,将选择一个倾向于死锁概率小于其他动作的动作,我们在第一步知道这个死锁概率为零对于每个细胞,它会逐渐改变。 The knight in the chessboard has between 2 and 8 moves, so each cells has predetermined value for next move.棋盘上的骑士有 2 到 8 步,因此每个单元格都有预定的下一步移动值。

Selecting the cells that have less available movement is best choice because it will tend to the deadlock in the future unless it is filled.选择可用移动较少的单元格是最好的选择,因为除非填充它,否则将来会趋于死锁。 There is an inverse relationship between allowed movement number and reach an impasse.允许移动次数与陷入僵局之间存在反比关系。 the outer cells is in the highest priority, As regards in a knight's tour problem the knight has to cross a cell only once, these value will be changed gradually in future travels.最外层的单元格是最高优先级,对于骑士的旅行问题,骑士只需要穿过一个单元格一次,这些值会在以后的旅行中逐渐改变。 Then in the next step a cell will be chose that has these conditions然后在下一步中将选择具有这些条件的单元格

  1. The number of its adjacent empty cells is less than others, or in the other words the probability to be filled is more它的相邻空单元的数量比其他的少,或者说被填充的概率更大
  2. After selecting, the adjacent houses doesn't going to deadlock选择后,相邻房屋不会陷入僵局

you can read my full article about this problem here Knight tour problem article你可以在这里阅读我关于这个问题的完整文章骑士之旅问题文章

and you can find the full source from here Full Source in GitHub你可以从这里找到完整的源代码 GitHub 中的完整源代码

I hope it will be useful我希望它会很有用

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM