当前位置: 代码迷 >> 综合 >> “Beginning Python”(二)The eight queens
  详细解决方案

“Beginning Python”(二)The eight queens

热度:37   发布时间:2024-01-09 07:11:52.0

    “The eight queens”(八个皇后)是一个经典的推理游戏。参与者需要在一个8?8的(国际象棋)棋盘上安放8个皇后,并使她们互不威胁,即没有任何两个个皇后位于同一行或同一列或同一对角线上(no two queens share the same row, column, or diagonal)。

    Wiki: https://en.wikipedia.org/wiki/Eight_queens_puzzle

                                                   

一、算法

    对于“the eight queens”问题的典型解法是应用“trackbacking”(回溯算法)。它的核心思想是:“选择 -> 验证 -> 前进 / 回退”。形象的理解如下:

    To draw a parallel from real life, imagine that you have an important meeting to attend. You’re not sure where it
is, but you have two doors in front of you, and the meeting room has to be behind one of them. You choose the left and step through. There, you face another two doors. You choose the left, but it turns out to be wrong. So you backtrack, and choose the right door, which also turns out to be wrong (excuse the pun). So, you backtrack again, to the point where you started, ready to try the right door there.


二、问题抽象

1,状态描述(state representation)

    首先,我们要将8个皇后的位置用程序语言来描述。很明显,它们是一个序列,我们选择用“list”来描述它们。这个list中的元素序数index表示棋盘的行数,元素自身的值表示棋盘的列数。如上图,可以描述为:

    state = [5, 3, 6, 0, 7, 1, 4, 2]

注意,我们选择从0开始计数。此外,这种序列的表示方法,已经隐含了“各个皇后不在同一行”的意思。

    从坐标系的角度来看这个list:

                                 

    如上图所示,横轴是X轴,纵轴是Y轴,各点的坐标即代表各个皇后在棋盘的位置。如果将上述坐标系沿X轴对折,将能直观的与棋盘对应。


2,验证

    一当list中有新的元素加入时,需要验证新元素是否和原list冲突。如果不冲突就继续加新元素(前进),如果冲突则逐级回退。因此,如何定义“冲突”至关重要。在此,我们把它抽象为一个函数:

def conflict(state, nextX):nextY = len(state)for i in range(nextY):if abs(state[i]-nextX) in (0, nextY-i):return Truereturn False

上面代码中这个表达式比较巧妙:

        abs(state[i]-nextX) in (0, nextY-i)

它包含了两层意义:

1)state[i] -nextX == 0

    即新加入的元素与原list中的某个元素列数相同(在同一列)

2)abs(state[i] -nextX) == nextY - i

    这个式子变换一下:abs(state[i] -nextX)/ (nextY - i) == 1

    即新元素和原list中的任一元素两点之间的连线,斜率为+1或-1,也就是说在同一对角线上。

注:这个表达式的写法反映了python程序的优雅。membership函数“in”可以让python跳出一维的限制,从多维的角度来进行运算。


3,递归(recursion)

    “The eight queens”可以用递归算法来求解。关于递归算法,我在此做一下扩展:

    递归算法的基本思想就是,将对大规模问题的求解转化为结构相似的规模更小的问题的求解。比如将规模为N的问题用规模为N-1的问题来表示。从编程的角度简单来说,就是函数内调用自身函数。

1)典型的递归算法

    在这里我举两个典型的递归算法问题:阶乘和Fibonacci数列。

    N!=  (N-1)! ? N,这个就是阶乘的递归公式。

    Fib(n) = Fib(n-1) + Fib(n-2),这个是Fibonacci数列的递归公式。

    除了递归公式,还有一个重要的点就是“递归结束条件”。从递归公式可以看出,随着递归层次的增加,递归逐渐深入,待求解问题的规模不断变小,但是,什么时候结束递归呢?

    这就要用到递归结束条件,也称为“the base case”(最简情况)。

    0!= 1,这个是阶乘的递归结束条件。

    Fib(1) = 1, Fib(0) = 0,这个是扩展的Fibonacci数列的“the base case”。完整的定义如下:

def fibs(num):result = [0, 1]for i in range(num-2):result.append(result[-2] + result[-1])return result


注:关于recursion在《Beginning Python》的第五章 - “Abstraction” 中进行了重点描述。其中对递归函数给出了两个要求,或者说是一个有效的递归函数的两个组成部分:

