0Pricing
Coding Interview Prep · บทเรียน

N-ควีนและการเผยแพร่ข้อจำกัด

วางควีน N ตัวบนกระดานขนาด N×N โดยใช้เซตคอลัมน์และเส้นทแยงมุมเพื่อตรวจสอบข้อจำกัดในเวลา O(1) และอภิปรายความแตกต่างระหว่างการนับกับการแจกแจงคำตอบ

N-ควีนและการเผยแพร่ข้อจำกัด เป็นบทเรียน Coding Interview Prep ฟรีบน CoddyKit นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน Coding Interview Prep และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส Coding 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) และปลดล็อคส่วนที่เหลือของคอร์ส Coding Interview Prep ให้อัปเกรดเป็น CoddyKit PRO คอร์ส Coding Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน

คุณจะเรียนรู้อะไรในบทเรียน “N-ควีนและการเผยแพร่ข้อจำกัด”

วางควีน N ตัวบนกระดานขนาด N×N โดยใช้เซตคอลัมน์และเส้นทแยงมุมเพื่อตรวจสอบข้อจำกัดในเวลา O(1) และอภิปรายความแตกต่างระหว่างการนับกับการแจกแจงคำตอบ คุณปฏิบัติ Coding Interview Prep ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน

คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน Coding Interview Prep หรือไม่

ไม่จำเป็นต้องมีประสบการณ์มาก่อน Coding Interview Prep บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน

บทเรียน “N-ควีนและการเผยแพร่ข้อจำกัด” ใช้เวลานานแค่ไหน

บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย

ฉันเขียนและรันโค้ดในบทเรียน Coding Interview Prep นี้ได้ไหม

ได้ บทเรียน Coding Interview Prep ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ

บทเรียนทั้งหมดในหลักสูตรนี้

  1. แม่แบบการย้อนรอย: เลือก สำรวจ ยกเลิกการเลือก
  2. เซตย่อยและเพาเวอร์เซต
  3. การเรียงสับเปลี่ยนและการจัดหมู่
  4. N-ควีนและการเผยแพร่ข้อจำกัด
← กลับไปที่ Coding Interview Prep