ナイトツアーアルゴリズムをバックトラッキング を使用してC++でコーディングします=メソッド。しかし、n> 7(7 x 7チェス盤よりも大きい)の場合、速度が遅すぎるか、無限ループでスタックしているように見えます。
問題は、このアルゴリズムの時間計算量とは何ですか?どのように最適化できますか?!
ナイトツアーの問題は次のように述べることができます。
N×n個の正方形のチェス盤がある場合、すべての正方形を1回だけ訪れる騎士の道を見つけます。
これが私のコードです:
#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)の出力は次のようになります。
各ステップでチェックする可能性が8つあり、これは各セル(最後のセルを除く)に対して実行する必要があるため、このアルゴリズムの時間計算量はO(8 ^(n ^ 2-1))= O(8 ^( n ^ 2))ここで、nはチェックボードの端にある正方形の数です。正確には、これは最悪の場合の時間計算量です(何も見つからない場合、またはそれが最後の場合、すべての可能性を調査するのにかかる時間)。
最適化に関しては、2つのタイプの改善があります。
X-2、x-1、x + 1、x + 2を計算しており、yについては少なくとも2倍の時間で同じです。私はこのようなものを書き直すことを提案することができます:
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係数になります。
私はこれを考えることができます:
counter % 8 == 4
またはそれ以上のcounter > 2*n && counter % 8 == 4
をチェックします)。さようなら
アルゴリズムを調べます。再帰の各深さで、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()は非常に反復的であることに注意してください。実際には、ボードをmark()し、すべての合法的な動きを確認し、backtrack()のいずれもtrueを返さない場合は、場所をunmark()するだけです。
そして、関数を書き直すと、すべてが少し明確になります。
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]を訪問するために、再帰がどれほど深くなければならないかを考えてみてください。かなり深いですねしたがって、より効率的な戦略について考えたいと思うかもしれません。これらの2つのプリントアウトをボードディスプレイに追加すると、発生したバックトラックステップの数が表示されます。
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!
しかし、あなたの最初の質問は、
問題は:thisアルゴリズムの時間計算量とは何ですか?どうすればそれを最適化できますか?!
バックトラッキングアルゴリズムは約8 ^(n ^ 2)ですが、n ^ 2の移動で答えが見つかる場合があります。これをO()複雑度メトリックに変換します。
私はこれがあなたに答えを言わずにあなたを答えに導くと思います。
これが私の2セントです。私は基本的なバックトラッキングアルゴリズムから始めました。あなたが言ったように、それはn> 7を無期限に待っていました。 warnsdorff rule を実装しました。これは魔法のように機能し、n = 31までのサイズのボードで1秒未満の結果になります。n> 31の場合、再帰の深さが制限を超えたため、スタックオーバーフローエラーが発生していました。 。私はより良い議論を見つけることができました ここ これはwarnsdorffルールの問題と可能なさらなる最適化について話します。
参考までに、warnsdorff最適化を使用したKnight'sTour問題の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..'