N-皇后问题是可用回溯法解决的经典案例。而回溯法本质上基于递归思想,对所有可行的方案分支进行深度优先的探索,而对于验证失败的分支回溯到其他可能分支,或全部验证失败后pass。
本文将N-皇后问题进行拓展,衍生为 M × N M\times N M×N棋盘布局下的布局问题,各棋子间不能同行、同列或对角线。当 M = N M=N M=N时,即为严格意义上的N-皇后问题。
回溯法,或者更广义意义上的递归法的关键在于明确如下问题:
(1)函数的定位/意义;
(2)边界条件;
(3)出口条件;
(4)子问题间的迭代关系。
下面从两个不同的方向来解决N-皇后问题。
解法1:前向分解
这里所谓的前向,指的是迭代关系为 f ( n ) = g ( f ( n − 1 ) ) f(n)=g(f(n-1)) f(n)=g(f(n−1))。在N-皇后问题中即指:子问题的分解方向是沿着棋盘规模不断变小的方向。
值得注意的是:N-皇后( N × N N\times N N×N)的子问题并不应当理解为N-1-皇后( ( N − 1 ) × ( N − 1 ) (N-1)\times (N-1) (N−1)×(N−1)),而应当理解为棋盘布局为 ( N − 1 ) × N (N-1)\times N (N−1)×N的子问题。
在前向方法中,我们可以把函数定义为:从空棋盘开始到指定行的所有可能布局路径,其出口条件为:当棋局只有1行时的所有布局方式,而迭代关系为在子问题布局(即更小规模的函数)路径下,在下一行添加一个新点的所有可能路径。
基于上述理解,我们可以快速给出递归函数:
def conflict(point, state):
"""
point: 新point缩放的列
state: (), 之前各行放的棋子元组序列
新point所在位置:(len(state), point)
"""
for row, previous in enumerate(state):
if abs(point - previous) in (0, len(state) - row):
return True
return False
def queen(row, columns):
"""
row: 当前行
columns: 所有列
"""
if row == 1:
for c in range(columns):
yield (c, )
else:
for c in range(columns):
for sequence in queen(row-1, columns):
if not conflict(c, sequence):
yield sequence + (c, )
解法2:后向分解
与上文前向的定义相反,这里的后向指的是迭代关系为 f ( n − 1 ) = g ( f ( n ) ) f(n-1)=g(f(n)) f(n−1)=g(f(n))。在N-皇后问题中即指:子问题的分解方向是沿着棋盘规模不断变大的方向。
这里的规模变大或许让人费解,问题既然没有解决,那棋盘不断变大不是更难解决吗?
因此,这里需要明确,所谓的规模变大只具有实际问题背景下的意义,而我们在思考迭代关系时,应当捕捉的是随着子问题分解方向,不断变小的变量。所以,无论前向分解,还是后向分解,其本质思想还是一致的。以路线长度的计算为例,在前向分解中,我们从终点出发,不断将问题分解为靠近起点的子问题,我们关注的核心变量应当是起点距当前点的距离;而在后向分解中,我们将问题分级为距终点的子问题,那我们关注的核心变量应当是当前点距终点的距离。
所以按照后向分解的思路,我们可以把函数定义为:从给定棋盘状态下,距布满全局所有可能布局路径,其出口条件为:当棋局只剩最后1行时的所有不与前序布局冲突的落子方式,而迭代关系为在子问题布局(即对应更大棋盘规模的函数)路径下,在该行添加一个新点的所有可能路径。
基于上述理解,我们引入一个当前棋局的状态变量state,可以写出如下的迭代过程:
def conflict(point, state):
"""
point: 新point缩放的列
state: (), 之前各行放的棋子元组序列
新point所在位置:(len(state), point)
"""
for row, previous in enumerate(state):
if abs(point - previous) in (0, len(state) - row):
return True
return False
def queen(max_rows, columns, state=()):
"""
row: 当前行
columns: 所有列
"""
assert len(state) <= max_rows
if len(state) == max_rows - 1:
for c in range(columns):
if not conflict(c, state):
yield (c, )
else:
for c in range(columns):
if not conflict (c, state):
for sequence in queen(max_rows, columns, state=state+(c, )):
yield (c, ) + sequence
上述的queen
函数中在出口函数和迭代函数部分的判断部分有重复,为了阅读上的顺畅,这里不做简化,读者可自行进行简化。