From Gossip@caterpillar

Algorithm Gossip: 騎士走棋盤

 說明

騎士旅遊(Knight tour)在十八世紀初倍受數學家與拼圖迷的注意,它什麼時候被提出已不可考,騎士的走法為西洋棋的走法,騎士可以由任一個位置出發,它要如何走完[所有的位置?

解法

騎士的走法,基本上可以使用遞迴來解決,但是純綷的遞迴在維度大時相當沒有效率,一個聰明的解法由J.C. Warnsdorff在1823年提出,簡單的說,先將最難的位置走完,接下來的路就寬廣了,騎士所要走的下一步,「為下一步再選擇時,所能走的步數最少 的一步。」,使用這個方法,在不使用遞迴的情況下,可以有較高的機率找出走法(找不到走法的機會也是有的)。

演算法

FOR(m = 2; m <= 總步數; m++) [
測試下一步可以走的八個方向,記錄可停留的格數count。

IF(count == 0) [
遊歷失敗
]
ELSE IF(count == 1) [
下一步只有一個可能
]
ELSE [
找出下一步的出路最少的格子
如果出路值相同,則選第一個遇到的出路。
]

走最少出路的格子,記錄騎士的新位置。
]

實作

#include <stdio.h> 

int board[8][8] = {0};

int main(void) {
int startx, starty;
int i, j;

printf("輸入起始點:");
scanf("%d %d", &startx, &starty);

if(travel(startx, starty)) {
printf("遊歷完成!\n");
}
else {
printf("遊歷失敗!\n");
}

for(i = 0; i < 8; i++) {
for(j = 0; j < 8; j++) {
printf("%2d ", board[i][j]);
}
putchar('\n');
}

return 0;
}

int travel(int x, int y) {
// 對應騎士可走的八個方向
int ktmove1[8] = {-2, -1, 1, 2, 2, 1, -1, -2};
int ktmove2[8] = {1, 2, 2, 1, -1, -2, -2, -1};

// 測試下一步的出路
int nexti[8] = {0};
int nextj[8] = {0};

// 記錄出路的個數
int exists[8] = {0};

int i, j, k, m, l;
int tmpi, tmpj;
int count, min, tmp;

i = x;
j = y;

board[i][j] = 1;

for(m = 2; m <= 64; m++) {
for(l = 0; l < 8; l++) {
exists[l] = 0;
}

l = 0;

// 試探八個方向
for(k = 0; k < 8; k++) {
tmpi = i + ktmove1[k];
tmpj = j + ktmove2[k];

// 如果是邊界了,不可走
if(tmpi < 0 || tmpj < 0 || tmpi > 7 || tmpj > 7)
continue;

// 如果這個方向可走,記錄下來
if(board[tmpi][tmpj] == 0) {
nexti[l] = tmpi;
nextj[l] = tmpj;
// 可走的方向加一個
l++;
}
}

count = l;

// 如果可走的方向為0個,返回
if(count == 0) {
return 0;
}
else if(count == 1) {
// 只有一個可走的方向
// 所以直接是最少出路的方向
min = 0;
}
else {
// 找出下一個位置的出路數
for(l = 0; l < count; l++) {
for(k = 0; k < 8; k++) {
tmpi = nexti[l] + ktmove1[k];
tmpj = nextj[l] + ktmove2[k];

if(tmpi < 0 || tmpj < 0 ||
tmpi > 7 || tmpj > 7) {
continue;
}

if(board[tmpi][tmpj] == 0)
exists[l]++;
}
}

tmp = exists[0];
min = 0;

// 從可走的方向中尋找最少出路的方向
for(l = 1; l < count; l++) {
if(exists[l] < tmp) {
tmp = exists[l];
min = l;
}
}
}

// 走最少出路的方向
i = nexti[min];
j = nextj[min];
board[i][j] = m;
}

return 1;
}
public class Knight {
public boolean travel(int startX,
int startY, int[][] board) {
// 對應騎士可走的八個方向
int[] ktmove1 = {-2, -1, 1, 2, 2, 1, -1, -2};
int[] ktmove2 = {1, 2, 2, 1, -1, -2, -2, -1};

// 下一步出路的位置
int[] nexti = new int[board.length];
int[] nextj = new int[board.length];

// 記錄出路的個數
int[] exists = new int[board.length];

int x = startX;
int y = startY;

board[x][y] = 1;

for(int m = 2; m <= Math.pow(board.length, 2); m++) {
for(int k = 0; k < board.length; k++) {
exists[k] = 0;
}

int count = 0;
// 試探八個方向
for(int k = 0; k < board.length; k++) {
int tmpi = x + ktmove1[k];
int tmpj = y + ktmove2[k];

// 如果是邊界了,不可走
if(tmpi < 0 || tmpj < 0 ||
tmpi > 7 || tmpj > 7) {
continue;
}

// 如果這個方向可走,記錄下來
if(board[tmpi][tmpj] == 0) {
nexti[count] = tmpi;
nextj[count] = tmpj;
// 可走的方向加一個
count++;
}
}

int min = -1;
if(count == 0) {
return false;
}
else if(count == 1) {
min = 0;
}
else {
// 找出下一個位置的出路數
for(int l = 0; l < count; l++) {
for(int k = 0; k < board.length; k++) {
int tmpi = nexti[l] + ktmove1[k];
int tmpj = nextj[l] + ktmove2[k];

if(tmpi < 0 || tmpj < 0 ||
tmpi > 7 || tmpj > 7) {
continue;
}

if(board[tmpi][tmpj] == 0)
exists[l]++;
}
}

int tmp = exists[0];
min = 0;

// 從可走的方向中尋找最少出路的方向
for(int l = 1; l < count; l++) {
if(exists[l] < tmp) {
tmp = exists[l];
min = l;
}
}
}

// 走最少出路的方向
x = nexti[min];
y = nextj[min];
board[x][y] = m;
}

return true;
}

public static void main(String[] args) {
int[][] board = new int[8][8];
Knight knight = new Knight();

if(knight.travel(
Integer.parseInt(args[0]),
Integer.parseInt(args[1]), board)) {
System.out.println("遊歷完成!");
}
else {
System.out.println("遊歷失敗!");
}

for(int i = 0; i < board.length; i++) {
for(int j = 0; j < board[0].length; j++) {
if(board[i][j] < 10) {
System.out.print(" " + board[i][j]);
}
else {
System.out.print(board[i][j]);
}
System.out.print(" ");
}
System.out.println();
}
}
}