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، أي مسارين من أصل 12. - نسيان أن نقطة البداية أو المخرج قد تكون جدارًا. يجب أن يؤدي وجود جدار عند المخرج، كما في المثال الثاني، إلى إجابة فارغة، لا إلى مسار ينتهي بجواره.
- تجربة التحركات بالترتيب: أسفل، أعلى، يسار، يمين. مجموعة المسارات صحيحة لكن ترتيبها غير صحيح؛ الترتيب الأبجدي هو
DوLوRوU.
أسئلة شائعة3
ما هو التعقيد الزمني لخوارزمية «الفأر في المتاهة»؟
بعد حركته الأولى، يكون للمسار ثلاثة خيارات على الأكثر عند كل خلية، ويزور 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"]