上一篇我们讲解了八皇后的问题:八皇后
这一篇我们将继续聊迷宫棋盘类问题。八皇后问题我们使用一维数组模拟二维数组,以获得较好的效率。接下来,我们将考虑使用二维数组求解迷宫棋盘类问题的解。其中比较典型的是跳马问题。
马在中国象棋以日字形规则移动。
请编写一段程序,给定5×5大小的棋盘,以及马的初始位置(0,0),要求不能重复经过棋盘上的同一个点,计算马可以有多少途径遍历棋盘上的所有点。
八皇后问题好像我们讨巧了,我们使用一维数组换来了二维数组的功能。这种事情少之又少,我们不能总是遇到这样的情况。我说过,当二维数组中的某一个位置只有true
或者false
时,它是二维的信息,用一维数组就够了。但我也很少遇到这样的情况,八皇后是不多见的其中之一。
我们现在终于需要面对二维数组了。二维数组并没有我们想象中那么麻烦,甚至写多了还有一丝轻松,毕竟想这么单纯的思路,在算法里是不多见的。我们来看看吧。
一匹马在棋盘上会跳向何方?我们并不知晓,我们只好列出它的所有可能,我们每一个找一遍,总会找到它合适的位置。
马走日。如果马处于棋盘的正中央,它有8
种走法。这些走法该怎么表示呢?这些走法是用该变量表示的。举一个例子,从马(红色)当前位置跳到a
处,它是行减少2
,列减少1
达成的。因此,我们可以使用-2,-1
来表示马跳到a
的方向。按照这样的方式,我们可以得到以下表格:
方向 | 表示方式 |
---|---|
a | -2,-1 |
b | -2, 1 |
c | -1, 2 |
d | 1, 2 |
e | 2, 1 |
f | 2,-1 |
g | 1,-2 |
h | -1,-2 |
我们能看到每一个方向由两个数字组成。因此我们可以使用8*2
的二维数组表示。8
表示8
个方向,2
表示两个下标的改变,如d[0][0]
表示第1
个方向中的行下标
。同样的道理,d[0][1]
表示第1
个方向的列下标
。这样的话,我们就可以使用循环访问到每一个方向了。
跳马问题要求马跳满每一个格子,那么对于5*5
的棋盘来说,跳满25
个格子程序就可以结束了。这个作为程序的终止条件,原因是由于跳的方向不同,我们根本不知道结束位置在什么位置,因此我们只能选择跳的次数了。如果在迷宫问题中,终止条件可能就是迷宫的出口了。
与八皇后稍有不同的是,马会在不同位置之间跳动。因此可能会跳到之前走过的位置,为了避免这种情况,我们将跳过的格子标记下来。当下一次跳到这个位置,程序可以直接跳过。
我们尝试描述一下题解的过程。
int arr[5][5] = {}; //棋盘,标记跳过的格子,使用跳的次数进行标记
int d[8][2] = {
{-2,-1},
{-2, 1},
{-1, 2},
{ 1, 2},
{ 2, 1},
{ 2,-1},
{ 1,-2},
{-1,-2}
};//八个方向
int total = 0; //跳法种类
// sx 表示当前位置的行下标
// sy 表示当前位置的列下标
// index 表示当前跳的次数,跳满25次程序结束
void f(int sx, int sy, int index)
{
if(index > 25)
{//跳满25次,表明棋盘所有位置都跳满了,跳法种类增加1
++total;
return;
}
//还未跳满
//并不知道该跳向哪个方向
//8个方向都试一遍
for(int i = 0;i < 8; ++i)
{
//检查跳向该方向之后的位置是否有效:如是否越界,是否跳过等
int x = sx + d[i][0]; //跳过之后的行下标
int y = sy + d[i][1]; //跳过之后的列下标
if(x >= 0 && x < 5 && y >= 0 && y < 5)
{//下一个位置未越界
if(arr[x][y] == 0) //未跳过的位置
{
arr[x][y] = index; //将该位置标记为跳过了
f(x, y, index + 1); //从这个位置向下一个位置跳
arr[x][y] = 0; //递归回溯,恢复未跳过的状态
}
}
}
}
int main()
{
arr[0][0] = 1; //假设马的起始位置为跳了第一次
f(0, 0, 2);
cout << total << endl;
}
这里需要说明的一点是,我们将跳过的位置标记为跳的次数,并将马的初始位置标记为第一次跳。这是为了让跳过的位置保持非0
值,让未跳过的位置保持0
值。当然,我们也可以选择其他的方式进行标记,这并不是限定的。其中容易遗漏的语句是arr[x][y] = 0
,将跳过的位置恢复未跳过的状态。这是后续多种跳法的保证。