Alien Dictionary
רשימת מילים ממוינת לפי אלפבית שאינך מכיר: 26 האותיות האנגליות הקטנות בסדר סודי כלשהו. משווים מילים כרגיל. המיקום הראשון שבו שתי מילים שונות הוא שקובע, וכאשר מילה אחת היא תחילתה של מילה אחרת, המילה הקצרה יותר מופיעה קודם.
החזר את האותיות שמופיעות במילים, כמחרוזת אחת בסדר האלפביתי. כשכמה סדרים מתאימים, החזר את הסדר שמופיע ראשון לפי סדר מילוני רגיל. כשאין סדר מתאים, החזר "invalid".
פונקציה
- wordsstring-array
- המילים, ממוינות לפי האלפבית הלא ידוע
- מחזירהstring
- האותיות בסדר הקטן ביותר שמתאים, או "invalid"
אילוצים
1 ≤ words.length ≤ 50001 ≤ words[i].length ≤ 10- כל מילה מכילה רק אותיות אנגליות קטנות.
- אותה מילה עשויה להופיע יותר מפעם אחת.
דוגמאות
- קלט
- words = ["tea", "ten", "ate", "act", "cat"]
- פלט
- "etacn"
- הסבר
teaלפניtenמציב את a לפני n. שאר הזוגות נותנים t לפני a, t לפני c ו־a לפני c. שום כלל לא מזכיר את e, ולכן הסדר הקטן ביותר מתחיל בו: e, t, a, c, n.
- קלט
- words = ["bat", "tab", "tub", "bus"]
- פלט
- "invalid"
- הסבר
batלפניtabמציב את b לפני t, ו-tubלפניbusמציב את t לפני b. שני הדברים לא יכולים להתקיים, ולכן אין סדר שמתאים.
- קלט
- words = ["cooking", "cook"]
- פלט
- "invalid"
- הסבר
cookהוא תחילתה שלcooking, ולכן הוא מופיע ראשון בכל סדר אלפביתי. הרשימה מציבה אותו במקום השני, ואין שום סדר של האותיות שיכול להסביר זאת.
שאלת המשך
איך היית קובע אם סדר ההתאמה הוא היחיד?
רמזים
פתחו אותם אחד אחד. כל רמז חושף קצת יותר.
התבוננו בשתי מילים סמוכות, כמו
teaו־ten. מה הן מספרות לכם על האלפבית, ומה הן משאירות פתוח?זוג מילים סמוכות נותן לכל היותר כלל אחד: במיקום הראשון שבו המילים שונות, האות במילה הראשונה מופיעה לפני האות במילה השנייה. הכללים הם קשתות בגרף של האותיות, והתשובה היא סדר שמכבד כל קשת. שימו לב לזוג שאין בו מיקום שונה, ובו המילה הראשונה ארוכה יותר.
השתמשו באלגוריתם של קאהן: הציבו אות שאין כלל שמצביע עליה, הסירו את הכללים שלה וחזרו על הפעולה. שמרו את האותיות המוכנות בערימת מינימום והציבו תמיד את הקטנה ביותר. אם יש אותיות שאינן מוצבות לעולם, הכללים מכילים מחזור.
פתרון
כל זוג של מילים סמוכות נותן לכל היותר כלל אחד, במיקום הראשון שבו הן שונות: האות x לפני האות y. סדר מתאים הוא סדר טופולוגי של הכללים האלה. מעגל, או מילה שמופיעה לפני התחילית של עצמה, הופכים את הרשימה לבלתי אפשרית. ערימת מינימום שתמיד מציבה את האות הקטנה ביותר מבין האותיות המוכנות נותנת את הסדר הקטן ביותר.
נסו כל סדר אפשרי של האותיות
נכונה, אבל לא מסתיימת בבדיקות הגדולות ביותר
האינטואיציה
בדוק סידור אחד של k האותיות השונות על ידי בדיקת כל זוג אותיות שכנות. במיקום הראשון שבו הן שונות, האות במילה הראשונה חייבת להופיע מוקדם יותר; אם אין הבדל, אסור שהמילה הראשונה תהיה ארוכה יותר. עבור על הסידורים בסדר עולה, החל מהסידור האלפביתי, באמצעות הפרמוטציה הבאה, והסידור הראשון שעובר את הבדיקה הוא הסדר הקטן ביותר שמתאים.
ל-k אותיות יש k! סידורים: עבור 10 מתקבלים 3,628,800 ועבור 26 מתקבלים בערך 4 × 10^26, וכל בדיקה קוראת עד 5 × 10^4 תווים. כשאין סידור מתאים, החיפוש מנסה את כולם.
אלגוריתם
- אסוף את האותיות השונות ומיין אותן לפי סדר האלפבית.
- תעד את הדירוג של כל אות בסידור הנוכחי.
- בדוק כל זוג סמוך: במיקום הראשון שבו יש הבדל, האות במילה הראשונה צריכה להיות בעלת דירוג נמוך יותר; אם אין הבדל, אסור שהמילה הראשונה תהיה ארוכה יותר.
- אם כל הזוגות עומדים בתנאי, החזר את הסידור; אחרת עבור לסידור הבא בגודלו.
- כשאין סידור הבא, החזר
"invalid".
from itertools import permutations
def fits(words, rank):
# The list is sorted under rank when every neighbouring pair is in order.
for first, second in zip(words, words[1:]):
for x, y in zip(first, second):
if x != y:
if rank[x] > rank[y]:
return False
break
else:
# One word starts the other: the shorter must come first.
if len(first) > len(second):
return False
return True
def alienOrder(words):
letters = sorted(set("".join(words)))
# permutations() of a sorted list yields the orders from smallest to largest,
# so the first order that fits is the answer.
for order in permutations(letters):
rank = {ch: i for i, ch in enumerate(order)}
if fits(words, rank):
return "".join(order)
return "invalid"האלגוריתם של קאן עם ערימת מינימום
האינטואיציה
בזוג שכנים, רק ההבדל הראשון נחשב: act לפני cat מציין ש-a באה לפני c, והאותיות שאחריה לעולם אינן מושוות. לכן כל זוג נותן לכל היותר קשת אחת.
האלגוריתם של קאהן הופך את הקשתות לסדר: סופרים את הקשתות הנכנסות של כל אות, ממקמים אות שמספר הקשתות שלה הוא 0, מסירים את הקשתות היוצאות שלה וחוזרים על כך. אות שנמצאת במעגל תמיד תשמור על קשת מהאות שלפניה במעגל, ולכן היא לעולם לא תמוקם. אם ממוקמות פחות אותיות מכפי שמופיעות, התשובה היא "invalid".
שומרים את האותיות המוכנות בערימת מינימום וממקמים את הקטנה ביותר. כל סדר מתאים מתחיל באות מוכנה, ולכן זו האות הראשונה הקטנה ביותר האפשרית, ומיקומה מסיר רק קשתות, כך שהוא לא חוסם אף אות אחרת. בדוגמה הראשונה e ו-t מוכנות בהתחלה, ו-e מופיעה ראשונה. תור רגיל נותן סדר תקין, אך לא תמיד את הקטן ביותר.
אלגוריתם
- סמנו כל אות שמופיעה במילים.
- עבור כל זוג מילים שכנות, מצאו את המיקום הראשון שבו הן שונות והוסיפו פעם אחת את הקשת בין שתי האותיות שלהן. אם אין הבדל והמילה הראשונה ארוכה יותר, החזירו
"invalid". - ספרו את הקשתות הנכנסות ודחפו לערימת מינימום כל אות מסומנת שמספרן 0.
- הוציאו את האות הקטנה ביותר והוסיפו אותה. הפחיתו את המספר של כל אות שאליה היא מצביעה, ודחפו כל אות שמספרה מגיע ל-0.
- החזירו את האותיות שסודרו אם כל האותיות המסומנות סודרו, אחרת החזירו
"invalid".
import heapq
def alienOrder(words):
# Letters are numbered 0 for 'a' up to 25 for 'z'.
present = [False] * 26
for word in words:
for ch in word:
present[ord(ch) - 97] = True
# before[a][b] is True once you know letter a comes before letter b.
before = [[False] * 26 for _ in range(26)]
indegree = [0] * 26
for first, second in zip(words, words[1:]):
for x, y in zip(first, second):
if x != y:
# Only the first difference tells you anything.
a, b = ord(x) - 97, ord(y) - 97
if not before[a][b]:
before[a][b] = True
indegree[b] += 1
break
else:
# No difference: one word starts the other, so the shorter must come first.
if len(first) > len(second):
return "invalid"
# A min-heap of the letters with nothing left before them.
heap = [c for c in range(26) if present[c] and indegree[c] == 0]
heapq.heapify(heap)
order = []
while heap:
c = heapq.heappop(heap)
order.append(chr(c + 97))
for nxt in range(26):
if before[c][nxt]:
indegree[nxt] -= 1
if indegree[nxt] == 0:
heapq.heappush(heap, nxt)
# A letter on a cycle never gets down to indegree 0, so it is never placed.
if len(order) < sum(present):
return "invalid"
return "".join(order)
מלכודות ומקרי קצה
- לקיחת יותר מכלל אחד מתוך זוג. קריאת המיקום השני של
actושלcatמוסיפה את c לפני a, והדוגמה הראשונה הופכת למעגל שגוי. - החמצת מלכודת הקידומת. ב-
cookingלפניcookאין אות שונה, ולכן לולאה שמטפלת רק בהבדלים מחזירה סדר. - השמטת אותיות שאינן מופיעות באף כלל. בדוגמה הראשונה, אף כלל לא מזכיר את e, ובכל זאת היא צריכה להיכלל בתשובה.
- ספירת כלל חוזר פעמיים בדרגת הכניסה, אך אחסונו פעם אחת בלבד. האות לעולם לא מגיעה ל-0, ורשימה תקינה מדווחת כמעגל.
- התייחסות לשתי מילים שכנות זהות כאל מלכודת הקידומת; מילה שחוזרת על עצמה נמצאת בסדר.
שאלות נפוצות3
מהי סיבוכיות הזמן של מילון חייזרים?
O(C + k²), כאשר C הוא מספר התווים הכולל ו-k ≤ 26 הוא מספר האותיות השונות. מעבר אחד על הרשימה מוצא כל הבדל ראשון, והאלגוריתם של Kahn עובר על לכל היותר k² קשתות. טבלת הקשתות תופסת מקום O(k²).
למה להשוות רק בין מילים סמוכות?
סדר הוא תכונה טרנזיטיבית: אם כל מילה קטנה או שווה לזו שאחריה, הרשימה כולה ממוינת. לכן כל כלל שאפשר להסיק משתי מילים רחוקות כבר נובע מהזוגות הסמוכים שביניהן.
למה מילה שמופיעה לפני הקידומת שלה אינה תקינה?
בכל אלפבית, מילה מופיעה אחרי הקידומת שלה, כי ההשוואה מסתיימת כשנגמרות האותיות במילה הקצרה יותר, לפני שנמצא הבדל. זו הדרך היחידה שבה רשימה יכולה להיות בלתי אפשרית בלי שום מעגל בין הכללים שלה.
בעיות דומות
בעיות שמשתמשות באותם רעיונות. פתרון של שתיים או שלוש מהן מקבע את הדפוס.
def alienOrder(words):
# כתבו כאן קודבדיקה #1
בדיקה #2
בדיקה #3
ארגומנט
words = ["tea", "ten", "ate", "act", "cat"]
הפלט הצפוי
"etacn"