web-dev-qa-db-ja.com

ナイトのツアーアルゴリズムを最適化する方法は?

ナイトツアーアルゴリズムをバックトラッキング を使用して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)の出力は次のようになります。

enter image description here

19
Kasra

各ステップでチェックする可能性が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係数になります。

アルゴリズムの改善

私はこれを考えることができます:

  • 対角線のセルで始まる各ツアー(例のように(0,0)で始まるように)では、最初の動きだけが対角線によって作成された2つのハーフチェックボードの1つにあると見なすことができます。
    • これは、simmetryが原因であるか、2つのsimmetricソリューションが存在するか、まったく存在しません。
    • これにより、その場合はO(4 * 8 ^(n ^ 2-2))が得られますが、非対称の場合も同じです。
    • ここでも、O(4 * 8 ^(n ^ 2-2))= O(8 ^(n ^ 2))であることに注意してください。
  • 現在のマーキングでは解決が不可能であることが何らかのグローバル条件で示唆されている場合は、ラッシュを早期に中断してみてください。
    • たとえば、馬は2つのバルク列または行をジャンプできないため、2つのバルクマーク列(または行)とマークされていないセルが両側にある場合、解決策はありません。更新された列/行ごとにマークされたセルの数を維持する場合は、これをチェックインできることを考慮してくださいO(n)次に、各マークの後にこれをチェックすると、O(n * 8 ^(n ^ 2))n <= 8の場合、悪くない時間。回避策は、単にalwaisをチェックするのではなく、n/8のマーキングごとにチェックすることです(たとえば、counter % 8 == 4またはそれ以上のcounter > 2*n && counter % 8 == 4をチェックします)。
  • 検索を早期に巧妙に中断する他のアイデアを見つけますが、8つのオプションを備えたバックトラックアルゴリズムは常にO(8 ^(2 ^ n))であるという性質を持っていることを忘れないでください。

さようなら

4
Diego Mazzaro

アルゴリズムを調べます。再帰の各深さで、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
ChuckCottrill

これが私の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..'

2
hari