N-ควีนและการเผยแพร่ข้อจำกัด
วางควีน N ตัวบนกระดานขนาด N×N โดยใช้เซตคอลัมน์และเส้นทแยงมุมเพื่อตรวจสอบข้อจำกัดในเวลา O(1) และอภิปรายความแตกต่างระหว่างการนับกับการแจกแจงคำตอบ
N-ควีนและการเผยแพร่ข้อจำกัด เป็นบทเรียน DSA Interview Prep ฟรีบน CoddyKit นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน 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) ล้วนมีค่าแถว-คอลัมน์=0 ช่องทั้งหมดบนแนวทแยงย้อนกลับเดียวกันจะมีค่า row + col เท่ากัน เช่น (0,2), (1,1), (2,0) ล้วนมีค่าแถว+คอลัมน์=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 เดียวกัน ตัวแก้แบบย้อนกลับจะทำดังนี้: ค้นหาช่องว่างช่องแรก ลองใส่ตัวเลขที่ถูกต้องทีละตัว แล้วเรียกตัวเองซ้ำ หากพบข้อขัดแย้ง (ช่องว่างที่ไม่มีตัวเลขที่ถูกต้องเลย) ให้ backtrack ตัวแก้ซูโดกุที่ดีจะใช้การเผยแพร่ข้อจำกัด (เครื่องหมายดินสอ) ก่อนการย้อนกลับด้วย
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 boardตารางจำนวนคำตอบของปัญหา N-ควีน
จำนวนคำตอบของปัญหา 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 ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส DSA Interview Prep ให้อัปเกรดเป็น CoddyKit PRO คอร์ส DSA Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “N-ควีนและการเผยแพร่ข้อจำกัด”
วางควีน N ตัวบนกระดานขนาด N×N โดยใช้เซตคอลัมน์และเส้นทแยงมุมเพื่อตรวจสอบข้อจำกัดในเวลา O(1) และอภิปรายความแตกต่างระหว่างการนับกับการแจกแจงคำตอบ คุณปฏิบัติ DSA Interview Prep ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน DSA Interview Prep หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน DSA Interview Prep บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน
บทเรียน “N-ควีนและการเผยแพร่ข้อจำกัด” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน DSA Interview Prep นี้ได้ไหม
ได้ บทเรียน DSA Interview Prep ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- แม่แบบการย้อนรอย: เลือก สำรวจ ยกเลิกการเลือก
- เซตย่อยและเพาเวอร์เซต
- การเรียงสับเปลี่ยนและการจัดหมู่
- N-ควีนและการเผยแพร่ข้อจำกัด