问题:
n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
给定一个整数 n,返回 n 皇后不同的解决方案的数量。
- 按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法。
- 回溯是深度优先搜索的一种
皇后问题的解的个数:
| 问题规模 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 解的个数 | 0 | 1 | 0 | 0 | 2 | 10 | 4 | 40 | 92 | 352 | 724 | 2680 | 14200 |
循环+递归
获得解的列表
Queen(foundK, queenStore)接受参数是一个数foundK(表示已经找到了foundK个皇后,现在要找索引为foundK的皇后,也就是第foundK+1个皇后),和一个列表queenStore(其长度与皇后问题规模N相同)。queenStore列表的前foundK个元素,表示已经找到的foundK个皇后在其所在行的位置。- 返回值是皇后问题的解(N个皇后的摆放方案)的列表,如果没有则返回空列表。
- 子函数
_check(idx, foundQs)接受参数是一个数idx(假定第foundK+1个皇后在其所在行的位置)和一个列表foundQs(已经找到的foundK个皇后的列表),返回值表示第foundK+1个皇后放在i这个位置是否成立。 - 进入分支之前用
_check(idx, foundQs)进行审核,审核通过的才会进入分支。- 边界判断语句
if k > N or k < 0 or N == 0: return res,只要调用最初调用Queen(foundK, queenStore)的参数没问题,这个判断语句就不会起作用。
- 边界判断语句
1 | def Queen(foundK, queenStore): |
用Queen(foundK, queenStore)求解6皇后问题的用法:
1 | N = 6 |
获得解的个数
如果只要返回解的数量,而非具体的摆放皇后的方案,则可将上述代码改成如下版本:
- 如果参数有误则返回0,如果找到一个解(最后一行,且能放置皇后)则返回1,其它情况(所在位置放置了皇后,且不在最后一行)则各分支的返回值求和并返回。
1 | def Queen(foundK, queenStore): |
纯递归
纯递归
- 只用递归,不用循环。
纯递归与循环+递归都是深度优先,区别在于前者搜索路径上任意一点在往下搜索时不记录该点的其它分支,搜索路径是一条线,而后者通过循环结构记录了其它分支,其搜索路径是一棵树。 - 相比
循环+递归方案,纯递归方案的递归层数大大增加,而且一直累加,问题规模大于6时就发生栈溢出而导致该方案不可用。前者的栈的深度等于皇后问题的规模N,而后者的上限可能是\(N^N\)。 - Queen(foundK, idx,
pathStore)返回值是方案个数,三个输入参数是搜索所需要的全部信息。这三个输入参数是
- 一个数foundK\(\in [0, 1, \cdots, N]\)(表示已经找到了foundK个皇后,现在要找行索引为foundK的皇后,也就是第foundK+1个皇后),
- idx是要检测的第foundK行的\(idx\in [0, 1, \cdots, N-1]\)位置,
- 一个列表queenStore(其长度与皇后问题规模N相同),
- Queen函数的递归调用形成一个不断增长的栈,每个栈上有个值用于标记该栈上产生的解的数量(0个或1个),直到搜索完毕。搜索完毕后不断出栈,同时将抛出栈的解的数量累加并将该值往调用处传递,如此则最初调用Queen函数处的返回值就是皇后问题的解的数量。
- 当某一层已经搜索到最后一个位置且审核未通过,或者搜索到最后一层的最后一个位置(通过与否无所谓),就要用
_goback(foundK, queenStore)往之前的层回退,直到遇到第一个该层皇后的位置不是该行最后一个位置的层,继续搜索。如果这样的层不存在,则开启出栈过程。
1 | def Queen(foundK, idx, queenStore): |
用法,求五皇后问题的解的个数:
1 | N = 5 |
尾递归
如果既要用纯递归,又要摆脱栈溢出问题,只能采用尾递归。尾递归就是操作的最后一步是调用自身的递归。它和一般的递归不同在对内存的占用,普通递归创建stack累积而后计算收缩,尾递归只会占用恒量的内存(和迭代一样)。与普通递归相比,调用递归函数是用return语句,另外搜索状态完全由递归函数的参数决定,而不依赖函数参数之外的参数。
- Python语言不直接支持尾递归,因此写了个装饰器
@tail_call_optimized修饰Queen函数,用以手工操作尾递归的栈数据。 - 与上边的纯递归版本相比,尾递归版本的所有的
Queen函数递归调用都是直接将其本身执行return。只好定义一个引用变量(单元素列表)记录找到的解的数量,作为递归函数的全局变量。
1 | def tail_call_optimized(g): |
用法,求八皇后问题的解的个数:
1 | N = 8 |
纯循环
尾递归仍然进行大量栈操作,效率不佳。好在尾递归可以很容易改造成纯循环,大大降低栈的开销。
- 尾递归改成纯循环的方法:主要在每个循环体开始时把递归函数需要的几个参数准备好即可,也就是在尾递归的递归调用处,更新参数并用continue进入下一个循环体。循环体共用一个全局变量来统计解的个数。
_goback(foundK, queenStore)无返回值时,打破循环,返回解的数量。
1 | def Queen(foundK, idx, queenStore): |