Word Search
لديك شبكة من الأحرف board، وهي قائمة من السلاسل النصية حيث يكون board[r][c] هو الحرف في الصف r، والعمود c، وسلسلة نصية word.
أعِد true إذا كان بإمكانك تتبّع word: ابدأ من أي خلية وانتقل في كل مرة إلى الخلية التي فوقها أو تحتها أو إلى يسارها أو يمينها، بحيث تكوّن الخلايا التي تزورها word بالترتيب، من دون استخدام أي خلية مرتين. وإلا فأعِد false. الأحرف حساسة لحالة الأحرف.
الدالة
- boardstring-array
- الشبكة، سلسلة من الأحرف في كل صف
- wordstring
- الكلمة المراد تتبّعها
- تُرجعboolean
- ما إذا كان يمكن تتبّع الكلمة عبر خلايا متجاورة، مع استخدام كل خلية مرة واحدة على الأكثر
القيود
1 ≤ board.length ≤ 61 ≤ board[i].length ≤ 6، ويكون طول كل صف متساويًا.1 ≤ word.length ≤ 20boardوwordيحتويان فقط على أحرف إنجليزية، كبيرة وصغيرة.
أمثلة
- المدخلات
- board = ["STAR", "POOL", "ENDS"]word = "STOOLS"
- المخرجات
- true
- الشرح
- ابدأ عند
Sفي أعلى اليسار، واتجه يمينًا إلىT، ثم انزل إلىO، واتجه يمينًا إلىOوL، ثم انزل إلىSأسفلL. لا تتكرر أي خلية.
- المدخلات
- board = ["STAR", "POOL", "ENDS"]word = "POP"
- المخرجات
- false
- الشرح
- تحتوي اللوحة على
Pواحدة. بعدPوOتحتاج إلىPأخرى، والوحيدة هي خلية البداية، التي لا يمكن استخدامها مرتين.
- المدخلات
- board = ["STAR", "POOL", "ENDS"]word = "SAND"
- المخرجات
- false
- الشرح
- كل حرف من
SANDموجود على اللوحة، لكن حرفAالوحيد، في الصف 0 والعمود 2، لا يلامس أيًا من حرفيS.
سؤال إضافي
بدلًا من الإجابة بنعم أو لا، هل يمكنك عدّ عدد المسارات المختلفة لرسم word على اللوحة؟
تلميحات
افتحها واحدًا تلو الآخر. كل تلميح يكشف أكثر قليلًا.
جرّب كل خلية لتكون موضع بداية الكلمة. عندما تطابق خلية الحرف الحالي، فأيّ الخلايا يمكن أن تحتوي على الحرف التالي؟
هذا بحث عبر المسارات: عند كل حرف تختار واحدًا من أربعة جيران كحد أقصى، والاختيار الخاطئ يعني الرجوع خطوة وتجربة خيار آخر. ولأن المسار لا يجوز أن يعيد استخدام خلية، ضع علامة على الخلية ما دامت ضمن المسار الحالي، وأزل العلامة عنها عند الرجوع عنها.
اكتب
dfs(r, c, i): أعد الفشل إذا كان(r, c)خارج الشبكة، أو كان موجودًا بالفعل في المسار، أو لم يكنword[i]؛ وأعد النجاح إذا كانiهو الفهرس الأخير؛ وإلا فضع علامة على الخلية، وجرّب الجيران الأربعة باستخدامi+1، وأزل العلامة عنها، وأعِد ما إذا كان أي جار قد نجح. قبل البحث، تحقّق من أن اللوحة تحتوي على عدد كافٍ من كل حرف، وابدأ من الطرف الذي يحتوي على الحرف الأقل شيوعًا في الكلمة.
الحل
عليك البحث عن المسارات عبر الشبكة. ينمّي التراجع مسارًا حرفًا تلو الآخر، مع وضع علامة على كل خلية ما دام المسار يمر بها وإزالة العلامة عند الرجوع، حتى لا يعيد أي مسار استخدام خلية. يزداد البحث أُسّيًا مع طول الكلمة، وهو ما تسمح به لوحة 6 × 6.
التراجع باستخدام شبكة تمت زيارتها
الفكرة
يواصل المسار الذي يطابق أول i أحرف إلى أي خلية مجاورة تحتوي على word[i] وليست ضمن المسار. إذا لم توجد خلية مجاورة كهذه، يصل المسار إلى طريق مسدود، فتعود خطوة إلى الوراء.
إزالة العلامة هي ما يجعل هذا صحيحًا: يجب أن تكون الخلية التي مرّ بها مسار وصل إلى طريق مسدود متاحة للمحاولة التالية. على اللوحة AA / AB مع AAA، تتعثر كلتا المحاولتين انطلاقًا من أعلى اليسار. لو بقيت خلاياهما معلّمة، لفاتتنا الإجابة: أسفل اليسار ثم أعلى اليسار ثم أعلى اليمين.
بعد الخطوة الأولى، لكل خطوة ثلاثة اتجاهات جديدة على الأكثر، لذا يمكن للبحث استكشاف ما يصل إلى m·n·3^L مسارًا. على لوحة 5 × 5 مكوّنة من A، مع 8 أحرف A يتبعها حرف B، يجري البحث نحو 53,000 فحص للخلية قبل أن يعيد false.
الخوارزمية
- أنشئ شبكة
visited، واجعل جميع قيمها false. dfs(r, c, i): أعد false إذا كان(r, c)خارج الشبكة، أو تمت زيارته، أو لم يكنword[i]؛ وأعد true إذا كانiهو الفهرس الأخير.- وإلا، علّم
(r, c)، وجرّب الجيران الأربعة باستخدامi+1، ثم أزل العلامة عنه، وأعد ما إذا كانت أي محاولة قد نجحت. - أعد true إذا نجح
dfs(r, c, 0)بدءًا من أي خلية.
def exist(board, word):
rows, cols = len(board), len(board[0])
visited = [[False] * cols for _ in range(rows)]
def dfs(r, c, i):
# Can word[i:] be traced starting at cell (r, c)?
if r < 0 or r >= rows or c < 0 or c >= cols:
return False
if visited[r][c] or board[r][c] != word[i]:
return False
if i == len(word) - 1:
return True
visited[r][c] = True # mark: the current path owns this cell
found = (dfs(r + 1, c, i + 1) or dfs(r - 1, c, i + 1)
or dfs(r, c + 1, i + 1) or dfs(r, c - 1, i + 1))
visited[r][c] = False # restore: other paths may use it
return found
for r in range(rows):
for c in range(cols):
if dfs(r, c, 0):
return True
return Falseالتراجع باستخدام العلامات في موضعها والتقليم
الفكرة
أبقِ على البحث، مع تعديلين. علِّم الخلايا على نسخة من اللوحة بكتابة #، وأعِد الحرف عند الرجوع. لا يطابق # أي حرف من الكلمة، لذا فإن فحص الحرف يرفض أيضًا الخلايا الموجودة على المسار.
ثم قلِّم البحث. إذا احتاجت الكلمة إلى كمية من أحد الأحرف أكبر مما تحتويه اللوحة، فأعِد false: فهذا يجيب عن اللوحة المكوّنة بالكامل من A دون بحث. يتهجّى المسار عند قراءته بالعكس الكلمة المعكوسة على الخلايا نفسها، لذا فإن البحث عن الكلمة المعكوسة يعطي الإجابة نفسها. ابدأ من الطرف الذي حرفه أندر على اللوحة: إذ يمكن لعدد أقل من الخلايا أن يكون نقطة بداية، وتفشل البدايات الخاطئة فورًا.
عندما يكون حرف B الوحيد في زاوية بين حرفَي C، يجري البحث الأمامي نحو 35,000 فحص للخلايا؛ أما عند عكس الكلمة، فيجري 27 فحصًا. تظل أسوأ حالة O(m·n·3^L).
الخوارزمية
- إذا كانت الكلمة تحتاج إلى عدد من أي حرف أكبر مما تحتويه اللوحة، فأعِد false.
- إذا كانت اللوحة تحتوي على نسخ من
word[0]أكثر من نسخ الحرف الأخير، فاعكسword. - انسخ اللوحة إلى شبكة يمكنك تعديلها.
dfs(r, c, i): أعد الفشل إذا لم تكن الخلية هيword[i]؛ وأعِد النجاح عند الفهرس الأخير؛ وإلا فعيّن قيمتها إلى#، وجرّب كل جار داخل الحدود باستخدامi+1، ثم أعد القيمة الأصلية، وأعِد ما إذا كان أيٌّ منها قد نجح.- أعِد true إذا نجح
dfsانطلاقًا من أي خلية.
from collections import Counter
def exist(board, word):
rows, cols = len(board), len(board[0])
# Pruning 1: the board must hold every letter as many times as the word uses it.
have = Counter("".join(board))
for letter, need in Counter(word).items():
if have[letter] < need:
return False
# Pruning 2: a path read backwards is the same path, so start from the
# end whose letter is rarer on the board: fewer cells begin a search.
if have[word[0]] > have[word[-1]]:
word = word[::-1]
grid = [list(row) for row in board]
def dfs(r, c, i):
# Can word[i:] be traced starting at cell (r, c)?
if grid[r][c] != word[i]:
return False
if i == len(word) - 1:
return True
grid[r][c] = "#" # mark: "#" matches no letter, so this path cannot reuse the cell
found = False
for nr, nc in ((r + 1, c), (r - 1, c), (r, c + 1), (r, c - 1)):
if 0 <= nr < rows and 0 <= nc < cols and dfs(nr, nc, i + 1):
found = True
break
grid[r][c] = word[i] # restore the letter for other paths
return found
for r in range(rows):
for c in range(cols):
if dfs(r, c, 0):
return True
return False
أخطاء شائعة وحالات حدّية
- عدم إلغاء تحديد خلية بعد فشل أحد الفروع: تعطي
AAAعلىAA/ABالنتيجة false. - عدم وضع علامة مطلقًا: يعود المسار إلى خليته الأخيرة، وتُرجع
POPعلى لوحة المثال true. - قراءة الخلية قبل التحقق من الحدود: في Python، تكون
board[-1]هي الصف الأخير، لذا تلتف المسارات حول الشبكة. - التحقق من النجاح بعد إجراء حركة فقط: يجب أن تُرجع
["A"]معAالقيمة true. - وضع علامة بتبديل حالة الأحرف في خلية: في
["aA"]، تُقرأaالتي وُضعت عليها علامة كأنهاAثانية، لذا تكون نتيجةaAAtrue.
أسئلة شائعة3
ما التعقيد الزمني لمسألة البحث عن الكلمات؟
O(m·n·3^L) للوحة أبعادها m × n وكلمة طولها L: يمكن لكل خلية أن تكون بداية مسار، وبعد الخطوة الأولى يكون لكل خلية ثلاثة جيران غير مزورين على الأكثر. المساحة الإضافية هي O(L) للاستدعاء التعاودي، بالإضافة إلى O(m·n) لنسخة من اللوحة.
كيف يجعل التقليم البحث عن الكلمات أسرع؟
أحصِ الحروف أولًا، وأعِد false إذا كانت اللوحة تحتوي على عدد قليل جدًا من أحدها. ثم ابحث من النهاية باستخدام الحرف الأقل شيوعًا، الذي يمكن لعدد أقل من الخلايا أن تبدأ به. لا يغيّر أيٌّ من الفحصين الحالة الأسوأ.
ما الفرق بين Word Search وWord Search II؟
يسأل Word Search II عن الكلمات الموجودة في قائمة والتي تظهر على اللوحة. البحث مرة واحدة لكل كلمة يكرر العمل، لذا يضع الحل المعتاد الكلمات في شجرة بادئة ويتنقل عبر اللوحة مرة واحدة، متخليًا عن المسار عندما لا تبدأ أي كلمة بالحروف التي يتضمنها.
مسائل مشابهة
مسائل تعتمد على الأفكار نفسها. حلّ اثنتين أو ثلاث منها يثبّت النمط.
def exist(board, word):
# اكتب الكود هنااختبار #1
اختبار #2
اختبار #3
الوسائط
board = ["STAR", "POOL", "ENDS"] word = "STOOLS"
الإخراج المتوقع
true