Rat in a Maze
עכברוש מתחיל בתא השמאלי העליון של n × n maze, כאשר 1 הוא תא פתוח ו-0 הוא קיר, ועליו להגיע לתא הימני התחתון. כל מהלך עובר תא אחד למטה (D), שמאלה (L), ימינה (R) או למעלה (U) אל תא פתוח. מסלול אינו עובר באותו תא פעמיים ומסתיים ביציאה.
יש להחזיר כל מסלול כמחרוזת של מהלכים, בסדר אלפביתי, או מערך ריק אם אין מסלול.
פונקציה
- mazeinteger-2d-array
- רשת n × n, שבה 1 מציין תא פתוח ו־0 מציין קיר
- מחזירהstring-array
- כל מסלול מהפינה השמאלית העליונה לפינה הימנית התחתונה כמחרוזת של מהלכים, לפי סדר האלפבית
אילוצים
2 ≤ n ≤ 5maze[i][j]הוא0או1.- בכל מבוך בדיקה יש לכל היותר 2,000 מסלולים. במבוך פתוח בגודל 5 × 5 יש 8,512 מסלולים, ולכן הוא לא נכלל בבדיקות.
דוגמאות
- קלט
- maze = [[1, 1, 0, 0], [0, 1, 1, 0], [1, 1, 1, 0], [1, 0, 1, 1]]
- פלט
- ["RDDRDR", "RDRDDR"]
- הסבר
- בהתחלה רק התא שמימין פתוח, ומשם רק התא שמתחת, לכן שני המסלולים מתחילים ב־
RD.RDDRDRמופיע ראשון בסדר המיון כי המהלך השלישי שלו,D, מופיע לפניR.
- קלט
- maze = [[1, 1, 1], [1, 0, 1], [1, 1, 0]]
- פלט
- []
- הסבר
- היציאה, התא הימני התחתון, היא קיר, לכן שום מסלול לא יכול להסתיים שם והתשובה ריקה, אף שכל תא אחר פתוח.
שאלת המשך
כאשר כל ארבעת המהלכים מותרים, האם תוכל לספור את המסלולים בלי לפרט אותם? כמה גדול n יכול להיות לפני שהספירה נעשית בלתי אפשרית?
רמזים
פתחו אותם אחד אחד. כל רמז חושף קצת יותר.
מסלול הוא רצף של מהלכים מתא התא ההתחלתי, לכן בונים אותו מהלך אחד בכל פעם. מכל תא אפשר לבצע לכל היותר ארבעה מהלכים: אילו מהם מותרים?
התקדם קדימה באמצעות פונקציה רקורסיבית, וסמן את התא הנוכחי כמשומש לפני שאתה עוזב אותו, כדי שהמסלול לא יוכל לחזור אליו. כשהקריאה חוזרת, בטל את הסימון שלו: מסלול אחר עדיין עשוי לעבור דרך התא הזה.
נסה את המהלכים בסדר
D,L,R,U, שהוא סדר אלפביתי. שני מסלולים נבדלים לראשונה במהלך שבו הם מתפצלים, והענף עם האות המוקדמת יותר נחקר ראשון, כך שהמסלולים מתקבלים כבר ממוינים.
פתרון
עליך לרשום כל מסלול, בלי חזרות ובלי פערים. נסיגה לאחור עושה זאת: מאריכים את המסלול בצעד אחד בכל פעם, חוסמים את התאים שבהם הוא משתמש ומבטלים את החסימה של כל תא כשיוצאים ממנו בחזרה. ניסיון של המהלכים לפי סדר האלפבית גורם למסלולים להופיע ממוינים.
הרחב כל מסלול לרוחב תחילה, ואז מיין
האינטואיציה
יש לשמור תור של מסלולים חלקיים, שכל אחד מהם כולל את התא הנוכחי, את התאים שבהם כבר עבר ואת המהלכים שלו. מוציאים מסלול אחד ומכניסים בחזרה את כל ההרחבות שלו במהלך אחד אל תא פתוח שטרם עברו בו, או שומרים אותו אם הוא מגיע ליציאה. כל מסלול נמצא בדיוק פעם אחת. כשיש לכל היותר 25 תאים, קבוצת התאים שבהם כבר עברו נכנסת למספר שלם אחד: הביט r * n + c מייצג את התא (r, c).
התור מוסר מסלולים מהקצרים לארוכים, וזה לא סדר ממויין. במבוך פתוח בגודל 5 × 5, המסלול בן 16 המהלכים DDDDRRRULLURRRDD מופיע לפני המסלול בן 8 המהלכים DDDRDRRR במיון. לכן ממיינים בסוף, והתור מחזיק בזיכרון שכבה שלמה של מסלולים חלקיים.
אלגוריתם
- אם נקודת ההתחלה או היציאה היא קיר, החזר מערך ריק.
- הכנס את נקודת ההתחלה לתור: התא
(0, 0), קבוצת התאים שבהם ביקרנו מסומנת בביט 0, ללא מהלכים. - הוצא את הרשומה הבאה. אם היא נמצאת ביציאה, שמור את המהלכים שלה והמשך.
- אחרת, עבור כל מהלך שמגיע לתא פתוח שעדיין לא ביקרנו בו בתוך המבוך, הוסף רשומה עם התא הזה, הביט של התא נוסף לקבוצת התאים שבהם ביקרנו, והמהלך מצורף.
- כשהתור מתרוקן, מיין את המסלולים שנשמרו והחזר אותם.
from collections import deque
def findPaths(maze):
n = len(maze)
if maze[0][0] == 0 or maze[n - 1][n - 1] == 0:
return []
moves = (("D", 1, 0), ("L", 0, -1), ("R", 0, 1), ("U", -1, 0))
result = []
# Each entry is a path in progress: its cell, the cells it has used
# (bit r * n + c), and its moves so far.
queue = deque([(0, 0, 1, "")])
while queue:
r, c, used, path = queue.popleft()
if r == n - 1 and c == n - 1:
result.append(path)
continue
for letter, dr, dc in moves:
nr, nc = r + dr, c + dc
if 0 <= nr < n and 0 <= nc < n and maze[nr][nc] == 1:
bit = 1 << (nr * n + nc)
if not used & bit:
queue.append((nr, nc, used | bit, path + letter))
result.sort() # shorter paths were found first, so sort by text
return resultחיפוש עם חזרה לאחור בסדר אלפביתי
האינטואיציה
פונקציה רקורסיבית נמצאת בתא. ביציאה היא שומרת את המהלכים. אחרת היא מגדירה את התא כ־0 כדי שהמסלול לא יוכל לחזור אליו, מנסה כל מהלך אל תא פתוח ומחזירה את התא למצב 1, כי מסלול מאוחר יותר עשוי לעבור דרכו. הזיכרון הוא המסלול הנוכחי ועומק הרקורסיה הוא לכל היותר n².
שני מסלולים חולקים את המהלכים שלהם עד למהלך הראשון שבו הם שונים, ושם החיפוש מסיים קודם את הענף של האות הקטנה יותר. מסלול גם נעצר ביציאה, ולכן שום מסלול אינו תחילתו של מסלול אחר. לכן, כאשר מנסים את המהלכים בסדר D, L, R, U, המסלולים נשמרים בסדר ממוין.
אלגוריתם
- אם נקודת ההתחלה או נקודת היציאה היא קיר, יש להחזיר מערך ריק.
- כתבו
backtrack(r, c). אם(r, c)היא נקודת היציאה, שמרו את המהלכים הנוכחיים וחזרו. - הגדירו את
maze[r][c]ל-0. - עבור כל מהלך בסדר
D,L,R,Uשמגיע לתא פתוח בתוך המבוך, הוסיפו את האות שלו, בצעו קריאה רקורסיבית והסירו את האות. - הגדירו את
maze[r][c]בחזרה ל-1. קראו ל-backtrack(0, 0)והחזירו את הנתיבים שנשמרו.
def findPaths(maze):
n = len(maze)
result = []
if maze[0][0] == 0 or maze[n - 1][n - 1] == 0:
return result
# The moves in alphabetical order, so the paths come out sorted.
moves = (("D", 1, 0), ("L", 0, -1), ("R", 0, 1), ("U", -1, 0))
path = []
def backtrack(r, c):
if r == n - 1 and c == n - 1:
result.append("".join(path))
return
maze[r][c] = 0 # block the cell while this path uses it
for letter, dr, dc in moves:
nr, nc = r + dr, c + dc
if 0 <= nr < n and 0 <= nc < n and maze[nr][nc] == 1:
path.append(letter)
backtrack(nr, nc)
path.pop()
maze[r][c] = 1 # open it again for the other paths
backtrack(0, 0)
return result
מלכודות ומקרי קצה
- לא לבטל את הסימון של תא כשחוזרים לאחור. התא נשאר חסום, ולכן שום ענף מאוחר יותר לא יכול לעבור דרכו. במבוך פתוח בגודל 3 × 3 שמחזיר
DDRRו־DDRURD, 2 מתוך 12 המסלולים. - לשכוח שתא ההתחלה או תא היציאה יכולים להיות קיר. קיר בתא היציאה, כמו בדוגמה השנייה, חייב להחזיר תשובה ריקה, ולא מסלול שמסתיים לידו.
- לנסות את המהלכים בסדר למטה, למעלה, שמאלה, ימינה. קבוצת המסלולים נכונה, אבל הסדר שלהם לא;
D,L,R,Uהוא הסדר האלפביתי.
שאלות נפוצות3
מהי סיבוכיות הזמן של Rat in a Maze?
אחרי הצעד הראשון שלו, למסלול יש לכל היותר שלוש אפשרויות בכל תא, והוא מבקר לכל היותר ב־n² תאים, לכן יש O(3^(n²)) מסלולים חלקיים, שכל אחד מהם דורש O(n²) כדי לכתוב אותו. קירות ותאים שכבר נעשה בהם שימוש מצמצמים את המספר הזה במידה רבה.
למה צריך להסיר את סימון הביקור כשחוזרים לאחור?
הסימון חייב לציין "במסלול הנוכחי", ולא "נראה בעבר". שני מסלולים שונים עשויים לעבור באותו תא. אם תא נשאר מסומן אחרי שהחיפוש חוזר ממנו לאחור, כל המסלולים המאוחרים יותר שעוברים בתא הזה יאבדו.
מה אם החולדה יכולה לנוע רק ימינה ולמטה?
אז שום מסלול לא יכול לבקר שוב בתא, ולכן אין צורך בסימוני ביקור, ולכל מסלול יש בדיוק 2n-2 צעדים. ספירת המסלולים הופכת לתכנות דינמי: מספר הדרכים להגיע לתא הוא מספר הדרכים להגיע לתא הפתוח שמעליו ועוד מספר הדרכים להגיע לתא הפתוח שמשמאלו.
בעיות דומות
בעיות שמשתמשות באותם רעיונות. פתרון של שתיים או שלוש מהן מקבע את הדפוס.
def findPaths(maze):
# כתבו כאן את הקודבדיקה #1
בדיקה #2
ארגומנט
maze = [[1, 1, 0, 0], [0, 1, 1, 0], [1, 1, 1, 0], [1, 0, 1, 1]]
הפלט הצפוי
["RDDRDR", "RDRDDR"]