Letter Combinations of a Phone Number
בלוח המקשים של טלפון, לכל ספרה מ־2 עד 9 משויכות כמה אותיות: ל־2 יש abc, ל־3 יש def, ל־4 יש ghi, ל־5 יש jkl, ל־6 יש mno, ל־7 יש pqrs, ל־8 יש tuv ול־9 יש wxyz.
מקבלים מחרוזת digits. בחרו אות אחת לכל ספרה, תוך שמירה על סדר הספרות, ותקבלו מחרוזת אחת שאפשר להקליד באמצעות המקשים. החזירו את כל המחרוזות האפשריות, ממוינות בסדר לקסיקוגרפי (מילוני).
פונקציה
- digitsstring
- הספרות שנלחצו, כל אחת בין 2 ל־9
- מחזירהstring-array
- כל מחרוזת שהמקשים יכולים להקליד, בסדר לקסיקוגרפי
אילוצים
1 ≤ digits.length ≤ 4- כל תו ב־
digitsהוא ספרה בין2ל־9. - התשובה מכילה לכל היותר
44 = 256מחרוזות.
דוגמאות
- קלט
- digits = "23"
- פלט
- ["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"]
- הסבר
- 2 מציע
a,b,cו־3 מציעd,e,f. כל אות ראשונה מצטרפת לכל אות שנייה: 3 × 3 = 9 מחרוזות, ממוינות כך שהאות הראשונה משתנה בקצב האיטי ביותר.
- קלט
- digits = "7"
- פלט
- ["p", "q", "r", "s"]
- הסבר
- בספרה אחת, כל אחת מאותיותיה היא תשובה שלמה. 7 היא אחת משתי המקשים שיש בהם ארבע אותיות, ולכן לתשובה יש ארבע מחרוזות.
- קלט
- digits = "94"
- פלט
- ["wg", "wh", "wi", "xg", "xh", "xi", "yg", "yh", "yi", "zg", "zh", "zi"]
- הסבר
- ל־9 יש ארבע אותיות ול־4 יש שלוש, ולכן יש 4 × 3 = 12 מחרוזות. כל שלוש המחרוזות שמתחילות ב־
wמופיעות לפני הראשונה שמתחילה ב־x.
שאלת המשך
נניח שרוצים רק את הצירופים שהם מילים אמיתיות במילון. איך אפשר להימנע מבניית כל המחרוזות באורך n מתוך 4 אפשרויות לכל תו?
רמזים
פתחו אותם אחד אחד. כל רמז חושף קצת יותר.
ציירו את האפשרויות כעץ. ברמה הראשונה בוחרים אות לספרה הראשונה, ברמה השנייה אות לספרה השנייה, וכן הלאה. מה מאיית הנתיב מהשורש ועד לעלה?
כל עלה הוא תשובה אחת, וכל תשובה היא עלה אחד. סרקו את העץ לעומק, נסו את האותיות של כל מקש משמאל לימין, ותפגשו את העלים בסדר מילוני.
שמרו על מחרוזת אחת שגדלה. במיקום
i, הוסיפו בתור כל אות מתוךdigits[i], המשיכו למיקוםi+1, ואז הסירו שוב את האות. כש-iמגיע לסוף שלdigits, שמרו עותק של המחרוזת.
פתרון
התשובה מכילה עד 4^n מחרוזות, ולכן כל פתרון משקיע לפחות עבודה בהיקף הזה כדי לכתוב אותן. מה שחשוב הוא להפיק כל בחירה פעם אחת, בלי להחמיץ אף אחת ובלי לחזור עליה. זהו חיפוש עם חזרה לאחור בעץ החלטות, עם רמה אחת לכל ספרה, כאשר כל עלה הוא תשובה.
בנו את המחרוזות ספרה אחת בכל פעם
האינטואיציה
מתחילים ברשימה שמכילה מחרוזת ריקה אחת ומרחיבים אותה בספרה אחת בכל פעם. עבור "23", הספרה 2 הופכת אותה ל־a, b, c, והספרה 3 מרחיבה כל אחת מהן באמצעות d, e ו־f, וכך מתקבלות תשע מחרוזות.
הסדר נשאר ממוין. אם הרשימה ממוינת לפני שמוסיפים ספרה, מרחיבים את הקידומות לפי הסדר הזה, ואת כל קידומת לפי האותיות של המקשים שלה משמאל לימין: קידומת שמופיעה קודם עדיין נשארת ראשונה, וקידומות זהות מסודרות לפי האות החדשה.
העבודה היא בהתאם לגודל התשובה, אבל בזמן שבונים רמה, כל הרמה הקודמת נשארת גם היא בזיכרון.
אלגוריתם
- התחילו עם
combos = [""], קידומת ריקה אחת. - עבור כל ספרה, צרו רשימה חדשה: עבור כל קידומת ב-
combosועבור כל אות במקש של אותה ספרה, הוסיפוprefix + letter. - החליפו את
combosברשימה החדשה. - אחרי הספרה האחרונה, החזירו את
combos.
KEYPAD = {"2": "abc", "3": "def", "4": "ghi", "5": "jkl",
"6": "mno", "7": "pqrs", "8": "tuv", "9": "wxyz"}
def letterCombinations(digits):
combos = [""] # every prefix built so far; one empty prefix to start
for digit in digits:
# Each old prefix grows by each letter of this digit, in order.
combos = [prefix + letter for prefix in combos for letter in KEYPAD[digit]]
return combosחזרה לאחור לאורך עץ ההחלטות
האינטואיציה
התשובות הן העלים של עץ החלטה. השורש הוא המחרוזת הריקה, וכל רמה מוסיפה אות אחת של הספרה הבאה: עבור "23" לשורש יש ילדים a, b, c, ולכל אחד מהם ילדים עבור d, e, f.
עוברים על העץ לעומק תחילה באמצעות מאגר אחד, path. ברמה i, בוחרים אות מתוך digits[i] על ידי הוספתה, חוקרים באמצעות קריאה רקורסיבית עם i+1, ומבטלים על ידי הסרתה. הביטול מאפשר למאגר אחד לשרת את כל העץ: אחרי ad, ae ו-af, הסרה מחזירה את path ל-a, ואז למחרוזת הריקה, ומכינה אותו ל-b. כש-i מגיע לסוף של digits, שומרים עותק.
הסיבוכיות בזמן עדיין O(4^n · n), אבל הזיכרון הנוסף הוא המאגר ומחסנית הקריאות, O(n).
אלגוריתם
- השאר נתיב ריק ו־
pathו־resultריקים. - הגדר
backtrack(i): אםiשווה לאורך שלdigits, שמור עותק שלpathוחזור. - אחרת, עבור כל אות במקש של
digits[i], לפי הסדר: הוסף אותה ל־path, קרא ל־backtrack(i+1), ואז הסר אותה. - קרא ל־
backtrack(0)והחזר אתresult.
KEYPAD = {"2": "abc", "3": "def", "4": "ghi", "5": "jkl",
"6": "mno", "7": "pqrs", "8": "tuv", "9": "wxyz"}
def letterCombinations(digits):
result = []
path = [] # the letters chosen so far, one per digit
def backtrack(i):
if i == len(digits):
# Every digit has a letter: this leaf is one finished string.
result.append("".join(path))
return
for letter in KEYPAD[digits[i]]:
path.append(letter) # choose
backtrack(i + 1) # explore the digits after this one
path.pop() # undo, so the next letter can take its place
backtrack(0)
return result
מלכודות ומקרי קצה
- הנחה שלכל מקש יש שלוש אותיות. לקיחת שלוש אותיות מהאינדקס
(d-2)*3באלפבית משמיטה אתsמ־7 ומתחילה את 8 ב־s. כתבו את לוח המקשים כטבלה. - שוכחים לבצע ביטול.
pathממשיך לגדול, והתשובה השנייה עבור"23"יוצאתadeבמקוםae. - שומרים את המאגר במקום עותק. ב־Python,
result.append(path)שומר את אותה רשימה תשע פעמים, ובסוף היא ריקה. - מנסים את האותיות של מקש מימין לשמאל, או בונים מחרוזות מתוך מחסנית בגרסה האיטרטיבית. כל אחת מהפעולות האלה מפרה את הסדר הממוין.
- ב־PHP וב־R, ייתכן ש־
"23"יתקבל כמספר 23. המירו אותו לטקסט לפני שתיגשו לתווים שלו לפי אינדקס.
שאלות נפוצות3
מהי סיבוכיות הזמן של צירופי אותיות של מספר טלפון?
O(4^n · n) עבור n ספרות: ייתכנו 4^n מחרוזות, כאשר כל ספרה היא 7 או 9, וכתיבת כל אחת מהן דורשת n צעדים. זהו גודל הפלט, ולכן אין פתרון יעיל יותר. חיפוש עם חזרה לאחור דורש O(n) מקום נוסף, מעבר לפלט.
למה חזרה לאחור מחזירה את הצירופים בסדר ממוין?
לכל התשובות אותו אורך, וסריקה לעומק מסיימת כל מחרוזת שמתחילה ב־a לפני שהיא בוחרת ב־b. הדבר נכון בכל רמה, כל עוד מנסים את האותיות של כל מפתח משמאל לימין. זהו סדר מילוני, ולכן אין צורך במיון.
ומה לגבי הספרות 0 ו-1?
בלוח המקשים של הטלפון, 0 ו־1 אינם משויכים לאותיות, והגרסה הזו משתמשת רק ב־2 עד 9. אם הם היו יכולים להופיע, היה עליך להחליט אם מדלגים על ספרה כזו או שהיא גורמת לתשובה להיות ריקה. בריאיון, שאל איזו אפשרות רצויה.
בעיות דומות
בעיות שמשתמשות באותם רעיונות. פתרון של שתיים או שלוש מהן מקבע את הדפוס.
def letterCombinations(digits):
# כתבו כאן קודבדיקה #1
בדיקה #2
בדיקה #3
ארגומנט
digits = "23"
הפלט הצפוי
["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"]