这道题和「51. N 皇后」非常相似,区别在于,第 51题需要得到所有可能的解,这道题只需要得到可能的解的数量。因此这道题可以使用第 51题的做法,只需要将得到所有可能的解改成得到可能的解的数量即可。
皇后的走法是:可以横直斜走,格数不限。因此要求皇后彼此之间不能相互攻击,等价于要求任何两个皇后都不能在同一行、同一列以及同一条斜线上。
直观的做法是暴力枚举将 N个皇后放置在 N×N的棋盘上的所有可能的情况,并对每一种情况判断是否满足皇后彼此之间不相互攻击。暴力枚举的时间复杂度是非常高的,因此必须利用限制条件加以优化。
显然,每个皇后必须位于不同行和不同列,因此将 N个皇后放置在 N×N的棋盘上,一定是每一行有且仅有一个皇后,每一列有且仅有一个皇后,且任何两个皇后都不能在同一条斜线上。基于上述发现,可以通过回溯的方式得到可能的解的数量。
回溯的具体做法是:依次在每一行放置一个皇后,每次新放置的皇后都不能和已经放置的皇后之间有攻击,即新放置的皇后不能和任何一个已经放置的皇后在同一列以及同一条斜线上。当 N个皇后都放置完毕,则找到一个可能的解,将可能的解的数量加 1。
由于每个皇后必须位于不同列,因此已经放置的皇后所在的列不能放置别的皇后。第一个皇后有 N列可以选择,第二个皇后最多有 N?1列可以选择,第三个皇后最多有 N?2列可以选择(如果考虑到不能在同一条斜线上,可能的选择数量更少),因此所有可能的情况不会超过 N!种,遍历这些情况的时间复杂度是 O(N!)。
为了降低总时间复杂度,每次放置皇后时需要快速判断每个位置是否可以放置皇后,显然,最理想的情况是在 O(1)的时间内判断该位置所在的列和两条斜线上是否已经有皇后。
以下两种方法分别使用集合和位运算对皇后的放置位置进行判断,都可以在 O(1)的时间内判断一个位置是否可以放置皇后,算法的总时间复杂度都是 O(N!)。
方法一:基于集合的回溯
为了判断一个位置所在的列和两条斜线上是否已经有皇后,使用三个集合 columns和 diagonals1、diagonals2? 分别记录每一列以及两个方向的每条斜线上是否有皇后。
列的表示法很直观,一共有 N列,每一列的下标范围从 0到 N?1,使用列的下标即可明确表示每一列。
如何表示两个方向的斜线呢?对于每个方向的斜线,需要找到斜线上的每个位置的行下标与列下标之间的关系。
方向一的斜线为从左上到右下方向,同一条斜线上的每个位置满足行下标与列下标之差相等,例如 (0,0)和 (3,3)在同一条方向一的斜线上。因此使用行下标与列下标之差即可明确表示每一条方向一的斜线。
方向二的斜线为从右上到左下方向,同一条斜线上的每个位置满足行下标与列下标之和相等,例如 (3,0)和 (1,2) 在同一条方向二的斜线上。因此使用行下标与列下标之和即可明确表示每一条方向二的斜线。
每次放置皇后时,对于每个位置判断其是否在三个集合中,如果三个集合都不包含当前位置,则当前位置是可以放置皇后的位置。
方法二:基于位运算的回溯
方法一使用三个集合记录分别记录每一列以及两个方向的每条斜线上是否有皇后,每个集合最多包含 N 个元素,因此集合的空间复杂度是 O(N)。如果利用位运算记录皇后的信息,就可以将记录皇后信息的空间复杂度从 O(N)降到 O(1)。
具体做法是,使用三个整数 columns、diagonals1\textit{diagonals}_1diagonals1? 和 diagonals2分别记录每一列以及两个方向的每条斜线上是否有皇后,每个整数有 N个二进制位。棋盘的每一列对应每个整数的二进制表示中的一个数位,其中棋盘的最左列对应每个整数的最低二进制位,最右列对应每个整数的最高二进制位。
那么如何根据每次放置的皇后更新三个整数的值呢?在说具体的计算方法之前,首先说一个例子。
棋盘的边长和皇后的数量 N=8。如果棋盘的前两行分别在第 2列和第 4列放置了皇后(下标从 0开始),则棋盘的前两行如下图所示。
如果要在下一行放置皇后,哪些位置不能放置呢?我们用 0代表可以放置皇后的位置,1代表不能放置皇后的位置。
新放置的皇后不能和任何一个已经放置的皇后在同一列,因此不能放置在第 2列和第 4列,对应 columns=00010100(2)。新放置的皇后不能和任何一个已经放置的皇后在同一条方向一(从左上到右下方向)的斜线上,因此不能放置在第 4列和第 5列,对应 diagonals1=00110000(2)?。其中,第 4 列为其前两行的第 2 列的皇后往右下移动两步的位置,第 5列为其前一行的第 4列的皇后往右下移动一步的位置。新放置的皇后不能和任何一个已经放置的皇后在同一条方向二(从右上到左下方向)的斜线上,因此不能放置在第 0列和第 3 列,对应 diagonals2=00001001(2)。其中,第 0列为其前两行的第 2列的皇后往左下移动两步的位置,第 3列为其前一行的第 4列的皇后往左下移动一步的位置。
由此可以得到三个整数的计算方法:
??? 初始时,三个整数的值都等于 0,表示没有放置任何皇后;
??? 在当前行放置皇后,如果皇后放置在第 i 列,则将三个整数的第 i个二进制位(指从低到高的第 i个二进制位)的值设为 1;
??? 进入下一行时,columns的值保持不变,diagonals1 左移一位,diagonals2右移一位,由于棋盘的最左列对应每个整数的最低二进制位,即每个整数的最右二进制位,因此对整数的移位操作方向和对棋盘的移位操作方向相反(对棋盘的移位操作方向是 diagonals1右移一位,diagonals2左移一位)。
每次放置皇后时,三个整数的按位或运算的结果即为不能放置皇后的位置,其余位置即为可以放置皇后的位置。可以通过 (2^n?1) & (~(columns∣diagonals1∣diagonals2))得到可以放置皇后的位置(该结果的值为 1的位置表示可以放置皇后的位置),然后遍历这些位置,尝试放置皇后并得到可能的解。
遍历可以放置皇后的位置时,可以利用以下两个按位与运算的性质:
??? x & (?x)可以获得 x的二进制表示中的最低位的 1的位置;
??? x & (x?1)可以将 x的二进制表示中的最低位的 1置成 0。
具体做法是,每次获得可以放置皇后的位置中的最低位,并将该位的值置成 0,尝试在该位置放置皇后。这样即可遍历每个可以放置皇后的位置。