? A base case (for the smallest possible problem) when the function returns a value
directly
? A recursive case, which contains one or more recursive calls on smaller parts of the
problem

再给一个二分法搜索函数:

def search(sequence, number, lower, upper):if lower == upper:assert number == sequence[upper]return upperelse:middle = (lower + upper) // 2if number > sequence[middle]:return search(sequence, number, middle+1, upper)else:return search(sequence, number, lower, middle)


2)the base case

    对于“八个皇后”问题的“the base case”是:

def queens(num, state):if len(state) == num-1:for pos in range(num):if not conflict(state, pos):yield pos

    从上面的函数可以看出,前面num-1个皇后的位置已经找好,现在要放下最后一个皇后。递归迭代到最底层,它走的是一个特殊分支,不再进行更多的迭代。


3)the recursive case

    

    ...else:for pos in range(num):if not conflict(state, pos):for result in queens(num, state + (pos,)):yield (pos,) + result

这段代码要这样来看:总共有num个皇后,已排好num-N个,还剩N个待安排位置。每迭代一次,看似state列表增长了,实际上是待安置的皇后个数N变小了。


三、完整程序

    经过上面的分析,可以发现复杂的“八个皇后”问题只需要两个函数就可以求解:

import randomdef conflict(state, nextX):nextY = len(state)for i in range(nextY):if abs(state[i]-nextX) in (0, nextY-i):return Truereturn Falsedef queens(num=8, state=()):for pos in range(num):if not conflict(state, pos):if len(state) == num-1:yield (pos,)else:for result in queens(num, state + (pos,)):yield (pos,) + resultelse: pass

    其中,queens函数重写了,更加精炼。最后那一行是我为了方便理解加上去的,可以删除。

    queens函数不是一般的递归函数,它还包含了回溯算法(backtracking)。

    这个“backtracking”体现在:在递归迭代的过程中,它不像普通的递归迭代,单向的向深处迭代(每次迭代,N逐渐变小),然后单向返回,比如之前说的“阶乘”和“Fibonacci数列”。“backtracking”在迭代的过程中,有进有退,它在前向迭代的过程中,如果遇到“死胡同”,比如在第n次迭代时(棋盘的第n行),无论这个皇后放在0-7中的任何一格,都会与之前的那些皇后中的一个产生冲突,那么,算法会退回到前一次迭代(n-1行),并选择其他格。同样的,如果遍历了n-1行,也不行,算法还会继续回退。

    下面,我以简单的4个皇后为例,演示这个过程:

                                                     


    上图将从第一次迭代到找到一组合适的数据的所有过程,画图如上,横轴表示迭代次序,纵轴表示迭代层次(深度)。从这个图可以清晰的看出,迭代的过程不是单向的。注意:每个子迭代过程,都是从0开始(纵轴)。


四、补充

1,generator

    queens函数复杂的地方,除了“递归”和“回溯”,还有就是:它使用了generator。关于generator,主要是在第7章引入的,它的关键词是“yeild”。

    “yeild”类似“return”,不同的是:“yeild”并不会终止函数体的执行流,而只是暂时冻结,等执行完“generator”所在语句后,继续执行之前冻结的执行流。

    事实上,queens函数的终止,并不是因为“yeild”,而是因为“for”语句。for循环结束,从而结束整个函数。理解这一点尤为重要。

    You’ve seen that a generator is a function that contains the keyword yield. When it is called, the code in the function body is not executed. Instead, an iterator is returned. Each time a value is requested, the code in the generator is executed until a yield or a return is encountered. A yield means that a value should be yielded. A return means that the generator should stop executing (with-out yielding anything more; return can be called without arguments only when used inside a generator).

    这段话将generator比作一个函数。“yeild”不会终止程序执行流,“return”会,queens函数中的for结束,隐含了“return”。

    In other words, generators consist of two separate components: the generator-function and the generator-iterator. The generator-function is what is defined by the def statement con-taining a yield. The generator-iterator is what this function returns. In less precise terms, these two entities are often treated as one and collectively called a generator.

    这段话解释了generator的内部机制,generator是函数和迭代器的复合体。


2,花式打印

    原书为了清楚演示“the eight queens”,设计了一个花式打印程序:

def prettyprint(solution):def line(pos, length=len(solution)):return '. ' * (pos) + 'X ' + '. ' * (length-pos-1)for pos in solution:print(line(pos))prettyprint(random.choice(list(queens(4))))

效果如下: