世界上有万分之5的天才;他们降生的那天就基本注定要去清华;他们靠着早慧和智商碾压别人,你别不信:人家搞定数学竞赛的时候你在干什么?人家IMO姚期智的时候你看得懂“八皇后”的题干么?人与人的差别比“人与狗”还要大,只是多数人一开始都以为自己是那个人。
——曾博:我与清华的差距在哪里
一、 原题
八皇后问题是一道很经典的算法题,题目大致如下:
在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问一共有多少种摆法。
二、四年前我的想法
这道题是我在大四时看知乎时第一次遇到。说来可笑,那时候没有学过任何的算法 ,所了解的编程语言只有在材料学院学了半年的Matlab。那天花了两个多小时做完这道题,我不禁喊出曾博先生那句话:我是傻逼!
四年前的我的第一想法是尝试把这个问题变成一个数学问题,应该存在某种递推关系,尝试推出其递推关系,然后进行面向结果编程,但是显然失败,没有推出来。这个思路某种意义上不能说错,后文也会介绍。
第二想法是这个问题进行简化,首先先满足前面两个条件即:任意两个皇后都不能处于同一行、同一列。那么这个说法等价于一个八阶的单位矩阵只进行行置换或者列置换,就是这个问题的所有解。只要遍历里面所有置换,再判断是否有皇后处于同一对角线上。
Queen = [1 0 0 0 0 0 0 0;
0 1 0 0 0 0 0 0;
0 0 1 0 0 0 0 0;
0 0 0 1 0 0 0 0;
0 0 0 0 1 0 0 0;
0 0 0 0 0 1 0 0;
0 0 0 0 0 0 1 0;
0 0 0 0 0 0 0 1]
再经过进一步简化,由于我现在只进行列置换,所以此时我只需要记录一下每列皇后出现的位置,即数组a[1,2,3,4,5,6,7,8],这个数组其实记录了原来单位矩阵的所有信息,对原来矩阵进行列置换也等价于对这个数组的两个元素之间的交换。这样我完全可以本来二维的问题,转化为一个一维的问题。
最后尝试将“不能处于同一斜线”转化为数学语言,即|a[i]-a[j]|!=|i-j|.。
好了,接下来就是编码。第一反应是枚举,众所周知数组a进行交换之后共有8!情况,但是其中绝大多数的情况是没必要一一试验的,譬如前面交换之后俩数字是2,3,那么后面的数字无论如何排列,都无法符合要求。
以下就是我当时写的代码:
function eight_queens
a = [1, 2, 3, 4, 5, 6, 7, 8];
n = length(a);
results = [];
results = permuteAndCheck(a, 1, n, results);
disp(size(results, 1));
end
function results = permuteAndCheck(a, startIdx, n, results)
if startIdx == n
if isValid(a, n)
results = [results; a];
disp(a);
end
else
for i = startIdx:n
a([startIdx i]) = a([i startIdx]);
if isValid(a, startIdx)
results = permuteAndCheck(a, startIdx + 1, n, results);
end
a([startIdx i]) = a([i startIdx]);
end
end
end
function is_valid = isValid(a, startIdx)
is_valid = true;
for i = 1:startIdx - 1
if abs(a(i) - a(startIdx)) == abs(i - startIdx)
is_valid = false;
break;
end
end
end
现在来看这是一个典型的递归回溯算法,总体的流程如下:
-
初始化:创建一个为1到8的整数序列。该数组的索引代表棋盘的行,而数组中的值代表对应行中皇后的列位置。
-
生成排列:从数组的第一个元素开始,递归地与后面的元素进行交换,生成所有可能的排列。每次递归调用会处理数组的一个子序列。
-
约束条件检查:在递归函数中,每次交换后立即检查交换是否违反了斜线上是否有两个皇后。这个检查确保了只有有效的排列才会被进一步扩展。
-
回溯:如当前部分排列不满足约束条件,则不继续在这个方向上进行进行排列,而是回退到前一个状态并尝试其他交换。
-
找到全排列:当一个排列从头到尾全部检查并满足所有约束条件时,这个排列代表一个有效的棋盘布局。
-
终止条件:当所有排列都生成并检查完毕之后,算法终止。
虽然在没学过算法的情况下解出来有点兴奋,但又看到勃勃的一段话:
别和我说现在让你去做奥数你也能用微积分全解了:社会对一个人达到一定智力水平所花时间的要求,是很苛刻的。一个50岁的人哪怕IMO满分,也想必无人问津。因为人类是一个害怕时间的生物,你只有那么一次,就是在高中时代证明自己的天才;你没有,那说明你不是。这也是为什么高考这么重要的道理。说到底,高考为什么这么重要,是因为我们都会死。你在跑步中摔了一跤,社会却已经没空把你慢慢扶起。
——曾博:我与清华的差距在哪里
不禁潸然泪下。
三、现在的想法
1.利用对称性进行减枝
返回棋盘来看,棋盘是一个对称性很强的图案。那么我完全可以利用棋盘的对称性进行剪枝。这里仅仅以列的对称性为例而言进行减枝:
function eight_queens
n = 8; %
a = 1:n;
results = [];
half = floor(n / 2);
mirror = mod(n, 2) == 0;
if ~mirror
% 对于奇数N,需考虑中间的列
half = half + 1;
end
for i = 1:half % 只在前半部分遍历
a([1 i]) = a([i 1]);
results = permuteAndCheck(a, 2, n, results, mirror);
a([1 i]) = a([i 1]);
end
disp(size(results, 1));
end
function results = permuteAndCheck(a, startIdx, n, results, mirror)
if startIdx == n
if isValid(a)
results = [results; a];
if mirror
results = [results; mirrorSolution(a)];
end
end
else
for i = startIdx:n
a([startIdx i]) = a([i startIdx]);
if isValid(a(1:startIdx))
results = permuteAndCheck(a, startIdx + 1, n, results, mirror);
end
a([startIdx i]) = a([i startIdx]); % 回溯
end
end
end
function is_valid = isValid(a)
n = length(a);
is_valid = true;
for i = 1:n
for j = i+1:n
if abs(a(i) - a(j)) == abs(i - j) || a(i) == a(j)
is_valid = false;
return;
end
end
end
end
function mirrored = mirrorSolution(a)
n = length(a);
mirrored = a;
for i = 1:n
mirrored(i) = n - a(i) + 1;
end
end
注意的是,虽然我们可以单独或者同时使用镜像或者旋转对称进行减枝,但是在处理的过程种可能出现重复的情况,有时候反而不妙了。
2. 通过使用Set来校验斜对角是否存在皇后
我们注意到,每个棋子两个方向的斜线,其完全可以用(a[i]+i)和(a[i]-i)表示。所以完全可以把之前通过遍历纵坐标和横坐标的绝对值之差是否相同,修改将先前的皇后(a[i]+i)和(a[i]-i)放到两个set中。在Set中去查找是否存在相同的斜列的值。这样虽然需要新开辟空间,但是时间复杂度由O(n)减少到O(1),这能提升算法的效率,特别是对于较大的 N 值。但是相较于回溯算法本身恐怖的O(n!),这点减少似乎也是微不足道的。
但是需要注意的是在Matlab中,并没有直接等同于其他编程语言(如Python的set或Java的HashSet)的“集合”数据类型,就可以将containers.Map凑合着用,代码我也懒得写了
3. 数学上的一些想法
一开始拿到这种题目,我以为能直接得出一个高阶的线性递推公式,然后解一个特征方程,就能获得通项(材料狗只能想到这些高中内容)。实际上我是navie了
这篇回答种给出在n*n的棋盘上放置k个皇后的递推,在k为8的情况下,递推方程已经高达400多阶。
另外对于n皇后的可解性的证明也是比较有趣的:
Constructions for the Solution of the m Queens Problem.pdf
4. 其他关于n皇后问题的方法
https://sites.google.com/site/nqueensolver/home/algorithm-results
最后,我要喊出勃学名言:
人,生而失败。