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