Path Sum
נתון לך עץ בינארי המאוחסן במערך tree בסדר רמות, ומספר targetSum. השורש נמצא באינדקס 0, הילדים של הצומת באינדקס i נמצאים באינדקסים 2*i+1 (שמאלי) ו-2*i+2 (ימני), -1 מסמן מקום ריק, והמערך עשוי להסתיים בערכי -1 נוספים. החזר true אם יש מסלול כלשהו מהשורש כלפי מטה אל עלה שסכום הערכים בו הוא targetSum, ואחרת החזר false. עלה הוא צומת ששני מקומות הילדים שלו ריקים.
פונקציה
- treeinteger-array
- העץ הבינארי לפי סדר רמות, כאשר -1 מציין מקום ריק
- targetSuminteger
- הסכום שאליו מסלול משורש לעלה חייב להגיע
- מחזירהboolean
- true אם סכום הערכים במסלול כלשהו מהשורש לעלה שווה ל־targetSum, אחרת false
אילוצים
1 ≤ tree.length ≤ 32767- כל
tree[i]הוא-1או ערך שמקיים0 ≤ tree[i] ≤ 1000. tree[0]אף פעם לא שווה ל--1, ולכן יש לעץ לפחות צומת אחד.- ייתכן שבסוף המערך יהיו ערכי
-1נוספים אחרי הצומת האחרון. - גם שני הילדים של מקום ריק ריקים, והעומק הוא לכל היותר
14. 0 ≤ targetSum ≤ 15000
דוגמאות
- קלט
- tree = [3, 9, 6, -1, 2, 1, 7]targetSum = 14
- פלט
- true
- הסבר
- המסלול
3,9,2(אינדקסים0,1,4) מסתכם ב-14, וה-2באינדקס4הוא עלה.
- קלט
- tree = [3, 9, 6, -1, 2, 1, 7]targetSum = 12
- פלט
- false
- הסבר
3 + 9 = 12, אבל ל־9יש צאצא, ולכן אף מסלול לא מסתיים שם. סכומי שלושת המסלולים מהשורש לעלה הם14,10ו־16, ואף אחד מהם אינו12.
- קלט
- tree = [4, -1, -1]targetSum = 4
- פלט
- true
- הסבר
- שני המקומות של הצאצאים של השורש ריקים, ולכן השורש הוא עלה בפני עצמו. המסלול שמכיל רק את
4מסתכם ב־4.
שאלת המשך
האם תוכל לספור את המסלולים שסכומם targetSum, אם מסלול יכול להתחיל בכל צומת ולהסתיים בכל צומת שנמצא מתחתיו?
רמזים
פתחו אותם אחד אחד. כל רמז חושף קצת יותר.
רד מהשורש ושמור על סכום מצטבר. היכן מותר לך להשוות את הסכום הזה ל־
targetSum?רק בעלה, צומת ששני מקומות הילדים שלו ריקים. צומת עם ילד אחד אינו מסיים נתיב, גם אם הסכום הכולל כבר מתאים. העבר את סכום הנתיב עד כה אל כל אחד מהילדים.
שמור מחסנית של זוגות: אינדקס של צומת והסכום מהשורש עד לאותו צומת. הוצא זוג מהמחסנית; אם הצומת הוא עלה והסכום שווה ל־
targetSum, החזרtrue. אחרת, הוסף למחסנית כל ילד קיים יחד עם הסכום בתוספת הערך של הילד.
פתרון
רק מסלולים שלמים נחשבים, מהשורש ועד לעלה. סכום מצטבר יכול להגיע ל־targetSum בצומת שעדיין יש לו צאצאים, וזה לא נחשב. לכן העבירו את סכום המסלול לכל צומת, כפרמטר או במחסנית, והשוו אותו ליעד רק בעלים.
רקורסיה על הסכום שנותר
האינטואיציה
מסלול שסכומו targetSum מתחיל בערך של השורש, ולכן שאר המסלול, שמתחיל באחד מילדי השורש, חייב להסתכם ב-targetSum פחות הערך הזה. זו אותה שאלה על עץ קטן יותר. מחסרים את הערך של כל צומת בדרך למטה; בעלה המסלול מסתיים, ולכן התשובה היא האם לא נשאר כלום.
בדוגמה הראשונה השורש משאיר 14 - 3 = 11, ה-9 משאיר 2 והעלה 2 משאיר 0. בשנייה, ה-9 כבר משאיר 0, אבל יש לו ילד, ולכן החיפוש נמשך. מבקרים בכל צומת פעם אחת, בזמן O(n), ומחסנית הקריאות מחזיקה מסגרת אחת לכל רמה, לכל היותר 15.
אלגוריתם
- כתבו
walk(i, remaining)והחסירו אתtree[i]מ־remaining. - אם שני מקומות הילדים של
iריקים (האינדקס מעבר לסוף או-1), החזירו האםremainingהוא0. - אחרת החזירו
trueאם הפעלה שלwalkעל ילד שמאלי קיים או על ילד ימני קיים מחזירהtrue. - החזירו
walk(0, targetSum).
def hasPathSum(tree, targetSum):
n = len(tree)
def walk(i, remaining):
# remaining is what the path still needs once it reaches node i.
remaining -= tree[i]
left, right = 2 * i + 1, 2 * i + 2
has_left = left < n and tree[left] != -1
has_right = right < n and tree[right] != -1
if not has_left and not has_right:
return remaining == 0 # a leaf: the path ends here
return (has_left and walk(left, remaining)) or (has_right and walk(right, remaining))
return walk(0, targetSum)חיפוש לעומק תחילה באמצעות מחסנית מפורשת
האינטואיציה
הרקורסיה שומרת מספר אחד בכל קריאה. שמור אותו בעצמך במקום זאת, במחסנית לצד כל צומת: סכום המסלול מהשורש ועד לאותו צומת. התחל עם (0, tree[0]); כל ילד מקבל את סכום ההורה שלו בתוספת הערך שלו. שלוף זוג. אם הצומת הוא עלה שסכומו targetSum, סיימת; אחרת דחוף למחסנית את ילדיו האמיתיים.
כל צומת אמיתי נדחף פעם אחת, זמן O(n), והמחסנית מכילה בערך אח אחד שממתין בכל רמה, O(h). אותה לולאה עובדת גם על עץ עמוק המבוסס על מצביעים, שבו הרקורסיה עלולה למצות את המחסנית.
אלגוריתם
- דחוף
(0, tree[0])למחסנית. - הוצא זוג
(i, total)והסתכל על מקומות הילדים2*i+1ו-2*i+2. - אם אף אחד מהילדים אינו אמיתי ו-
totalשווה ל-targetSum, החזרtrue. - דחוף כל ילד אמיתי
cבתור(c, total + tree[c]). - כשהמחסנית ריקה, החזר
false.
def hasPathSum(tree, targetSum):
n = len(tree)
stack = [(0, tree[0])] # (node index, sum of the path from the root to it)
while stack:
i, total = stack.pop()
left, right = 2 * i + 1, 2 * i + 2
has_left = left < n and tree[left] != -1
has_right = right < n and tree[right] != -1
if not has_left and not has_right and total == targetSum:
return True # a leaf whose path adds up
if has_left:
stack.append((left, total + tree[left]))
if has_right:
stack.append((right, total + tree[right]))
return False
מלכודות ומקרי קצה
- השוואת הסכום בכל צומת. בדוגמה השנייה,
3 + 9 = 12תואם בצומת9, שיש לו ילד, ולכן התשובה היאfalse. - מתן אפשרות למקום ריק של ילד לסיים מסלול. כך
9בדוגמה השנייה מסיים מסלול דרך המקום הריק של הילד השמאלי שלו. - שכחת השורש לבדו. צומת יחיד הוא עלה, ולכן
[4]עם יעד4הואtrue, וכך גם[0]עם יעד0. - קריאה מעבר לסוף המערך. לעלה סמוך לסוף יכולים להיות אינדקסים של ילדים שמעבר לאיבר האחרון.
- בלבול לגבי ההיסט ב-Lua וב-R. יש להשאיר את אינדקסי הצמתים מבוססי 0 עבור
2*i+1ולקרוא אתtree[i + 1].
שאלות נפוצות3
מהי סיבוכיות הזמן של Path Sum?
O(n): מבקרים בכל צומת לכל היותר פעם אחת, ואפשר לעצור את החיפוש בעלה הראשון שמתאים. המקום הנוסף הוא O(h) עבור הנתיב שנבדק, כמסגרות קריאה או כפריטים במחסנית משלך.
האם אפשר לפתור את Path Sum באמצעות BFS?
כן. הכנס זוגות של צומת וסכום המסלול שלו לתור במקום למחסנית, ובדוק כל עלה כשהוא יוצא. זמן הריצה עדיין O(n), אבל התור יכול להכיל רמה שלמה, בערך מחצית מהצמתים בעץ מלא, בעוד שמחסנית מכילה בערך צומת אחד לכל רמה.
איך מוצאים את כל המסלולים שסכומם שווה ליעד?
שמרו את רשימת הצמתים במסלול הנוכחי בזמן הירידה, העתיקו אותה לתשובה בכל עלה שסכומו תואם, והסירו את הצומת האחרון כשחוזרים למעלה. הסריקה נשארת זהה. העתקת המסלולים עלולה לעלות יותר מהסריקה כשיש עלים רבים שתואמים.
בעיות דומות
בעיות שמשתמשות באותם רעיונות. פתרון של שתיים או שלוש מהן מקבע את הדפוס.
def hasPathSum(tree, targetSum):
# כתבו כאן את הקודבדיקה #1
בדיקה #2
בדיקה #3
ארגומנטים
tree = [3, 9, 6, -1, 2, 1, 7] targetSum = 14
הפלט הצפוי
true