N 皇后与约束传播
使用列集合和对角线集合在 O(1) 时间内检查约束,将 N 个皇后放置在 N×N 棋盘上,并讨论如何统计解的数量与枚举解。
N 皇后与约束传播 是 CoddyKit 上的免费 DSA Interview Prep 课时。 这是第 4 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 DSA Interview Prep 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 DSA Interview Prep 课程共包含 4 节课。
N 皇后问题
N 皇后问题(LeetCode 51/52)要求您在 N×N 的棋盘上放置 N 个皇后,使任意两个皇后都无法相互攻击。皇后可以沿行、列以及两条对角线攻击。对于 N=4,恰好有 2 个解。对于 N=8(经典版本),有 92 个解。这是最经典的回溯问题,通过检查约束可以大幅剪枝搜索空间。
# N-Queens constraints:
# 1. Exactly one queen per row
# 2. No two queens in the same column
# 3. No two queens on the same diagonal (top-left to bottom-right)
# 4. No two queens on the same anti-diagonal (top-right to bottom-left)
# For N=4, the 2 solutions:
sol1 = ['.Q..', '...Q', 'Q...', '..Q.']
sol2 = ['..Q.', 'Q...', '...Q', '.Q..']
print('N=4 solutions:')
for row in sol1: print(row)
print()
for row in sol2: print(row)每行放置一个皇后
由于任意两个皇后不能位于同一行,我们在每行恰好放置一个皇后。回溯按行递归,为每一行选择一列。这样就把每个皇后 N² 个选择的搜索空间减少为每行只有 N 列,形成 N^N 个初始分支——但约束会大幅减少实际搜索量。递归深度为 N(每行一层),分支因子最多为 N。
def solve_n_queens(n):
results = []
queens = [] # queens[row] = column of queen in that row
def backtrack(row):
if row == n:
# Build the board representation
board = []
for r in range(n):
board.append('.' * queens[r] + 'Q' + '.' * (n - queens[r] - 1))
results.append(board)
return
for col in range(n):
if is_valid(row, col):
queens.append(col) # CHOOSE
backtrack(row + 1) # EXPLORE
queens.pop() # UNCHOOSE
def is_valid(row, col):
for r, c in enumerate(queens):
if c == col: return False # same column
if abs(row - r) == abs(col - c): return False # diagonal
return True
backtrack(0)
return results
print(len(solve_n_queens(4)), 'solutions for N=4') # 2
print(len(solve_n_queens(8)), 'solutions for N=8') # 92使用集合进行 O(1) 约束检查
通过扫描所有已放置的皇后来检查有效性时,每个候选项需要 O(N) 的时间,最坏情况下整个算法为 O(N² × N!)。通过维护三个集合,可以将每次有效性检查降为 O(1):cols(已占用的列)、diag(左上到右下对角线的行−列值)和 anti_diag(右上到左下对角线的行+列值)。位于同一条对角线上的皇后具有相同的行−列值;位于同一条反对角线上的皇后具有相同的行+列值。
def solve_n_queens_fast(n):
results = []
cols = set() # occupied columns
diag = set() # row - col (positive diagonal)
anti = set() # row + col (negative diagonal)
queens = []
def backtrack(row):
if row == n:
board = ['.' * c + 'Q' + '.' * (n-c-1) for c in queens]
results.append(board)
return
for col in range(n):
if col in cols or (row-col) in diag or (row+col) in anti:
continue # PRUNE: constraint violated
# CHOOSE
cols.add(col); diag.add(row-col); anti.add(row+col); queens.append(col)
backtrack(row + 1) # EXPLORE
# UNCHOOSE
cols.remove(col); diag.remove(row-col); anti.remove(row+col); queens.pop()
backtrack(0)
return results
print(len(solve_n_queens_fast(8))) # 92对角线不变量详解
对角线的关键规律是:同一条从左上到右下的对角线上的所有单元格,都具有相同的 row - col 值。例如,(0,0)、(1,1)、(2,2) 的 row-col=0。所有位于同一条反对角线上的单元格,都具有相同的 row + col 值:(0,2)、(1,1)、(2,0) 的 row+col=2。这些常数时间不变量让我们可以通过 O(1) 的集合查找来检查对角线冲突,而不必进行 O(N) 的线性扫描。
# Visualise the diagonal invariants for a 4x4 board
n = 4
print('row-col values (same diagonal):')
for r in range(n):
print([r-c for c in range(n)])
print('row+col values (same anti-diagonal):')
for r in range(n):
print([r+c for c in range(n)])
# Verify: (0,0) and (2,2) share diag value 0
print('(0,0) diag:', 0-0, '| (2,2) diag:', 2-2) # both 0
# Verify: (0,2) and (2,0) share anti-diag value 2
print('(0,2) anti:', 0+2, '| (2,0) anti:', 2+0) # both 2统计解的数量:N 皇后 II
N 皇后 II(LeetCode 52)只要求返回解的数量,而不是棋盘布局。这允许进行小幅优化:跳过棋盘构造步骤,只需递增计数器。使用位掩码代替集合,还可以将每次操作的速度进一步提升到接近 O(1)。解的数量并不单调增长:1(N=1)、0(N=2)、0(N=3)、2(N=4)、10(N=5)、4(N=6)、40(N=7)、92(N=8)。
def total_n_queens(n):
count = [0]
cols = set(); diag = set(); anti = set()
def backtrack(row):
if row == n:
count[0] += 1
return
for col in range(n):
if col in cols or (row-col) in diag or (row+col) in anti:
continue
cols.add(col); diag.add(row-col); anti.add(row+col)
backtrack(row + 1)
cols.remove(col); diag.remove(row-col); anti.remove(row+col)
backtrack(0)
return count[0]
for n in range(1, 11):
print(f'N={n}: {total_n_queens(n)} solutions')使用位掩码加速 N 皇后
对于非常大的 N,位掩码实现的运行速度会明显更快。使用三个整数作为位掩码:cols、left_diag(每行向左移位)和 right_diag(每行向右移位)。可用列为 ((1<<n)-1) & ~(cols|left_diag|right_diag)。使用 bit = available & -available 提取每个可用列(最低位的置位比特),然后进行递归。通过位运算,这种方法可以将每次约束检查的复杂度降为 O(1)。
def total_n_queens_bitmask(n):
full = (1 << n) - 1 # all n columns set
count = [0]
def bt(cols, left_diag, right_diag):
if cols == full:
count[0] += 1
return
available = full & ~(cols | left_diag | right_diag)
while available:
bit = available & -available # lowest set bit
available &= available - 1 # remove lowest bit
bt(cols | bit,
(left_diag | bit) << 1,
(right_diag | bit) >> 1)
bt(0, 0, 0)
return count[0]
for n in range(1, 13):
print(f'N={n}: {total_n_queens_bitmask(n)}')约束传播概念
约束传播 不只是简单的剪枝:放置一个皇后后,立即推导并排除未来各行中的所有无效位置。这比检查每个候选项的有效性更加积极——您会在进行分支前主动缩小搜索空间。最著名的例子是 SAT 求解器和数独求解器中的弧一致性:放置一个数字后,会排除同一行、同一列和同一个 3×3 宫中的其他选项。
# Constraint propagation in Sudoku:
# After placing 5 in cell (0,0):
# - Row 0: no other cell can have 5
# - Column 0: no other cell can have 5
# - Box (0,0)-(2,2): no other cell can have 5
# This is propagated BEFORE branching further
# Simple demo: remaining valid columns after placing queens
def remaining_columns(n, queens):
cols = set(q for q in queens)
diags = set(r - q for r, q in enumerate(queens))
anti_diags = set(r + q for r, q in enumerate(queens))
row = len(queens)
return [c for c in range(n)
if c not in cols
and (row-c) not in diags
and (row+c) not in anti_diags]
print(remaining_columns(8, [0])) # valid cols for row 1 after placing col 0 in row 0数独求解器
数独是约束传播问题的典型代表。在每个空单元格中,有效数字选项必须是同一行、同一列或同一个 3×3 宫中尚未出现的数字。回溯求解器的步骤是:找到第一个空单元格,尝试每个有效数字,然后递归求解。如果遇到矛盾(空单元格中没有任何有效数字),就回溯。优秀的数独求解器还会在回溯前应用约束传播(候选数标记)。
def solve_sudoku(board):
def is_valid(r, c, num):
for i in range(9):
if board[r][i] == num: return False # row
if board[i][c] == num: return False # col
br, bc = (r//3)*3, (c//3)*3
for i in range(3):
for j in range(3):
if board[br+i][bc+j] == num: return False # box
return True
def backtrack():
for r in range(9):
for c in range(9):
if board[r][c] == '.':
for d in '123456789':
if is_valid(r, c, d):
board[r][c] = d
if backtrack(): return True
board[r][c] = '.'
return False # no valid digit found
return True # no empty cells: solved
backtrack()
return board
# Mini test with a solvable board (simplified)
print('Sudoku solver implemented')最受约束变量启发式方法
这是约束满足问题的一项关键优化:始终优先选择受约束最多的变量(有效选项最少的单元格)。在数独中,如果某个单元格只有 1 个有效数字,那么立即填入它是必然的选择——无需回溯。优先选择这类单元格可以显著降低搜索树的深度。这就是人工智能约束编程中的最小剩余值(MRV)启发式方法。
def solve_sudoku_mrv(board):
'''Find cell with fewest valid choices (MRV heuristic).'''
def valid_choices(r, c):
nums = set('123456789')
for i in range(9):
nums.discard(board[r][i])
nums.discard(board[i][c])
br, bc = (r//3)*3, (c//3)*3
for i in range(3):
for j in range(3):
nums.discard(board[br+i][bc+j])
return nums
def find_mrv():
best = (10, -1, -1, set()) # (choices_count, r, c, choices)
for r in range(9):
for c in range(9):
if board[r][c] == '.':
choices = valid_choices(r, c)
if len(choices) < best[0]:
best = (len(choices), r, c, choices)
return best[1], best[2], best[3]
def backtrack():
r, c, choices = find_mrv()
if r == -1: return True # no empty cells
for d in choices:
board[r][c] = d
if backtrack(): return True
board[r][c] = '.'
return False
backtrack()
return boardN 皇后解数量表
N 皇后的解数量遵循以下著名序列:N=1:1,N=2:0,N=3:0,N=4:2,N=5:10,N=6:4,N=7:40,N=8:92,N=9:352,N=10:724。目前尚未发现闭式公式;必须通过计算得到数量。对于 N=27,大约存在 2.34 × 10^17 个解。面试题通常要求 N ≤ 9。理解这种指数级增长,有助于说明为什么对于较大的 N,位掩码优化非常重要。
def count_queens(n):
'''O(1) per constraint check using sets.'''
count = [0]
cols = set(); diag = set(); anti = set()
def bt(row):
if row == n: count[0] += 1; return
for col in range(n):
if col in cols or (row-col) in diag or (row+col) in anti: continue
cols.add(col); diag.add(row-col); anti.add(row+col)
bt(row+1)
cols.discard(col); diag.discard(row-col); anti.discard(row+col)
bt(0)
return count[0]
sequence = [count_queens(n) for n in range(1, 12)]
print('N-Queens counts:', sequence)
# [1, 0, 0, 2, 10, 4, 40, 92, 352, 724, 2680]N 皇后棋盘构造
当面试官要求返回实际棋盘(LeetCode 51)时,请根据 queens 列表构造每个棋盘,其中 queens[r] 表示第 r 行中皇后的列。字符串构造方式是:对每一行使用 '.' * col + 'Q' + '.' * (n - col - 1)。这种 O(n²) 的构造只会在递归树的叶节点处调用(即所有 N 个皇后都已放置),因此不会影响总体复杂度。
def n_queens_boards(n):
results = []
queens = []
cols = set(); diag = set(); anti = set()
def build_board():
return ['.' * c + 'Q' + '.' * (n-c-1) for c in queens]
def bt(row):
if row == n:
results.append(build_board())
return
for col in range(n):
if col in cols or (row-col) in diag or (row+col) in anti: continue
cols.add(col); diag.add(row-col); anti.add(row+col); queens.append(col)
bt(row+1)
cols.remove(col); diag.remove(row-col); anti.remove(row+col); queens.pop()
bt(0)
return results
for board in n_queens_boards(4):
for row in board: print(row)
print()快速检查
测试您对本课数据结构与算法——编程面试准备相关概念的理解。
课程回顾
在本课中,您学习了:N 皇后问题每行放置一个皇后,并使用集合记录列、对角线(行-列)和副对角线(行+列),从而以 O(1) 检查约束;位掩码可以进一步加速约束检查,使探索所有放置方案时每次操作的开销接近 O(1);以及约束传播(MRV 启发式方法)通过始终优先选择受约束最多的变量来减少搜索。接下来,我们将比较贪心与 DP 方法,并学习何时应用其中一种。
常见问题解答
「N 皇后与约束传播」课时是免费的吗?
是的 — 「N 皇后与约束传播」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 DSA Interview Prep 课程的其余内容,请升级到 CoddyKit PRO。 DSA Interview Prep 课程共包含 4 节课。
「N 皇后与约束传播」这节课中我会学到什么?
使用列集合和对角线集合在 O(1) 时间内检查约束,将 N 个皇后放置在 N×N 棋盘上,并讨论如何统计解的数量与枚举解。 你通过在浏览器中直接运行的动手代码来练习 DSA Interview Prep,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 DSA Interview Prep 需要有经验吗?
无需任何先前经验。CoddyKit 上的 DSA Interview Prep 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 4 节课,共 4 节。
「N 皇后与约束传播」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 DSA Interview Prep 课中编写并运行代码吗?
能。每节 DSA Interview Prep 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- 回溯模板:选择、探索、撤销
- 子集与幂集
- 排列与组合
- N 皇后与约束传播