مسألة الملكات الـN
درس 6 من 11
6♛ مسألة الملكات الـN
أول تراجع حقيقي لك: كيف تبني حلًا خطوة خطوة، وتتراجع لحظة ما يصير مستحيلًا، بدل أن تجرّب كل الاحتمالات وتنتظر إلى الأبد.
ماذا تشاهد في الرسم: نضع ملكة في كل صف، وعندما لا يبقى عمود آمن نتراجع صفًّا ونجرّب العمود التالي، حتى نصل إلى [1, 3, 0, 2].
خارطة هذا الجزء
المفاهيم بالترتيب، اضغط أيًّا منها للقفز إليه:
طريقان إلى نفس الفكرة
تريد أن تضع عدة ملكات على رقعة شطرنج بحيث لا تهدد أي واحدة الأخرى. تضع واحدة في كل صف، وكلما وجدت أن الملكة الجديدة مهدّدة ترفعها وتجرّب مكانًا آخر، وإن لم تجد مكانًا ترجع خطوة إلى الصف السابق. هكذا تصل للحل دون أن تجرّب كل الاحتمالات.
التراجع backtracking بحث بالعمق يبني الحل تدريجيًا ويقطع الفرع حين يخالف القيود. لكل صف تختار عمودًا، وتفحص الأمان بثلاث مجموعات: الأعمدة والقطرين r - c وr + c. هذا يقلّص البحث بشدة عن n^n.
المتطلبات قبل البداية
الاستدعاء الذاتي
دالة تنادي نفسها على مسألة أصغر، ولها حالة توقّف. هنا المسألة الأصغر هي «رتّب بقية الصفوف»، وحالة التوقّف هي الوصول للصف n.
المجموعة set
الفحص in في المجموعة فوري، وفي القائمة يمشي على العناصر. هذا الفرق وحده هو ما يجعل الحل سريعًا.
نسخ القوائم
lst[:] ينسخ القائمة، وlst يشير لنفس القائمة. الخلط بينهما هنا يجعل كل الحلول المحفوظة متطابقة.
sys.argv
قائمة وسائط سطر الأوامر، وأولها اسم البرنامج. تحقّق منها بدقّة، فرسائل الخطأ محدّدة حرفًا بحرف.
المفاهيم الأساسية
المطلوب: ضع N ملكة على رقعة N×N بحيث لا تهاجم أي ملكة أخرى، واطبع كل الحلول الممكنة. والملكة في الشطرنج تهاجم في صفّها وعمودها وقطريها بلا حدود.
1لماذا لا تنفع القوة الغاشمة
لو جرّبت كل توزيعات الملكات الثماني على 64 مربعًا لواجهت أكثر من أربعة مليارات حالة. وحتى لو حسّنتها قليلًا بوضع ملكة في كل صف، يبقى عندك Nᴺ احتمالًا لـ N=8، أي 16.7 مليون. المشكلة ليست في الحساب، بل في أنك تكتشف الفشل في آخر خطوة بعد أن بنيت الحل كاملًا.
2التمثيل الذكي: ملكة واحدة لكل صف
أول قرار تصميمي، وهو يحذف نصف المسألة مجانًا: بما أن ملكتين في نفس الصف تتهاجمان حتمًا، فكل حل فيه ملكة واحدة بالضبط في كل صف. فلا نسأل «أين أضع الملكات؟» بل «ما عمود ملكة كل صف؟». الحل صار قائمة أعمدة بطول N.
[[0, 1], [1, 3], [2, 0], [3, 2]]، لاحظ أن الأرقام الأولى 0 ثم 1 ثم 2 ثم 3 بالترتيب. شكل المخرجات نفسه يخبرك أن التمثيل صف-ثم-عمود، وأن الصفوف مرتّبة. كثير من قرارات التصميم مكتوبة في المثال، لا في النص.3متى تتهاجم ملكتان؟ الحيلة الحسابية
بقي الصف محلولًا بالتمثيل، فبقي ثلاثة فحوص. والعمود سهل، أما القطران فلهما خدعة جميلة:
| الاتجاه | الشرط | لماذا |
|---|---|---|
| نفس العمود | c1 == c2 | مباشر |
| القطر ↘ | r1 - c1 == r2 - c2 | كلما نزلت صفًا تحرّكت عمودًا لليمين، فالفرق r - c ثابت على القطر كله |
| القطر ↙ | r1 + c1 == r2 + c2 | كلما نزلت صفًا تحرّكت عمودًا لليسار، فالمجموع r + c ثابت |
وبهذا يصير الفحص ثلاث عمليات طرح وجمع بدل المرور على كل ملكة موضوعة. نحتفظ بثلاث مجموعات: الأعمدة المستعملة، وقيم r - c المستعملة، وقيم r + c المستعملة. والفحص كله:
if col in cols or (row - col) in diag or (row + col) in anti:
continue4التراجع: ابنِ، وتراجع لحظة الاستحالة
فكرة التراجع (backtracking) في جملة: ابنِ الحل جزءًا جزءًا، وفي اللحظة التي يصير فيها الجزء الحالي مستحيلًا، ارجع وجرّب غيره. الفرق عن القوة الغاشمة أنك تقطع فرعًا كاملًا من الاحتمالات دفعة واحدة، لا احتمالًا واحدًا.
وكل خطوة تراجع تتطلّب التنظيف: ترفع الملكة من اللوح، وتمسح تسجيلها من المجموعات الثلاث. نسيان التنظيف هو الخطأ الأول في التراجع، لأن اللوح يبقى ملوّثًا بمحاولة فاشلة.
cols.add(col) ونسيت cols.discard(col) بعد الاستدعاء الذاتي، فستبقى أعمدة محاولات فاشلة محسوبة كأنها مشغولة، وستفقد حلولًا صحيحة. القاعدة: كل add قبل الاستدعاء يقابله discard بعده، بالترتيب العكسي.5التحقّق من المدخلات: اقرأ النص حرفيًا
هذا الجزء يُفقد فيه الكثير من الدرجات بلا سبب، لأن الرسائل محدّدة بالضبط:
| الحالة | الرسالة | الخروج |
|---|---|---|
| عدد الوسائط ليس واحدًا | Usage: nqueens N | exit(1) |
| الوسيط ليس عددًا | N must be a number | exit(1) |
N أصغر من 4 | N must be at least 4 | exit(1) |
والترتيب مهم: تفحص العدد أولًا، ثم النوع، ثم القيمة. ولا تستورد شيئًا غير sys، فالمشروع يمنع غيره صراحة.
pycodestyle 2.5 على بايثون 3.8، أما هذا فبـPEP 8 (pep8 1.7) على بايثون 3.4. وعليه: السطر الأول #!/usr/bin/python3 لا #!/usr/bin/env python3، ولا تستعمل f-strings ولا تعليقات الأنواع الحديثة، فهي لم تكن موجودة في 3.4.N=2 وN=3 لا حل لهما إطلاقًا: الرقعة أصغر من أن تتجنّب الملكات فيها بعضها. وN=1 حله الوحيد التافه (ملكة واحدة). فالمسألة تبدأ فعليًا من 4.كيف يعمل؟
ثلاث صور: دورة التراجع، ثم شجرة البحث الحقيقية لـ N=4، ثم متى تتهاجم ملكتان.
شكل 15، دورة التراجع كاملة: ضع، انزل، وإن فشلت ارفع وجرّب التالي، وإن نفدت الأعمدة فتراجع صفًا للأعلى.
شكل 16، شجرة البحث لـ N=4 من العمود 0 والعمود 1. الفرع الأيسر كله يموت عند الصف 2، والتراجع يقطعه دفعة واحدة بدل تجربة ما تحته.
شكل 17، شجرة قرار التهديد: الصف محلول بالتمثيل، فيبقى العمود والقطران، وكل قطر يُختصر في عدد واحد.
الحل خطوة بخطوة
نكتب الحل، نشغّله على المثالين المطلوبين، نفحص رسائل الخطأ، ثم نقيس أثر التقليم بالأرقام. كل مخرج تحت حقيقي.
الخطوة 1، الحل
#!/usr/bin/python3
"""Solve the N queens puzzle."""
import sys
def solve(n, row, cols, diag, anti, board, out):
"""Place one queen per row, backtracking when a row has no safe column."""
if row == n:
out.append([pair[:] for pair in 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)
board.append([row, col])
solve(n, row + 1, cols, diag, anti, board, out)
board.pop()
anti.discard(row + col)
diag.discard(row - col)
cols.discard(col)
def main():
"""Validate the argument, then print every solution."""
if len(sys.argv) != 2:
print("Usage: nqueens N")
sys.exit(1)
try:
n = int(sys.argv[1])
except ValueError:
print("N must be a number")
sys.exit(1)
if n < 4:
print("N must be at least 4")
sys.exit(1)
out = []
solve(n, 0, set(), set(), set(), [], out)
for sol in out:
print(sol)
if __name__ == "__main__":
main()لاحظ [pair[:] for pair in board] في حفظ الحل: بدونها تحفظ إشارة إلى اللوح نفسه الذي سيتغيّر بعد سطر واحد، فتخرج لك حلول متطابقة أو فارغة.
الخطوة 2، المثالان المطلوبان
$ ./0-nqueens.py 4 [[0, 1], [1, 3], [2, 0], [3, 2]] [[0, 2], [1, 0], [2, 3], [3, 1]] $ ./0-nqueens.py 6 [[0, 1], [1, 3], [2, 5], [3, 0], [4, 2], [5, 4]] [[0, 2], [1, 5], [2, 1], [3, 4], [4, 0], [5, 3]] [[0, 3], [1, 0], [2, 4], [3, 1], [4, 5], [5, 2]] [[0, 4], [1, 2], [2, 0], [3, 5], [4, 3], [5, 1]]
الخطوة 3، رسائل الخطأ ورمز الخروج
$ ./0-nqueens.py ; echo "exit=$?" Usage: nqueens N exit=1 $ ./0-nqueens.py hello ; echo "exit=$?" N must be a number exit=1 $ ./0-nqueens.py 3 ; echo "exit=$?" N must be at least 4 exit=1
الخطوة 4، كيف يبدو الحل على الرقعة
[[0, 1], [1, 3], [2, 0], [3, 2]] -> . Q . .
. . . Q
Q . . .
. . Q .اقرأ القائمة هكذا: الصف 0 ملكته في العمود 1، والصف 1 في العمود 3، وهكذا. تحقّق بعينك أن لا اثنتين تشتركان في عمود أو قطر.
الخطوة 5، لماذا تعمل حيلة القطر
col0 col1 col2 col3 <- each cell shows (row-col)/(row+col)
row0 +0/0 -1/1 -2/2 -3/3
row1 +1/1 +0/2 -1/3 -2/4
row2 +2/2 +1/3 +0/4 -1/5
row3 +3/3 +2/4 +1/5 +0/6
(0,1) and (1,3): row-col = -1 and -2 -> different
row+col = 1 and 4 -> different -> safe
(0,1) and (1,2): row-col = -1 and -1 -> SAME -> same diagonal, attackingالخطوة 6، امشِ على N=4 خطوة خطوة
البحث الحقيقي الذي ينتجه الكود
الصف 0. نجرّب العمود 0 ونضع الملكة: cols={0} وdiag={0} وanti={0}.
الصف 1. العمود 0 مستعمل. العمود 1: 1-1=0 وهي في diag ← مهاجِم. العمود 2 آمن، فنضعه وننزل.
الصف 2. العمود 0 مستعمل، 1 على قطر، 2 مستعمل، 3 على قطر. لا عمود آمن ← نتراجع.
تنظيف. نرفع ملكة الصف 1 ونمسح تسجيلها، ونجرّب العمود 3. ننزل للصف 2 فنجد الحالة نفسها: لا عمود آمن ← نتراجع مرة أخرى.
نفدت أعمدة الصف 1، فنتراجع إلى الصف 0 ونرفع ملكته. لاحظ: قطعنا الفرع كله تحت العمود 0 بعد تجربتين فقط.
الصف 0 من جديد، العمود 1. ننزل: الصف 1 ← العمود 3 آمن. الصف 2 ← العمود 0 آمن. الصف 3 ← العمود 2 آمن.
وصلنا row == n فنحفظ نسخة من اللوح: [[0,1],[1,3],[2,0],[3,2]] ✓
لا نتوقّف. المطلوب كل الحلول، فنواصل التراجع من حيث وقفنا. والبحث يجد حلًا ثانيًا [[0,2],[1,0],[2,3],[3,1]] ثم تنفد الاحتمالات.
الخطوة 7، كم وفّر التقليم فعلًا؟
N solutions placements tried N^N N! 4 2 60 256 24 5 10 220 3,125 120 6 4 894 46,656 720 7 40 3,584 823,543 5,040 8 92 15,720 16,777,216 40,320 9 352 72,378 387,420,489 362,880 10 724 348,150 10,000,000,000 3,628,800
العمود الأهم هو الثالث مقابل الرابع: عند N=8 يجرّب الحل 15,720 وضعًا بدل 16.7 مليون ترتيب، أي أقل من واحد بالألف. وكلما كبرت N اتّسعت الفجوة.
N=8 : 92 solutions, 15,720 placements tried, 0.002s N=10: 724 solutions, 348,150 placements tried, 0.045s N=12: 14,200 solutions, 10,103,868 placements tried, 1.234s
اعتقاد شائع × الحقيقة
| الاعتقاد | الحقيقة |
|---|---|
| «التراجع نوع من القوة الغاشمة.» | كلاهما يستكشف الاحتمالات، لكن التراجع يقطع فرعًا كاملًا لحظة ما يصير مستحيلًا. الفرق قسناه: 15,720 موضعًا مقابل 16.7 مليون عند N=8. |
| «يجب أن أفحص كل ملكة موضوعة في كل خطوة.» | لا. ثلاث مجموعات تجعل الفحص فوريًا: عمود، وrow-col، وrow+col. |
«أستطيع الاكتفاء بـ return في نهاية الدالة.» | المطلوب كل الحلول، فالدالة تكمل البحث بعد كل حل تجده، ولا تتوقّف عند الأول. |
| «ترتيب فحوص المدخلات لا يهم.» | يهم. لو فحصت sys.argv[1] قبل عدد الوسائط لانهار البرنامج بـ IndexError حين يُستدعى بلا وسائط، بدل أن يطبع الرسالة المطلوبة. |
| «أضيف الملكة للمجموعات ثم أكمل، والتنظيف تفصيل.» | التنظيف هو التراجع نفسه. بدونه تبقى محاولات فاشلة مسجّلة فتختفي حلول صحيحة. |
«N=3 لها حل مثل البقية.» | لا حل لها إطلاقًا، ولا لـ N=2. ولهذا يبدأ المشروع من 4. |
أين نستخدمه فعليًا؟
سودوكو وألغاز الشبكات
نفس الهيكل حرفيًا: اختر خانة، جرّب قيمة، تحقّق من القيود، انزل، وتراجع عند الاستحالة.
الجدولة وتوزيع الموارد
جداول المحاضرات وتوزيع المناوبات: لكل عنصر خيارات محدودة وقيود تمنع التعارض، وهي صياغة أخرى للمسألة نفسها.
تلوين الخرائط والترددات
لوّن كل منطقة بحيث لا تتشارك مع جارتها لونًا، أو وزّع ترددات على أبراج متجاورة.
البحث في فضاءات كبيرة
كل مسألة «أوجد تركيبة تحقّق قيودًا» تُحلّ بهذا النمط، وهو أساس ما يسمى CSP، مسائل إرضاء القيود.
جرّب بنفسك
في دالة التراجع، نسيت السطر cols.discard(col) بعد الاستدعاء الذاتي. ماذا يصير؟
المجموعة cols كائن واحد مشترك بين كل مستويات الاستدعاء، ولا شيء ينظّفها عنك. فإذا بقي العمود مسجّلًا بعد فشل الفرع، ستظن المستويات اللاحقة أنه مشغول وتتخطّاه، فتفقد حلولًا صحيحة. ولو جرّبت N=4 مع هذا الخطأ لطبع حلًا واحدًا أو صفرًا بدل اثنين. القاعدة: كل add قبل الاستدعاء يقابله discard بعده.
بطاقات مراجعة سريعة
row - col ثابت على القطر كله، فنحفظ القيم المستعملة في مجموعة.row + col ثابت، وهو المجموعة الثالثة.[pair[:] for pair in board] عند الحفظ؟board يتغيّر بعد سطر واحد؛ بدون النسخ تحفظ إشارة إليه لا صورة منه.N=8؟تحقّق من فهمك
1. لماذا نفحص عدد الوسائط قبل تحويل الوسيط إلى عدد؟
لأن sys.argv[1] يرفع IndexError إذا استُدعي البرنامج بلا وسائط، فينهار بـ traceback بدل أن يطبع Usage: nqueens N ويخرج بـ 1. الترتيب الصحيح: العدد، ثم النوع، ثم القيمة.
2. اشرح بدقّة لماذا row - col يميّز القطر ↘.
تحرّك على هذا القطر يعني زيادة الصف واحدًا وزيادة العمود واحدًا معًا، فالفرق بينهما لا يتغيّر. وكل قطر ↘ له قيمة فرق فريدة، فصار الفحص «هل هذا الفرق مستعمل؟» بدل المرور على الملكات. ونفس المنطق مع row + col للقطر الآخر: النزول صفًا يقابله تراجع عمودًا، فيبقى المجموع ثابتًا.
3. (اكتشف الخطأ) حلك يطبع نفس الحل مكرّرًا بعدد الحلول. أين الخطأ؟
حفظت board مباشرة بدل نسخة منه. القائمة كائن واحد يتغيّر مع كل خطوة، فكل ما حفظته إشارات إلى نفس الكائن، وفي النهاية تطبعه كلها بحالته الأخيرة. الحل [pair[:] for pair in board]، ولاحظ أن board[:] وحدها لا تكفي لأنها نسخة سطحية تشارك القوائم الداخلية.
4. هل يتغيّر عدد الحلول لو بدأت من الأعمدة بدل الصفوف؟
لا. الرقعة متناظرة في هذا المعنى، وعدد الحلول خاصية للمسألة لا لطريقة بحثك. ما يتغيّر هو ترتيب ظهور الحلول، والنص يقول صراحة إن الترتيب غير مطلوب.
5. (قرار) تريد حلًا واحدًا فقط لـ N=30 بأسرع وقت. ماذا تغيّر؟
توقّف عند أول حل بدل جمع الكل، وهذا وحده يقلّص الزمن بشكل هائل. وإن احتجت أسرع، رتّب الأعمدة المجرّبة بحيث تبدأ بالأقل تقييدًا (least constraining value)، أو استخدم خوارزمية بناء مباشرة معروفة للملكات. «كل الحلول» و«حل واحد» مسألتان مختلفتان في التكلفة، فاقرأ المطلوب قبل أن تحسّن.
مصادر هذا الدرس
مرتّبة بالنوع. ابدأ بالتوثيق الرسمي إذا تبي المرجع الدقيق، وبالمقال أو الفيديو إذا تبي الشرح أولًا.
أدوات2
خلّصت الدرس؟
سجّل الدخول ليُحفظ تقدّمك.