前两篇的内容重点放在递
、归
的功能和作用上递归回溯的过程
这一篇我们聊一聊回溯法在迷宫棋盘类问题中的应用吧。一般来说,这种迷宫棋盘类的问题都是使用二维数组来求解。但又是也有例外。这是一个典型的例外——八皇后问题。那么,关于这类问题,我们在什么情况下选择一维数组,什么情况下选择二维数组呢?这跟数组的功能有关系。一般来说,一维数组中的元素最多可以表示两层含义。第一层由数组中的元素提供,第二层由数组的下标提供。比如说,对于数组int arr[4] = {1,2,3,4}
来说,arr[3]
得到的4
可以表示一层含义,其中的下标3
也可以表示一层含义。而二维数组则能表达三个含义——两个下标和一个值。接下来我们来查看八皇后问题。
在8*8的国际象棋棋盘上放下八个皇后。要求八个皇后任意两个不能在同一行,同一列,同一对角线上。求一共有多少种摆法,每一种摆法又是如何摆放的?
从题干中我们了解到棋盘是8*8
的,因此大家第一反应是创建一个二维数组来存放。这是可以的。但我们也了解到,二维数组的计算量远远大于一维数组,我们会思考能否使用一维数组来求解呢?我们知道,一个皇后在一行内只会放在一个位置——两个皇后不能在同一行。那么,也就是说,对于一行中的皇后我们只需要知道列就可以了。这样的话,我们可以使用一维数组来表示皇后的位置——下标表示行数,数组值表示列数。例如:arr[3] = 4
的意思是第4
行,第5
列的位置有一个皇后。
现在我们知道如何存放皇后的信息了。于是我们可以从第一行开始摆皇后,第一行放好之后,会接着放第二行…依次进行,直到第8
行的皇后也放好即可。那对于每一行的皇后该放在什么位置呢?我们并不了解,我们选择将每一个位置都尝试一遍,如果满足条件,则保留这个位置,否则换成下一个位置。
我们尝试使用伪代码的方式描述这个题解。
int arr[8] = {}; //存放皇后信息,arr[0] 存放的是第0行,第arr[0]列的皇后
void f(int row) //参数表示放到了第row行
{
if(row == 8) //8行都已经放完
{
//将完整的摆法输出
//使用0表示没有皇后,1表示皇后
for(int i = 0;i < 8; ++i)
{
for(int j = 0; j < 8; ++j)
if(j == arr[i])
cout << 1 << " ";
else
cout << 0 << " ":
cout << endl;
}
return;
}
for(int i = 0;i < 8; ++i) //皇后可以放在0~7的任何一列
{
arr[row] = i; //第row行第i列的位置放上皇后
if(满足放皇后的条件)
f(row + 1); //下一行
}
}
在程序中,我们使用循环从0
到7
的每一列都尝试放一遍皇后,如果满足,则开始放下一行的皇后。如果不满足条件,循环会继续将i
的值覆盖在arr[row]
的位置,也就相当于更换了列数。那么关键的判断满足皇后的条件该如何描述呢?我们需要从下标寻找规律——一般二维数组的规律都是从下标寻找。
-
题目中要求,任意两个皇后不能在同一行,同一列,同一对角线。也就是如图中皇后(红色)位置周围的绿色、蓝色、粉红色、黄色的位置都是不能放置皇后的。那么这该如何使用程序表达呢?我们一个一个查看。
-
1、同一行。我们使用一维数组来表示八皇后的信息,其中
下标
表示行,arr[row]
表示列。而一维数组中arr[row]
位置只能存放一个元素,因此,是不可能造成同一行有两个皇后的。除非arr[row]
的位置可以存放两个元素。 -
2、同一列。我们了解到
arr[row]
表示列数。那么也就要求arr[0]、arr[1]、....arr[row]
之间不能相同。这个判断是比较简单的。我们尝试描述一下。
for(int i = 0; i < row; ++i)
if(arr[i] == arr[row])
return false; //列相同,表明不能放置
return true; //列没有相同,表明可以放置
-
这段代码中需要注意的是,只有所有的列都满足条件,才能返回
-
3、同一对角线。对角线其实包含两条,一条是从左上角到右下角的。如图:
true
表明能够放置,而不是满足了一个列数不相同就返回
true
。
那么在这个图中,如何使用程序表达在同一个对角线上呢?我们尝试找到对应的下标。
位置 | 行下标 | 列下标 |
---|---|---|
皇后(红色) | 4 | 3 |
a | 2 | 1 |
b | 7 | 6 |
从下标关系来看,我们了解到,行下标
与列下标
的相等的两个位置,它们在同一对角线上。4-3
等于2-1
。
for(int i = 0;i < row; ++i)
if(i - arr[i] == row - arr[row]) //从左上角到右下角的对角线
return false;
return true;
-
这里值得注意的是,程序只需要判断到
-
4、同一对角线。从左下角到右上角的对角线。如图:
有了上一个图的经验,我们同样列一个表格来表示行下标和列下标的规律吧。
row
行即可,毕竟大于
row
的行还没有放置皇后呢。
位置 | 行下标 | 列下标 |
---|---|---|
皇后(红色) | 2 | 1 |
a | 0 | 3 |
b | 3 | 0 |
这个规律从a
和b
之间就很好发现了,a
的行下标和列下标之和与b
的行下标和列下标之和相等。1+2
也等于0+3
。因此我们可以这样描述:
for(int i = 0;i < row; ++i)
if(i + arr[i] == row + arr[row]) //在同一对角线,从左下角到右上角
return false;
return true;
终于,我们将四种情况都说明清楚了,我们可以使用一个函数表示放置皇后是否满足条件。
bool isOk(int row)
{
for(int i = 0;i < row; ++i)
{
if(arr[i] == arr[row]) //列相同
return false;
if(i - arr[i] == row - arr[row]) //从左上角到右下角
return false;
if(i + arr[i] == row + arr[row]) //从左下角到右上角
return false;
}
return true;
}
通过这样的方式,八皇后摆放的问题基本上就已经解决了。我们使用递归回溯的方式,将每一个位置尝试一遍,如果位置不冲突,则确定放置,否则,我们换到下一个位置。这样保证了程序的有效性。