Group Anagrams
ניתנת לך רשימת מילים strs. שתי מילים הן אנגרמות כאשר אחת מסדרת מחדש את האותיות של האחרת, וכל אות מופיעה אותו מספר פעמים. קבץ כל מילה עם כל האנגרמות שלה והחזר מחרוזת אחת לכל קבוצה: המילים שלה בסדר אלפביתי, מופרדות ברווח יחיד. סדר את הקבוצות בסדר אלפביתי לפי המילה הראשונה שלהן.
מילה שחוזרת מופיעה פעמיים בקבוצה שלה, ומילה שאין לה אנגרמה יוצרת קבוצה של מילה אחת. סדר אלפביתי פירושו סדר מילוני: aab מופיעה לפני ab, ו-ab לפני abc.
פונקציה
- strsstring-array
- המילים שיש לקבץ, אותיות קטנות בלבד
- מחזירהstring-array
- מחרוזת אחת לכל קבוצה: המילים שבה ממוינות ומחוברות ברווחים, והקבוצות מסודרות לפי המילה הראשונה שלהן
אילוצים
1 ≤ strs.length ≤ 40001 ≤ strs[i].length ≤ 8- כל מילה מכילה אותיות אנגליות קטנות בלבד.
דוגמאות
- קלט
- strs = ["listen", "stone", "silent", "notes", "enlist", "onset", "tones", "apple"]
- פלט
- ["apple", "enlist listen silent", "notes onset stone tones"]
- הסבר
enlist,listenו-silentמשתמשות באותיות e, i, l, n, s ו-t פעם אחת כל אחת.notes,onset,stoneו-tonesחולקות את האותיות e, n, o, s ו-t.appleלא תואמת לאף אחת, ולפי המילה הראשונה היא מובילה.
- קלט
- strs = ["race", "arc", "care", "car", "acre"]
- פלט
- ["acre care race", "arc car"]
- הסבר
acre,careו-raceחולקות את האותיות a, c, e ו-r. ב-arcוב-carאין e, ולכן הן יוצרות קבוצה משלהן.acreמופיעה לפניarcכי c מופיעה לפני r באות השנייה.
- קלט
- strs = ["b", "a", "b"]
- פלט
- ["a", "b b"]
- הסבר
- שני העותקים של
bהם אנגרמות זה של זה, ושניהם נשארים בקבוצה. ל־aאין בן זוג והוא מופיע ראשון.
שאלת המשך
אם המילים יכולות להכיל כל תווי Unicode, איזה מפתח — אותיות ממוינות או ספירת האותיות — עדיין יעבוד, ומה היית משנה בו?
רמזים
פתחו אותם אחד אחד. כל רמז חושף קצת יותר.
שתי מילים הן אנגרמות אם ורק אם הן מכילות את אותן אותיות באותן כמויות. מה אפשר לחשב ממילה אחת, בלי להסתכל על האחרות, ולקבל אותה תוצאה עבור כל האנגרמות שלה?
ממיינים את האותיות בכל מילה: גם
listenוגםsilentהופכות ל־eilnst. הצורה הממוינת הזאת משמשת כשם הקבוצה, ולכן מפת גיבוב שממפה אותה לרשימת מילים אוספת את כל הקבוצות במעבר אחד.מיין את כל הקלט לפני שמקבצים אותו. המילים מגיעות אז בסדר אלפביתי, כך שהרשימה של כל קבוצה כבר מסודרת, וכל קבוצה נוצרת כשהמילה הראשונה שלה מגיעה. חבר את כל הרשימות באמצעות רווחים.
פתרון
חשבו מפתח מכל מילה בנפרד, שיהיה זהה לכל האנגרמות שלה ושונה עבור כל מילה אחרת. אותיות של מילה בסדר ממויין הן מפתח כזה, ומפת גיבוב ממפתח לקבוצה אוספת את הקבוצות במעבר אחד. מיינו את המילים תחילה, והסדר הנדרש יתקבל בחינם.
השווה כל מילה לכל קבוצה
נכונה, אבל לא מסתיימת בבדיקות הגדולות ביותר
האינטואיציה
שמרו רשימה של קבוצות והשוו כל מילה למילה הראשונה בכל קבוצה. היותן אנגרמות היא יחס טרנזיטיבי, ולכן השוואה אחת לכל קבוצה קובעת אם המילה שייכת אליה. שתי מילים הן אנגרמות כאשר יש להן אותו אורך ואותו מספר מופעים של כל אחת מ-26 האותיות. מיינו תחילה את הקלט: כך כל קבוצה מתמלאת לפי סדר האלפבית, והקבוצות נוצרות לפי סדר המילה הראשונה שלהן.
כשאין שתי מילים שהן אנגרמות, כל מילה נבדקת מול כל הקבוצות שלפניה: 4000 מילים מובילות לכ-8 × 10^6 השוואות, שבכל אחת מהן נבדקים עד 8 אותיות ו-26 מונים.
אלגוריתם
- מיינו את המילים לפי סדר אלפביתי.
- עבור כל מילה, מצאו קבוצה שהמילה הראשונה בה מכילה את אותן כמויות של אותיות, והוסיפו אליה את המילה. אם אין קבוצה מתאימה, התחילו קבוצה חדשה.
- החזירו את המילים בכל קבוצה כשהן מחוברות ברווחים יחידים, לפי סדר יצירת הקבוצות.
def is_anagram(a, b):
# Same length, and every letter appears as often in a as in b.
if len(a) != len(b):
return False
counts = [0] * 26
for c in a:
counts[ord(c) - 97] += 1
for c in b:
counts[ord(c) - 97] -= 1
return all(x == 0 for x in counts)
def groupAnagrams(strs):
# Sort first: each group fills up in alphabetical order,
# and groups are created in the order of their first word.
groups = []
for word in sorted(strs):
for group in groups:
if is_anagram(group[0], word):
group.append(word)
break
else:
groups.append([word])
return [" ".join(group) for group in groups]קבץ לפי אותיות ממוינות במפת גיבוב
האינטואיציה
חשבו את שם הקבוצה מתוך המילה עצמה. מיינו את האותיות במילה, וכל אנגרמה תיתן את אותו הטקסט: listen, silent ו־enlist הופכות כולן ל־eilnst, ואילו stone הופכת ל־enost. שתי מילים חולקות צורה ממוינת בדיוק כאשר יש בהן אותן אותיות באותה כמות, וזוהי ההגדרה של אנגרמה. מפת גיבוב מהמפתח לרשימת המילים מקבצת את הכול במעבר אחד, ואף מילה אינה מושווית לקבוצה אחרת.
הסדר מתקבל ממיון הקלט תחילה, כמו קודם: כל רשימה מתמלאת בסדר אלפביתי, ומפתח נכנס למפה כשמגיעה המילה הראשונה בקבוצה שלו. מפה ששומרת על סדר ההכנסה מחזירה את הקבוצות בסדר הזה; אחרת, שמרו את הקבוצות ברשימה ומפו כל מפתח לאינדקס שלו.
מיון הקלט דורש כ־5 × 10^4 השוואות בין מילים עבור 4000 מילים, במקום 8 × 10^6.
אלגוריתם
- מיין את המילים בסדר אלפביתי.
- עבור כל מילה, בנה את המפתח שלה על ידי מיון האותיות שלה.
- חפש את המפתח במפת גיבוב. אם הוא חדש, התחל עבורו קבוצה ריקה, תוך שמירה על סדר יצירת הקבוצות.
- הוסף את המילה לקבוצה של המפתח שלה.
- החזר את המילים של כל קבוצה כשהן מחוברות ברווחים בודדים, ואת הקבוצות לפי סדר יצירתן.
def groupAnagrams(strs):
# Sort first: each group fills up in alphabetical order,
# and groups are created in the order of their first word.
groups = {} # key (the letters in sorted order) -> the group's words
for word in sorted(strs):
key = "".join(sorted(word))
groups.setdefault(key, []).append(word)
# A dict keeps insertion order, so the groups come out by first word.
return [" ".join(words) for words in groups.values()]
מלכודות ומקרי קצה
- סידור הקבוצות לפי המפתח במקום לפי המילה הראשונה. עבור
["cab", "bad"]המפתחותabcו-abdמציבים אתcabראשונה, אבלbadאמורה להוביל. - איסוף המילים בקבוצה.
["b", "a", "b"]חייב להחזירb b; קבוצה שומרת עותק אחד בלבד. - בניית מפתח שמורכב מהאותיות השונות בלבד.
abו-aabbמשתמשות באותן שתי אותיות, אבל הן אינן אנגרמות. - מיון כל קבוצה, אך לא הקלט, ואי-מיון הקבוצות. כך סדר ההכנסה עוקב אחר הקלט, ולא אחר המילים הראשונות.
שאלות נפוצות3
מהי סיבוכיות הזמן של Group Anagrams?
בניית המפתחות הממוינים לפי אותיות דורשת O(n · k log k) עבור n מילים שאורכן עד k אותיות, ועבודת המפה היא O(n · k). מיון המילים כדי לסדר את הפלט מוסיף O(n · k · log n), וזהו הגורם הדומיננטי. נדרשים O(n · k) מקום עבור המפתחות והקבוצות.
האם מפתח שמבוסס על ספירת אותיות מהיר יותר ממיון של כל מילה?
מפתח ספירה, ספירות האותיות של 26 האותיות הכתובות כטקסט כגון 1#0#2#…, דורש O(k) במקום O(k log k), ולכן עדיף עבור מילים ארוכות. עד 8 אותיות, המיון מהיר באותה מידה. שני המפתחות מדויקים: לשתי מילים יש אותן ספירות בדיוק אם ורק אם האותיות הממוינות שלהן זהות.
למה לא להשתמש בסכום של קודי האותיות כמפתח?
אותיות שונות יכולות להסתכם באותו סכום: a + d שווה ל־b + c, ולכן ad ו־bc יהיו באותה קבוצה. מכפלה של מספר ראשוני אחד לכל אות היא מדויקת, אבל כשמשתמשים ב־101 עבור z, מילה המורכבת מעשרה z-ים חורגת מטווח הערכים של מספר שלם בן 64 סיביות.
בעיות דומות
בעיות שמשתמשות באותם רעיונות. פתרון של שתיים או שלוש מהן מקבע את הדפוס.
def groupAnagrams(strs):
# כתבו את הקוד כאןבדיקה #1
בדיקה #2
בדיקה #3
ארגומנט
strs = ["listen", "stone", "silent", "notes", "enlist", "onset", "tones", "apple"]
הפלט הצפוי
["apple", "enlist listen silent", "notes onset stone tones"]