1.暴力解法
# coding:utf-8 import time def permutation(arr): if len(arr) == 0 or len(arr) == 1: return [arr] result = [] for i in arr: temp_list = arr[:] temp_list.remove(i) temp = permutation(temp_list) for j in temp: j[0:0] = [i] result.append(j) return result # 判断这条路能不能实现 N 皇后问题,不能返回False,可行则返回True # 判断皇后之间是否在同一斜行,要判断向左和向右的情况,向左时斜率为 1 ,向右时斜率为 -1 def judge(row): # 左相位 left_phase_position_list = [] # 右相位 right_phase_position_list = [] for i in range(len(row)): # 斜率为 1 时,左相位等于 row[i] - i left_phase_position = row[i] - i left_phase_position_list.append(left_phase_position) if len(list(set(left_phase_position_list))) < len(row): return False for i in range(len(row)): # 斜率为 -1 时,右相位等于 row[i] + i right_phase_position = row[i] + i right_phase_position_list.append(right_phase_position) if len(list(set(right_phase_position_list))) < len(row): return False return True if __name__ == '__main__': start_time_1 = time.clock() start_time_2 = time.clock() count = 0 arr = range(10) full_permutations = permutation(arr) end_time_2 = time.clock() start_time_3 = time.clock() for i in range(len(full_permutations)): row = list(full_permutations[i]) if judge(row): count = count + 1 print row end_time_3 = time.clock() end_time_1 = time.clock() print "when N = ", len(arr), ",available way is ", count print "make a list need ", end_time_2 - start_time_2, "second" print "judge all way need ", end_time_3 - start_time_3, "seconds" print "all need ", end_time_1 - start_time_1, "second"
输出结果
2.回溯法
# coding:utf-8 import time COUNT = 0 class Node: def __init__(self, data): self.data = data self.child = [] # 判断这条路能不能实现 N 皇后问题,不能返回False,可行则返回True def judge(row): # 左相位 left_phase_position_list = [] # 右相位 right_phase_position_list = [] for i in range(len(row)): # 斜率为 1 时,左相位等于 row[i] - i left_phase_position = row[i] - i left_phase_position_list.append(left_phase_position) if len(list(set(left_phase_position_list))) < len(row): return False for i in range(len(row)): # 斜率为 -1 时,右相位等于 row[i] + i right_phase_position = row[i] + i right_phase_position_list.append(right_phase_position) if len(list(set(right_phase_position_list))) < len(row): return False return True def traverse(node, arr): global COUNT a = arr[:] if node.data != -1: a.append(node.data) if judge(a) == 0: return if len(node.child) == 0: COUNT = COUNT + 1 print a return for i in range(len(node.child)): traverse(node.child[i], a) # 以树的形式进行全排列 def permutation(node, arr): if len(arr) == 0: return temp = arr[:] # 找到与节点的数据相同的元素并在arr中删除 for i in range(len(temp)): if temp[i] == node.data and node.data != -1: temp.remove(temp[i]) break for i in range(len(temp)): child = Node(temp[i]) node.child.append(child) permutation(child, temp) if __name__ == '__main__': start_time_1 = time.clock() start_time_2 = time.clock() root = Node(-1) arr = range(10) permutation(root, arr) end_time_2 = time.clock() start_time_3 = time.clock() traverse(root, []) end_time_3 = time.clock() end_time_1 = time.clock() print "when N = ", len(arr), ",available way is ", COUNT print "make a tree need ", end_time_2 - start_time_2, "second" print "judge all way need ", end_time_3 - start_time_3, "seconds" print "all need ", end_time_1 - start_time_1, "second"
输出结果
分析:
回溯法对树进行操作,需要创建全排列树,由于树的元素是一个数据结构,创建过程较慢,暴力解法直接对数组操作,所以暴力方法创建容器的过程比回溯法快。但是创建的过程并不是关注点,判断的过程才是关注点。
回溯法计算树的所有可行路径比暴力方法计算容器中的可行数组的数量快很多。
回溯法只需0.5秒,而暴力方法需要13秒。
当N越大时,回溯法的优势更明显。
实际上对于回溯法,求解NQueen问题可以采用边创建节点边判断,如果不满足则不创建树节点的策略,这个过程实际上才是回溯法的真正执行时间。
此时修改回溯法的代码为
# coding:utf-8 import time COUNT = 0 # 棋盘的长度 arr_len = 13 class Node: def __init__(self, data): self.data = data self.child = [] # 判断这条路能不能实现 N 皇后问题,不能返回False,可行则返回True def judge(row): # 左相位 left_phase_position_list = [] # 右相位 right_phase_position_list = [] for i in range(len(row)): # 斜率为 1 时,左相位等于 row[i] - i left_phase_position = row[i] - i left_phase_position_list.append(left_phase_position) # 斜率为 -1 时,右相位等于 row[i] + i right_phase_position = row[i] + i right_phase_position_list.append(right_phase_position) if len(list(set(left_phase_position_list))) < len(row) or len(list(set(right_phase_position_list))) < len(row): return False return True # 以树的形式进行全排列,并且创建结点的同时判断这条路径可不可行 def permutation(node, arr, list): global COUNT global arr_len temp = arr[:] # 找到与节点的数据相同的元素并在arr中删除 for i in range(len(temp)): if temp[i] == node.data and node.data != -1: temp.remove(temp[i]) break for i in range(len(temp)): l = list[:] child = Node(temp[i]) node.child.append(child) l.append(child.data) # 添加子节点到路径,当此路径无法解决NQueen问题,移除这个子节点,跳过这次循环,添加下一个子节点到路径,如此循环 if judge(l) == 0: node.child.remove(child) continue # 当长度到达原数组长度时,此时必为 1 个可行的解 if len(l) == arr_len: COUNT = COUNT + 1 return permutation(child, temp, l) if __name__ == '__main__': start_time = time.clock() root = Node(-1) arr = range(arr_len) permutation(root, arr, []) end_time = time.clock() print "when N = ", arr_len, ",available way is ", COUNT print "all need ", end_time - start_time, "second"
输出结果为
总过程的时间缩短为1.5秒
3.位运算
# coding:utf-8 import time COUNT = 0 UPPER_LIM = 1 # 用 1 表示已经放,0 表示可以放皇后,row, ld, rd 分别表示行,左斜线,右斜线 def bit(row, ld, rd): global UPPER_LIM, COUNT if row != UPPER_LIM: # 逻辑或得到所有禁位,取反后得到可以放的位置,之后 1 代表可以放,0 代表不可以放了, pos = 0 时跳出循环,没有位置给皇后 pos = UPPER_LIM & ~(row | ld | rd) while pos: # 得到右边第一个 1 的位置,其他位置 0 ,也就是取得可以放皇后的最右边的列 p = pos & (~pos + 1) # 去掉最右边的列,为下一次遍历最右边的列做准备 pos -= p # row + p,将当前列置 1,表示记录这次皇后放置的列。 # (ld + p) << 1,标记当前皇后左边相邻的列不允许下一个皇后放置。 # (rd + p) >> 1,标记当前皇后右边相邻的列不允许下一个皇后放置。 # 此处的移位操作实际上是记录对角线上的限制,只是因为问题都化归到一行网格上来解决,所以表示为列的限制就可以了。 # 显然,随着移位在每次选择列之前进行,原来N×N网格中某个已放置的皇后针对其对角线上产生的限制都被记录下。 # 在这一层的递归里,依次遍历可以放置皇后的列,某次进入bit函数时遍历一列,出栈回到这次bit函数后遍历前一列 bit(row + p, (ld + p) << 1, (rd + p) >> 1) else: COUNT = COUNT + 1 if __name__ == '__main__': N = 10 start_time = time.clock() # 得到一个各位数全为 1 的数,长度为 N UPPER_LIM = (UPPER_LIM << N) - 1 bit(0, 0, 0) end_time = time.clock() print "when N = ", N, ",available way is ", COUNT print "all need ", end_time - start_time, "second"
可以看出位运算是最快的方法
和普通算法一样,这是一个递归过程,程序一行一行地寻找可以放皇后的地方。过程带三个参数,row、ld和rd,分别表示在纵列和两个对角线方向的限制条件下这一行的哪些地方不能放。我们以6×6的棋盘为例,看看程序是怎么工作的。假设现在已经递归到第四层,前三层放的子已经标在左图上了。红色、蓝色和绿色的线分别表示三个方向上有冲突的位置,位于该行上的冲突位置就用row、ld和rd中的1来表示。把它们三个并起来,得到该行所有的禁位,取反后就得到所有可以放的位置(用pos来表示)。前面说过-a相当于not a + 1,这里的代码第6行就相当于pos and (not pos + 1),其结果是取出最右边的那个1。这样,p就表示该行的某个可以放子的位置,把它从pos中移除并递归调用test过程。注意递归调用时三个参数的变化,每个参数都加上了一个禁位,但两个对角线方向的禁位对下一行的影响需要平移一位。最后,如果递归到某个时候发现row=111111了,说明六个皇后全放进去了,此时程序从第1行跳到第11行,找到的解的个数加一。
当前行为4:
当前行为5: