0Pricing
DSA Interview Prep · درس

مسألة الملكات N وانتشار القيود

ضع N ملكات على رقعة بحجم N×N باستخدام مجموعات الأعمدة والأقطار لفحص القيود في O(1)، وناقش الفرق بين عدّ الحلول وتعدادها.

مسألة الملكات N وانتشار القيود درس مجاني في DSA Interview Prep على CoddyKit. هذا هو الدرس 4 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في DSA Interview Prep، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة DSA Interview Prep 4 دروس في المجموع.

مسألة N-Queens

تطلب مسألة N-Queens (LeetCode 51/52) وضع N من الملكات على رقعة شطرنج بحجم N×N بحيث لا تهاجم أي ملكتين إحداهما الأخرى. تهاجم الملكات على امتداد الصفوف والأعمدة والقطرين. بالنسبة إلى N=4، يوجد حلّان بالضبط. أمّا بالنسبة إلى 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 (قيم row-col للأقطار من أعلى اليسار إلى أسفل اليمين)، وanti_diag (قيم row+col للأقطار من أعلى اليمين إلى أسفل اليسار). تشترك الملكات الواقعة على القطر نفسه في قيمة row-col نفسها، بينما تشترك الملكات الواقعة على القطر المعاكس نفسه في قيمة row+col نفسها.

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-Queens II

تطلب مسألة N-Queens 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-Queens باستخدام قناع البتات للسرعة

بالنسبة إلى قيم 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)}')

مفهوم نشر القيود

نشر القيود يتجاوز التقليم البسيط: بعد وضع ملكة، استنتج فورًا واستبعد جميع المواضع غير الصالحة في الصفوف التالية. هذا أكثر شدة من التحقق من الصلاحية عند كل مرشح، إذ إنك تضيّق فضاء البحث استباقيًا قبل التفرع. وأشهر مثال على ذلك هو Arc Consistency في محللات 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')

استدلال المتغير الأكثر تقييدًا

تحسين أساسي لمسائل إرضاء القيود: اختر دائمًا المتغير الأكثر تقييدًا تاليًا، أي الخلية التي تحتوي على أقل عدد من الخيارات الصالحة. في سودوكو، إذا لم يتبقَّ في إحدى الخلايا سوى رقم صالح واحد، فإن تعبئتها فورًا تكون إلزامية، ولا حاجة إلى البحث بالتراجع. ويؤدي اختيار هذه الخلايا أولًا إلى تقليل عمق شجرة البحث بدرجة كبيرة. وهذا هو استدلال Minimum Remaining Values (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-Queens

يتبع عدد حلول مسألة N-Queens المتتالية المعروفة التالية: 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-Queens

عندما يطلب منك المحاور إعادة اللوحات الفعلية (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()

اختبار سريع

اختبر فهمك لمفاهيم Data Structures & Algorithms — Coding Interview Prep الواردة في هذا الدرس.

مراجعة الدرس

تعلمت في هذا الدرس أن مسألة N-Queens تضع ملكة واحدة في كل صف، وتستخدم مجموعات للأعمدة والأقطار (الصف-العمود) والأقطار المعاكسة (الصف+العمود) للتحقق من القيود في O(1)، وأن أقنعة البتات تسرّع عمليات التحقق من القيود بدرجة أكبر، وتتيح استكشاف جميع المواضع بزمن يقارب O(1) لكل عملية، وأن نشر القيود، باستخدام استدلال MRV، يقلل البحث عبر اختيار المتغير الأكثر تقييدًا تاليًا. في الجزء التالي، سنقارن بين أسلوبي الجشع والبرمجة الديناميكية، ونتعلم متى نطبّق كلًّا منهما.

الأسئلة الشائعة

هل درس «مسألة الملكات N وانتشار القيود» مجاني؟

نعم — نص درس «مسألة الملكات N وانتشار القيود» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة DSA Interview Prep، انتقل إلى CoddyKit PRO. تتضمن دورة DSA Interview Prep 4 دروس في المجموع.

ماذا ستتعلم في «مسألة الملكات N وانتشار القيود»؟

ضع N ملكات على رقعة بحجم N×N باستخدام مجموعات الأعمدة والأقطار لفحص القيود في O(1)، وناقش الفرق بين عدّ الحلول وتعدادها. تتمرن على DSA Interview Prep مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.

هل أحتاج إلى خبرة سابقة لأبدأ DSA Interview Prep؟

لا تُشترط خبرة سابقة. DSA Interview Prep على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 4 من أصل 4.

كم من الوقت يستغرق درس «مسألة الملكات N وانتشار القيود»؟

معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.

هل يمكنني كتابة وتشغيل أكواد في درس DSA Interview Prep هذا؟

نعم. كل درس في DSA Interview Prep يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.

جميع الدروس في هذه الدورة

  1. قالب التراجع: اختر واستكشف وتراجع
  2. المجموعات الجزئية ومجموعة القوى
  3. التبديلات والتوافيق
  4. مسألة الملكات N وانتشار القيود
← العودة إلى DSA Interview Prep