Lowest Common Ancestor of a Binary Tree
ניתן לך עץ בינארי במערך root, בסדר ערימה: השורש באינדקס 0, הילדים של האינדקס i באינדקסים 2*i+1 (שמאל) ו־2*i+2 (ימין), והערך -1 מציין מקום ריק. הערכים שונים זה מזה ואינם מסודרים לפי סדר כלשהו.
החזר את הערך של האב הקדמון המשותף הנמוך ביותר של p ושל q, שהם הערכים של שני צמתים שונים: הצומת העמוק ביותר שבתת־העץ שלו נמצאים שניהם. תת־עץ כולל את הצומת העליון שלו, לכן אם q נמצא מתחת ל־p, התשובה היא p.
פונקציה
- rootinteger-array
- עץ בינארי לפי סדר הרמות, כאשר -1 מציין מקום ריק
- pinteger
- הערך של צומת אחד
- qinteger
- הערך של צומת אחר
- מחזירהinteger
- הערך של הצומת הנמוך ביותר שתת-העץ שלו מכיל גם את p וגם את q
אילוצים
2 ≤ root.length ≤ 32767- כל
root[i]הוא-1או ערך שמקיים0 ≤ root[i] ≤ 104. root[0]לעולם אינו-1, ולעץ יש לפחות שני צמתים.- המערך עשוי להסתיים בערכי
-1נוספים אחרי הצומת האחרון. - גם שני הילדים של מקום ריק ריקים, והעומק הוא לכל היותר
14. - כל הערכים בעץ שונים זה מזה, והם אינם מסודרים לפי שום סדר.
pו-qהם הערכים של שני צמתים שונים בעץ, בכל סדר שהוא.
דוגמאות
- קלט
- root = [8, 3, 10, 6, 4, 12, 1, -1, -1, 7, 9]p = 6q = 9
- פלט
- 3
- הסבר
6הוא הילד השמאלי של3, ו-9נמצא מתחת ל-4מימין ל-3, לכן הם נפגשים ב-3. השוואת הערכים הייתה מובילה לשורש8:6קטן יותר ו-9גדול יותר.
- קלט
- root = [8, 3, 10, 6, 4, 12, 1, -1, -1, 7, 9]p = 4q = 7
- פלט
- 4
- הסבר
7הוא הילד השמאלי של4, ותת-העץ של4כולל את4עצמו, לכן התשובה היא4.
- קלט
- root = [1, -1, 6, -1, -1, 3, 0, -1, -1, -1, -1, -1, -1, -1, -1]p = 3q = 0
- פלט
- 6
- הסבר
- לשורש
1אין ילד שמאלי. לילד הימני שלו,6, יש3משמאלו ו-0מימינו, ולכן הערכים נפגשים ב-6.
שאלת המשך
אם p או q עשויים להיעדר, והתשובה היא -1, איזה קיצור דרך במעבר יחיד אינו עובד, ואיך בכל זאת מסיימים בסריקה אחת?
רמזים
פתחו אותם אחד אחד. כל רמז חושף קצת יותר.
נסו את כלל עץ החיפוש על הדוגמה הראשונה:
6קטן מהשורש8ו-9גדול ממנו. היכן הם באמת נמצאים? מה זה אומר על השוואת ערכים בעץ הזה?תן לכל תת־עץ לספר להורה שלו מה הוא מצא: כלום, אחד משני הערכים או התשובה. צומת ששומע על ערך משני ילדיו הוא המקום שבו
pו־qנפגשים.כתבו פונקציה רקורסיבית
find(i)שמחזירה-1עבור מקום ריק, אתiכאשרroot[i]הואpאוq, אתiכאשר שני הילדים מחזירים אינדקס, ובכל מקרה אחר את תוצאת הילד שאינה-1. התשובה היאroot[find(0)].
פתרון
אין סדר שמציין לך באיזה צד נמצא ערך, ולכן עליך לבדוק. מעבר אחד בסדר סריקת צאצאים מאפשר לכל תת־עץ לדווח על מה שמצא, והערכים נפגשים בצומת הראשון שמקבל מידע משני הצדדים.
רד במורד הדרך, חפש בכל צד
האינטואיציה
רד מהשורש, ובכל צומת חפש בתת־העץ השמאלי את p ואת q. אם שניהם שם: פנה שמאלה. אם אף אחד מהם לא שם: פנה ימינה. אם אחד נמצא בכל צד: הערכים מתפצלים, והצומת הזה הוא התשובה. גם צומת שמכיל את p או את q הוא התשובה, כי הערך האחר חייב להימצא מתחתיו. עבור 6 ו־9, השורש מוצא את שניהם בצד שמאל, וב־3 נמצא רק 6, שמכיל את p.
כל צעד כלפי מטה מחפש בתת־עץ שלם, ולכן h רמות עולות O(n·h), ועולות O(n²) בעץ גבוה וצר ללא מגבלת עומק. החיפוש ב־8 כבר ביקר בכל הצמתים שמתחת ל־3, והצעד ב־3 מבקר שוב בחלקם.
אלגוריתם
- כתבו
contains(i, value): החזירוfalseעבור מקום ריק (מעבר לסוף או-1),trueאםroot[i]שווה ל־value, אחרת בדקו את שני הילדים. - התחילו ב־
i = 0. אםroot[i]הואpאוq, החזירו אותו. - אם תת־העץ השמאלי ב־
2*i+1מכיל את שני הערכים, עברו אליו. אם הוא אינו מכיל אף אחד מהם, עברו אל2*i+2. - אחרת, החזירו את
root[i].
def lowestCommonAncestor(root, p, q):
n = len(root)
def contains(i, value):
# Is value somewhere in the subtree at index i?
if i >= n or root[i] == -1:
return False
if root[i] == value:
return True
return contains(2 * i + 1, value) or contains(2 * i + 2, value)
i = 0 # p and q are always inside the subtree at i
while True:
if root[i] == p or root[i] == q:
return root[i] # one value is here, the other is below it
left = 2 * i + 1
p_left = contains(left, p)
q_left = contains(left, q)
if p_left and q_left:
i = left # both on the left
elif not p_left and not q_left:
i = 2 * i + 2 # both on the right
else:
return root[i] # one on each sideמעבר אחד בסדר post-order
האינטואיציה
תן לכל תת-עץ להשיב פעם אחת, מלמטה למעלה. find(i) מחזירה את האינדקס של התשובה אם תת-העץ שב-i מכיל את שני הערכים, את האינדקס של p או q אם הוא מכיל אחד מהם, ואת -1 אם הוא אינו מכיל אף אחד מהם.
צומת שמכיל את p או את q מחזיר את האינדקס של עצמו בלי לבדוק מתחתיו. אם הערך השני נמצא בתת-העץ שלו, הצומת הזה הוא התשובה; אם לא, צומת גבוה יותר מקבל תשובה משני הצדדים. כל צומת אחר שואל את שני ילדיו. שתי תוצאות פירושן שהערכים הגיעו מצדדים שונים ונפגשים כאן. תוצאה יחידה מועברת למעלה ללא שינוי, בין אם מדובר בערך אחד ובין אם זו התשובה הסופית.
בדוגמה הראשונה, 3 מקבל את האינדקס 3 מצד שמאל ואת 10 מצד ימין, מחזיר את האינדקס שלו, 1, והשורש מעביר אותו למעלה. מבקרים בכל צומת פעם אחת: זמן O(n), רקורסיה O(h).
אלגוריתם
find(i)מחזירה-1עבור אינדקס שמעבר לסוף או-1.- צומת שהערך שלו הוא
pאוqמחזירהiבלי לבדוק מתחתיו. - כל צומת אחר מחשבת
left = find(2*i+1)ו-right = find(2*i+2). אם אף אחד מהם אינו-1, היא מחזירהi; אחרת, את זה שאינו-1, או-1. - מחזירים את
root[find(0)].
def lowestCommonAncestor(root, p, q):
n = len(root)
def find(i):
# In the subtree at index i: the index of the answer if both values are
# inside, the index of the one that is, or -1 when neither is.
if i >= n or root[i] == -1:
return -1
if root[i] == p or root[i] == q:
return i
left = find(2 * i + 1)
right = find(2 * i + 2)
if left != -1 and right != -1:
return i # one value on each side: this node is the answer
return left if left != -1 else right
return root[find(0)]
מלכודות ומקרי קצה
- השוואת ערכים לצומת. לעץ אין סדר: בדוגמה הראשונה ההליכה נעצרת בשורש
8במקום ב-3. - החזרת
trueאוfalseבמקום אינדקס. ההורה לא יכול לדעת אם התשובה הסופית כבר נמצאת מתחתיו. - קריאת ילד בלי לבדוק את האורך. ל-
9באינדקס10יש אינדקסים21ו-22בתור ילדים, מעבר לרשומה האחרונה. - החזרת
find(0), אינדקס, במקוםroot[find(0)]. - ב-Lua וב-R, המערכים מתחילים ב-1: השאר את אינדקסי הצמתים מבוססי 0 וקרא את
root[i + 1].
שאלות נפוצות3
מהי סיבוכיות הזמן של מציאת האב הקדמון המשותף הנמוך ביותר בעץ בינארי?
מעבר הסריקה בסדר שלאחר הסדר נכנס לכל צומת לכל היותר פעם אחת: זמן O(n). מחסנית הרקורסיה מחזיקה מסגרת אחת לכל רמה, ולכן המקום הוא O(log n) בעץ מאוזן ו-O(n) בעץ שצורתו דומה לנתיב.
במה זה שונה מהאב הקדמון המשותף הנמוך ביותר בעץ חיפוש בינארי?
בעץ חיפוש, השוואה בין p ל־q עם צומת מגלה לך באיזה צד נמצא כל אחד מהם, ולכן מסלול אחד מהשורש מוצא את התשובה בזמן O(h). כאן השוואה לא אומרת דבר, ולכן מחפשים בתתי־העצים.
האם תוכל למצוא את האב הקדמון המשותף הנמוך ביותר באמצעות מצביעי הורה?
כן. במערך הזה, ההורה של האינדקס i הוא (i-1)/2 בעיגול כלפי מטה. העלה את האינדקס הגדול מבין השניים להורה שלו עד שהם יהיו שווים; אינדקס גדול יותר לעולם אינו אב קדמון של אינדקס קטן יותר. עם צמתים מקושרים, הכנס את אבות הקדמונים של p לקבוצה וטפס מ-q אל הראשון שבה.
בעיות דומות
בעיות שמשתמשות באותם רעיונות. פתרון של שתיים או שלוש מהן מקבע את הדפוס.
def lowestCommonAncestor(root, p, q):
# כתבו את הקוד כאןבדיקה #1
בדיקה #2
בדיקה #3
ארגומנטים
root = [8, 3, 10, 6, 4, 12, 1, -1, -1, 7, 9] p = 6 q = 9
הפלט הצפוי
3