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שנייה, ולכןaAAמחזיר true.
שאלות נפוצות3
מהי סיבוכיות הזמן של חיפוש מילים?
O(m·n·3^L) עבור לוח בגודל m × n ומילה באורך L: כל תא יכול להיות נקודת התחלה של מסלול, ולאחר הצעד הראשון יש לכל תא לכל היותר שלושה שכנים שלא ביקרנו בהם. המרחב הנוסף הוא O(L) עבור הרקורסיה, ועוד O(m·n) עבור עותק של הלוח.
איך גיזום הופך את חיפוש המילים למהיר יותר?
תחילה ספור את האותיות, והחזר false אם יש על הלוח מעט מדי מאחת מהן. לאחר מכן חפש מהסוף באמצעות האות הנדירה יותר, שממנה יכולים להתחיל פחות תאים. אף אחת מהבדיקות לא משנה את המקרה הגרוע ביותר.
מה ההבדל בין Word Search ל־Word Search II?
ב-Word Search II נשאל אילו מילים מתוך רשימה מופיעות בלוח. חיפוש פעם אחת לכל מילה חוזר על אותה עבודה, ולכן הפתרון המקובל מכניס את המילים לעץ trie ועובר על הלוח פעם אחת, תוך נטישת מסלול כשאף מילה לא מתחילה באותיות שלו.
בעיות דומות
בעיות שמשתמשות באותם רעיונות. פתרון של שתיים או שלוש מהן מקבע את הדפוס.
def exist(board, word):
# כתבו כאן את הקודבדיקה #1
בדיקה #2
בדיקה #3
ארגומנטים
board = ["STAR", "POOL", "ENDS"] word = "STOOLS"
הפלט הצפוי
true