如何优化Knight的巡演算法?

时间:2013-10-06 21:20:35

标签: c++ algorithm optimization time-complexity backtracking

我使用 Knight's tour 方法在c ++中对 Backtracking 算法进行编码。 但它似乎太慢或陷入无限循环n> 7(大于7乘7棋盘)。

问题是:此算法的 Time complexity 是什么?我该如何优化它?!


骑士之旅的问题可以说明如下:

鉴于棋盘上有n×n个方块,找到一个骑士的路径,每个方格只能访问一次。

这是我的代码:

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

}

上面示例(n = 7)的输出如下:

enter image description here

4 个答案:

答案 0 :(得分:4)

因为在每个步骤中你有8种可能性需要检查,并且必须对每个单元格(减去最后一个单元格)进行检查,这个算法的时间复杂度为O(8 ^(n ^ 2-1))= O( 8 ^(n ^ 2))其中n是棋盘边缘的方格数。确切地说,这是最坏的情况时间复杂性(如果没有找到所有可能性或者它是最后一个,那么探索所有可能性所花费的时间)。

至于优化,可以有两种类型的改进:

实施改进

你正在计算x-2,x-1,x + 1,x + 2,并且y的计算时间至少为两倍。 我可以建议改写这样的事情:

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]);
}

请注意在后续块中重复使用预先计算的值。 我发现这比我想象的更有效;这意味着变量赋值和调用比再次执行操作更快。 另请考虑在构造函数中保存sm1 = s - 1;area = s * s;,而不是每次计算。

然而,这(实现改进而不是算法改进)不会改变时间复杂度顺序,而只会将时间除以某个因子。 我的意思是时间复杂度O(8 ^(n ^ 2))= k * 8 ^(n ^ 2),差异将是一个较低的k因子。

算法改进

我能想到这一点:

  • 对于从对角线中的单元格开始的每个巡视(如在示例中的(0,0)开始),您可以仅考虑由对角线创建的两个半棋盘中的一个上的第一个移动。
    • 这是simmetry的beacouse或它存在2个simmetric解决方案或没有。
    • 对于这种情况,这给出了O(4 * 8 ^(n ^ 2-2)),但对于非同步的情况,它仍然是相同的。
    • 再次注意O(4 * 8 ^(n ^ 2-2))= O(8 ^(n ^ 2))
  • 如果一些全球情况表明,鉴于目前的标记,解决方案是不可能的,那么
  • 尽量中断抢先一步。
    • 例如,马不能跳过两个大量的列或行,所以如果你有两个批量标记的列(或行)和两侧没有标记的单元格,你肯定没有解决方案。考虑到如果你更新每个col /行的标记单元数,可以在O(n)中检查这个。然后,如果你在每个标记之后检查这个,你加上O(n * 8 ^(n ^ 2))时间,如果n&lt; = 8.解决方法根本不是检查alwais,而是每个n / 8标记(检查counter % 8 == 4例如或更好counter > 2*n && counter % 8 == 4
  • 找到其他想法以便尽早巧妙地中断搜索,但请记住,带有8个选项的回溯算法总是具有O(8 ^(2 ^ n))的性质。

再见

答案 1 :(得分:2)

检查你的算法。在递归的每个深度,您检查8个可能的移动中的每一个,检查板上的哪些移动,然后递归地处理该位置。什么数学公式最能描述这种扩张?

你有一个固定的电路板尺寸,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;
        }
    }
}

通过添加检查电路板移动是否合法的功能来稍微改变测试,

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

请注意,mark()和unmark()是非常重复的,你真的只需要标记()板,检查所有合法的移动,然后取消标记()如果没有backtrack()返回true,< / p>

重写函数会让一切变得更清晰,

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

现在,想想每次访问[x] [y]时递归的深度是多少?相当深,是吧? 所以,您可能想要考虑一种更有效的策略。将这两个打印输出添加到电路板显示屏应该会显示发生了多少回溯步骤,

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++;
    ...

以下是5x5,6x6,7x6,

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

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

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

为什么7比5更少的步骤?考虑回溯中移动的顺序 - 如果更改顺序,步骤会改变吗?如果您列出了可能的移动[{1,2},{ - 1,2},{1,-2},{ - 1,-2},{2,1},{2,1},该怎么办? ,{2,1},{2,1}],并以不同的顺序处理它们?我们可以更轻松地重新排序,

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

将原始移动序列更改为此序列,并运行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!

但你最初的问题是,

问题是:算法的时间复杂度是多少?我该如何优化它?!

回溯算法大约是8 ^(n ^ 2),尽管它可以在n ^ 2次移动之后找到答案。我会让你把它转换成O()复杂度指标。

我认为这会引导您找到答案,而不会告诉您答案。

答案 2 :(得分:2)

这是我的2美分。我从基本的回溯算法开始。它无限期地等待n> 7如你所说。我实现了warnsdorff rule,它就像魔术一样,对于大小不等的板,在n = 31之前给出结果不到一秒。对于n> 31,当递归深度超过限制时,它给出了stackoverflow错误。我可以找到一个更好的讨论here,它讨论了warnsdorff规则的问题以及可能的进一步优化。

仅供参考,我提供我的python实现Knight's Tour问题与warnsdorff优化



    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..'


答案 3 :(得分:0)

这是一个新的解决方案:

在这种方法中,使用棋盘上骑士下一次移动时的死锁概率预测,将选择一个趋向于使死锁概率小于其他死锁概率的动作,我们首先知道此死锁每个单元的概率为零,并且将逐渐改变。棋盘上的骑士有2到8步棋,因此每个像元都有下一步的预定值。

选择活动较少的单元格是最佳选择,因为除非填充,否则将来可能会陷入僵局。允许的运动次数与达到僵局之间存在反比关系。 对于骑士的旅行问题,骑士只需要穿过一个小室一次,这些价值在以后的旅行中将逐渐改变。 然后,下一步将选择具有这些条件的单元格

  1. 其相邻的空单元格的数量少于其他单元格,换句话说,被填充的可能性更大
  2. 选择后,相邻房屋不会死锁

您可以在此处阅读有关此问题的全文 Knight tour problem article

,您可以在此处找到完整的源代码 Full Source in GitHub

我希望它会有用