N-Queens II
מלכה תוקפת כל משבצת בשורה שלה, בעמודה שלה ובשני האלכסונים שלה, ללא תלות במרחק. בהינתן מספר שלם n, החזירו את מספר הדרכים להציב n מלכות על לוח בגודל n × n כך שאף שתי מלכות לא יתקפו זו את זו.
לוחות שנבדלים במשבצת כלשהי נספרים בנפרד, כולל תמונות מראה.
פונקציה
- ninteger
- גודל הלוח ומספר המלכות
- מחזירהinteger
- מספר הדרכים להציב את המלכות כך שאף אחת מהן לא תתקוף אחרת
אילוצים
1 ≤ n ≤ 12- התשובה עבור
n = 12היא 14,200, ולכן היא נכנסת למספר שלם בן 32 סיביות.
דוגמאות
- קלט
- n = 4
- פלט
- 2
- הסבר
- כתיבת העמודה של המלכה בכל שורה, מלמעלה למטה, נותנת את הלוחות
1, 3, 0, 2ו-2, 0, 3, 1: תמונות מראה, הנספרות כשני פתרונות. כל בחירה אחרת מציבה שתי מלכות באותה עמודה או באותו אלכסון.
- קלט
- n = 3
- פלט
- 0
- הסבר
- מלכה בפינה העליונה משאירה משבצת בטוחה אחת בשורה האמצעית, ואז בשורה התחתונה אין אף אחת. מלכה במרכז העליון תוקפת את כל השורה האמצעית.
שאלת המשך
האם אפשר לספור רק לוחות שנשארים שונים לאחר סיבוב ושיקוף של הלוח? עבור n = 8, 92 הלוחות מתחלקים ל־12 קבוצות כאלה.
רמזים
פתחו אותם אחד אחד. כל רמז חושף קצת יותר.
שתי מלכות באותה שורה תוקפות זו את זו, ולכן בכל שורה יש בדיוק מלכה אחת. מה נשאר לבחור אחרי שיודעים זאת?
מלאו את הלוח שורה אחת בכל פעם, מלמעלה. ברגע שהמלכה החדשה מותקפת, נטשו את הלוח החלקי הזה, מכיוון ששום דבר שתוסיפו מתחת לא יוכל לתקן אותו. כדי לבדוק משבצת בלי להסתכל על הלוח כולו, זכרו באילו עמודות ובאילו אלכסונים כבר נמצאת מלכה. לאורך אחד מכיווני האלכסון
row + colזהה בכל המשבצות, ולאורך הכיוון האחרrow - colזהה.כתבו את
place(row), שמחזירה כמה לוחות מלאים אפשר להשלים מכאן. היא מחזירה 1 כאשרrow == n. אחרת היא מנסה כל עמודהcשהעמודה שלה, האלכסוןrow + cוהאלכסוןrow - cפנויים: סמנו את שלושתם, הוסיפו אתplace(row + 1)לסכום מצטבר, ואז בטלו את הסימונים שלהם. התשובה היאplace(0).
פתרון
שתי מלכות באותה שורה תמיד תוקפות, לכן לוח הוא עמודה אחת לכל שורה: n^n רשימות, בערך 8.9 × 10^12 עבור n = 12. במקום זאת, בנה אותו שורה אחר שורה, הסר לוח חלקי ברגע שמלכה מותקפת, וסמן את העמודות והאלכסונים התפוסים כך שבדיקת משבצת תעלה שלוש בדיקות בלבד.
נסו כל מיקום, עם מלכה אחת בכל שורה
נכונה, אבל לא מסתיימת בבדיקות הגדולות ביותר
האינטואיציה
כתבו הצבה כרשימה cols, שבה מצוין הטור של המלכה בכל שורה, ועברו על כל n^n הרשימות כמו במד־מרחק. מלכות בשורות i < j תוקפות זו את זו כאשר cols[i] == cols[j] או |cols[i] - cols[j]| == j - i, כי אלכסון נע טור אחד בכל שורה. כל רשימה נבדקת פעם אחת, ולכן אף לוח לא מוחמץ או נספר פעמיים.
שום דבר לא עוצר את החיפוש מוקדם: גם אם יש התנגשות בשתי השורות הראשונות, נותרות n^(n-2) רשימות. עבור n = 8 יש 16,777,216 רשימות עבור 92 לוחות; עבור n = 12, 8.9 × 10^12, כלומר כ־2.5 שעות אם כל אחת אורכת ננו־שנייה.
אלגוריתם
- מתחילים כשכל הערכים ב־
colsהם אפס. - מוסיפים 1 למונה אם אין זוג שורות
i < jשעבורו מתקייםcols[i] == cols[j]או|cols[i] - cols[j]| == j - i. - מתקדמים כמו במד מרחק: מהשורה האחרונה כלפי מעלה, מאפסים כל ערך שהוא
n-1ל־0, ואז מוסיפים 1 לערך הראשון שקטן מ־n-1. - עוצרים ומחזירים את המונה כאשר כל הערכים היו
n-1.
def totalNQueens(n):
def is_valid(cols):
# cols[r] is the column of the queen in row r, so rows never clash.
for i in range(n):
for j in range(i + 1, n):
if cols[i] == cols[j] or abs(cols[i] - cols[j]) == j - i:
return False
return True
cols = [0] * n
count = 0
while True:
if is_valid(cols):
count += 1
# Move to the next placement, like an odometer with n digits in base n.
row = n - 1
while row >= 0 and cols[row] == n - 1:
cols[row] = 0
row -= 1
if row < 0:
return count
cols[row] += 1חזרה לאחור באמצעות קבוצות של עמודות ואלכסונים
האינטואיציה
הצבו מלכות שורה אחר שורה ובדקו כל אחת בעת הצבתה. מילוי השורות שמתחת לא יכול להציל מלכה שמאוימת, לכן דלגו על המשבצת הזאת. מלכה בטוחה קוראת רקורסיבית לשורה הבאה ומוסרת כשקריאה זו חוזרת; הגעה לשורה n נחשבת כלוח אחד.
רק שלושה קווים יכולים לאיים על המשבצת (row, c): העמודה שלה, האלכסון /, שבו row + c קבוע, והאלכסון \, שבו row - c קבוע (הוסיפו n-1 כדי לאנדקס אותו החל מ-0). מערכי הסימון cols, diag ו-anti הופכים את בדיקת הבטיחות לשלוש שליפות. בכל קו יכולה להיות לכל היותר מלכה אחת, ולכן איפוס הסימונים בחזרה משחזר את המערכים בדיוק.
בשורה k יש לכל היותר n - k עמודות, ולכן החיפוש חסום על ידי O(n!), עם מקום בזיכרון של O(n).
אלגוריתם
- צרו מערכי דגלים
colsבגודלn, ואתdiagו-antiבגודל2n-1, כשכולם כבויים. place(row): אםrow == n, החזירו 1.- עבור כל עמודה
c, דלגו עליה אםcols[c],diag[row + c]אוanti[row - c + n - 1]דולקים. - אחרת, הדליקו את שלושת הדגלים, הוסיפו את
place(row + 1)לסכום וכבו אותם. - החזירו את הסכום. התשובה היא
place(0).
def totalNQueens(n):
cols = [False] * n # cols[c]: column c holds a queen
diag = [False] * (2 * n - 1) # diag[r + c]: that / diagonal holds a queen
anti = [False] * (2 * n - 1) # anti[r - c + n - 1]: that \ diagonal holds a queen
def place(row):
if row == n:
return 1 # a queen in every row: one complete board
count = 0
for c in range(n):
if cols[c] or diag[row + c] or anti[row - c + n - 1]:
continue # attacked: three lookups, no scan of the board
cols[c] = diag[row + c] = anti[row - c + n - 1] = True
count += place(row + 1)
cols[c] = diag[row + c] = anti[row - c + n - 1] = False # take it back
return count
return place(0)חזרה לאחור באמצעות מסכות סיביות
האינטואיציה
חיפוש הדגל עדיין בודק את כל n העמודות בכל שורה; מסכות סיביות קופצות ישירות לעמודות הפנויות. הסיבית c היא העמודה c בשורה שמתמלאת: cols מכיל את העמודות התפוסות, ו-left ו-right את המשבצות שמותקפות לאורך כל אחד מהאלכסונים. ואז free = ~(cols | left | right) & full, כאשר full מכיל את n הסיביות הנמוכות, ו-free & -free היא המשבצת הפנויה הנמוכה ביותר. בשורה הבאה, עמודה נשארת תפוסה וכל איום באלכסון זז עמודה אחת.
עם n = 4 ומלכה ראשונה ב-bit = 0010, בשורה 1 מתקבל free = 1000: עמודה 3, שנמצאה בלי לבדוק את האחרות. עבור n = 12, 856,188 צעדי לולאה מחליפים 10,103,868 בדיקות של משבצות. קוד R בונה שורה שלמה של לוחות חלקיים בבת אחת במקום לבצע קריאות רקורסיביות.
אלגוריתם
- הגדר
full = (1 << n) - 1. count(cols, left, right): אםcols == full, החזר 1.- חשב
free = ~(cols | left | right) & full. - כל עוד
freeאינו 0, קח אתbit = free & -free, הסר אותו מ־free, והוסף לסכום אתcount(cols | bit, ((left | bit) << 1) & full, (right | bit) >> 1). - החזר את הסכום. התשובה היא
count(0, 0, 0).
def totalNQueens(n):
full = (1 << n) - 1 # bit c stands for column c of the current row
def count(cols, left, right):
# cols: columns taken. left, right: squares of this row hit along a diagonal.
if cols == full:
return 1 # every column used: n queens placed
total = 0
free = full & ~(cols | left | right)
while free:
bit = free & -free # the lowest free square
free -= bit
# Moving down a row shifts each diagonal attack one column over.
total += count(cols | bit, ((left | bit) << 1) & full, (right | bit) >> 1)
return total
return count(0, 0, 0)
מלכודות ומקרי קצה
- יצירת אינדקס עבור
antiבעזרתrow - cבלי להוסיףn-1. ב-Java נזרקת שגיאה, ב-C מתבצעת קריאה מחוץ למערך, וב-Python הקריאהanti[-2]קוראת בשקט את הדגל של אלכסון אחר. - הקצאת גודל
nבמקום2n-1למערכי האלכסונים.row + cמגיע ל-2n-2. - אי-איפוס הדגלים לאחר הקריאה הרקורסיבית. ענפים מאוחרים יותר רואים מלכות שכבר אינן שם, והספירה יורדת.
- השארת
& fullמחוץ ל-free.~xמדליק את הסיביות שמעל לעמודהn-1, ולכן הלולאה בוחרת משבצות מחוץ ללוח; ב-Python או ב-Ruby הערך שלfreeנעשה שלילי והלולאה לא מסתיימת.
שאלות נפוצות3
מהי סיבוכיות הזמן של N-Queens II?
החיפוש עם חזרה לאחור חסום על ידי O(n!): בשורה הראשונה יש n אפשרויות, בבאה אחריה לכל היותר n-1, וכן הלאה. גיזום מצמצם את העבודה בפועל בהרבה, ל-856,189 לוחות חלקיים עבור n = 12. סיבוכיות המקום היא O(n).
מה ההבדל בין N-Queens ל-N-Queens II?
בעיית N-Queens מחזירה את כל הלוחות, כשהם מוצגים כשורות טקסט; בעיית N-Queens II מחזירה רק את מספר הלוחות. החיפוש זהה, אך ספירה אינה שומרת לוחות, ולכן היא מתאימה לגרסת מסכת הביטים.
האם אפשר להשתמש בסימטריה כדי להאיץ את N-Queens II?
שיקוף של לוח משמאל לימין יוצר לוח תקין נוסף, לכן סופרים את הלוחות שבהם המלכה הראשונה נמצאת בעמודות 0 עד n/2 - 1 ומכפילים את המספר הזה. עבור n אי־זוגי, מוסיפים פעם אחת את הלוחות שבהם המלכה הראשונה נמצאת בעמודה האמצעית.
בעיות דומות
בעיות שמשתמשות באותם רעיונות. פתרון של שתיים או שלוש מהן מקבע את הדפוס.
def totalNQueens(n):
# כתוב את הקוד כאןבדיקה #1
בדיקה #2
ארגומנט
n = 4
הפלט הצפוי
2