(一)回溯法
回溯法是求解 N 皇后问题的经典算法,详细可见 Leetcode 第 51 题,及其简化版第 52 题。
本文提供一种最经典的回溯法的 Python 代码。
from datetime import datetime
class NQueensBacktrack(object):
"""
N queens problem, backtracking
"""
def __init__(self, size: int):
"""
backtracking, initialisation
:param size: size of the chessboard, 'N'
"""
self.size = size
""" result """
# final result
self.list_result = []
# row state: columns index of queens in each row
self.list_state = [-1 for _ in range(self.size)]
def run(self):
"""
backtracking, run
:return: nothing
"""
dt_start = datetime.now()
self._run_recursion(row=0)
# print all the solutions
for s in self.list_result:
print(s)
print()
print("Number of solutions found: {}".format(len(self.list_result)), '\n')
dt_end = datetime.now()
tm_run = round((dt_end - dt_start).seconds + (dt_end - dt_start).microseconds / (10 ** 6), 3)
print("backtrack total time: {} s".format(tm_run), '\n')
def _run_recursion(self, row: int):
"""
backtracking, recursion
:param row: current row index
:return: nothing
"""
# end condition
if row == self.size:
list_solution = []
for i in range(self.size):
list_row = []
for j in range(self.size):
if j == self.list_state[i]:
list_row.append(1)
else:
list_row.append(0)
list_solution.append(list_row)
self.list_result.append(list_solution)
return
# recursion body
for column in range(self.size):
if self._is_valid(row=row, column=column):
self.list_state[row] = column
self._run_recursion(row=row + 1)
self.list_state[row] = -1
def _is_valid(self, row: int, column: int) -> bool:
"""
judge valid for a cell
:param row: row index of current cell
:param column: column index of current cell
:return: if current cell is valid
"""
for i in range(row):
if (self.list_state[i] == column) or (abs(row - i) == abs(column - self.list_state[i])):
return False
return True
if __name__ == '__main__':
N = 8
print('\n')
n_queens_backtrack = NQueensBacktrack(size=N)
n_queens_backtrack.run()
上述代码采用的是递归的实现方式,对应的也可以通过非递归的方式实现,此处不赘述。
此外,还有一种位运算的算法较为高效,但笔者在答题时发现,位运算算法似乎比上述算法稍微慢一些,不确定经验是否准确,有关位运算算法的代码可见 Leetcode 上的答案。
(二)约束规划
N皇后问题可以看作一个整数规划问题,但如果没有优化目标,只是为了获取所有可行解,则称之为约束规划。
OR-Tools CP-SAT 求解器提供了约束规划的求解方法。本文提供调用该求解器,获取N皇后问题的所有可行解的 Python 代码。
from datetime import datetime
from typing import List
from ortools.sat.python import cp_model
class VarMatrixSolutionPrinter(cp_model.CpSolverSolutionCallback):
"""
print intermediate solutions, and get total solution count
"""
def __init__(self, variables: List[List[cp_model.IntVar]], size: int):
"""
initialisation
:param variables: variable matrix
:param size: size of the matrix
"""
cp_model.CpSolverSolutionCallback.__init__(self)
self.variables = variables
self.size = size
# total solution count
self.solution_count = 0
def on_solution_callback(self):
"""
print intermediate solutions, and update total solution count
:return: nothing
"""
self.solution_count += 1
# print intermediate solutions
for i in range(self.size):
for j in range(self.size):
print('%s=%i' % (self.variables[i][j], self.Value(self.variables[i][j])), end=' ')
print()
class NQueensCPModel(object):
"""
N queens problem, solving by CP model
"""
def __init__(self, size: int):
"""
CP model, initialisation
:param size: size of the chessboard, 'N'
"""
self.size = size
def run(self):
"""
CP model, run
:return: nothing
"""
dt_start = datetime.now()
model = cp_model.CpModel()
x = [[model.NewBoolVar(name='x_[{},{}]'.format(i, j)) for j in range(self.size)] for i in range(self.size)]
""" constraints """
# cons 1: number of queens
model.Add(sum(x[i][j] for j in range(self.size) for i in range(self.size)) == self.size)
# cons 2: no row conflicts
for i in range(self.size):
model.Add(sum(x[i][j] for j in range(self.size)) <= 1)
# cons 3: no column conflicts
for j in range(self.size):
model.Add(sum(x[i][j] for i in range(self.size)) <= 1)
# cons 4: no diagonal conflicts, left bottom to right top diagonals
for s in range(0, 2 * (self.size - 1) + 1):
model.Add(sum(x[i][j] if i + j == s else 0 for i in range(self.size) for j in range(self.size)) <= 1)
# cons 5: no diagonal conflicts, left top to right bottom diagonals
for d in range(self.size - 1, - (self.size - 1) - 1, -1):
model.Add(sum(x[i][j] if i - j == d else 0 for i in range(self.size) for j in range(self.size)) <= 1)
""" solutions """
solver = cp_model.CpSolver()
solution_printer = VarMatrixSolutionPrinter(variables=x, size=self.size)
status = solver.SearchForAllSolutions(model, solution_printer)
print()
print('Status = %s' % solver.StatusName(status))
print()
print('Number of solutions found: %i' % solution_printer.solution_count, '\n')
dt_end = datetime.now()
tm_model = round((dt_end - dt_start).seconds + (dt_end - dt_start).microseconds / (10 ** 6), 3)
print("CP model total time: {} s".format(tm_model), '\n')
if __name__ == '__main__':
N = 8
print('\n')
n_queens_cp_model = NQueensCPModel(size=N)
n_queens_cp_model.run()
最后简述一下求解效率,笔者在 i5-6200 2.30GHz 2.40GHz CPU 和 8GB 内存的笔记本电脑上运行上述两个程序,回溯法的运行时间大概是 0.03s,而约束规划模型的运行时间大概是 0.3s,慢了约十倍,不过约束规划的思想还是蛮有意义的。
相关资料:
OR-Tools Constraint Optimization: