Longest Subarray with Sum K
בהינתן המערך השלם nums וערך היעד k, החזר את אורכו של תת-המערך הארוך ביותר שסכום איבריו הוא בדיוק k. תת-מערך הוא רצף של איברים עוקבים, שאורכו לפחות איבר אחד. חלק מהערכים עשויים להיות אפס או קטנים מאפס. אם אין תת-מערך שסכום איבריו הוא k, החזר 0.
פונקציה
- numsinteger-array
- המספרים השלמים לחיפוש, כולל מספרים שליליים ואפסים
- kinteger
- הסכום המדויק שצריך להיות למערך המשנה
- מחזירהinteger
- אורכו של תת־המערך הארוך ביותר שסכומו k, או 0 אם אין כזה
אילוצים
1 ≤ nums.length ≤ 2 × 104-104 ≤ nums[i] ≤ 104-109 ≤ k ≤ 109- כל סכומי הקידומות נשארים בין
-2 × 108ל־2 × 108, ולכן מספרים שלמים בני 32 סיביות מספיקים. - החזר
0כאשר אין תת־מערך שסכומו שווה ל־k.
דוגמאות
- קלט
- nums = [2, -1, 3, 1, -4, 2, 3]k = 5
- פלט
- 5
- הסבר
- שלושה רצפים מסתכמים ב־5:
[2, 3]בסוף,[2, -1, 3, 1]בהתחלה, ו־[3, 1, -4, 2, 3]באינדקסים 2 עד 6. האחרון הוא הארוך ביותר, עם 5 איברים.
- קלט
- nums = [4, 1, 1, 2, -1, 1]k = 3
- פלט
- 4
- הסבר
[1, 1, 2, -1]באינדקסים 1 עד 4 וגם[1, 2, -1, 1]באינדקסים 2 עד 5 מסתכמים שניהם ב־3 עם 4 איברים. מחזירים אורך, ולכן אין חשיבות לשוויון.
- קלט
- nums = [-3, 6, -3]k = 1
- פלט
- 0
- הסבר
- סכומי ששת תת-המערכים הם -3, 6, -3, 3, 3 ו-0. אף אחד מהם אינו 1, ולכן התשובה היא 0.
שאלת המשך
איך תמצא את תת־המערך הארוך ביותר שסכומו לכל היותר k, כאשר עדיין מותר לכלול ערכים שליליים?
רמזים
פתחו אותם אחד אחד. כל רמז חושף קצת יותר.
עם 20,000 מספרים, שיטת הכוח הגס מסתכמת בכ־2 × 10^8 זוגות של התחלה וסוף. ערכים שליליים גם שוברים חלון הזזה: רצף שסכומו גדול מ־
kיכול לרדת בחזרה אליו. איזה סכום מצטבר יאפשר לך לחשב את הסכום של כל תת־מערך באמצעות חיסור אחד?בחישוב סכומי קידומות רצים, סכום האיברים שאחרי המיקום
iועד למיקוםjהואprefix[j] - prefix[i]. תת־מערך שמסתיים ב־jיהיה בעל סכוםkכאשר סכום קידומת מוקדם יותר שווה ל־prefix[j] - k. אם כמה סכומי קידומת מוקדמים מתאימים, איזה מהם ייתן את תת־המערך הארוך ביותר?תעדו, עבור כל סכום מצטבר, את האינדקס הראשון שבו הוא הופיע, והתחילו בסכום 0 באינדקס -1. בכל אינדקס, חפשו את
prefix - kוהשוו את המרחק לאורך הטוב ביותר שמצאתם. הוסיפו את הסכום המצטבר הנוכחי למפה רק אם הוא עדיין לא נמצא בה.
פתרון
סכומים מצטברים הופכים את סכום האיברים בכל תת־מערך לחיסור אחד: האיברים שאחרי האינדקס s ועד האינדקס i מסתכמים ב־prefix[i] - prefix[s]. זכור את האינדקס הראשון שבו מופיע כל סכום, ובמעבר אחד אפשר למצוא בכל מיקום את ההתאמה הארוכה ביותר שמסתיימת בו.
הרחיבו כל תת־מערך מכל נקודת התחלה
נכונה, אבל לא מסתיימת בבדיקות הגדולות ביותר
האינטואיציה
מכל נקודת התחלה, מתקדמים ימינה ומוסיפים כל איבר לסכום מצטבר. בכל פעם שהסכום שווה ל־k, מתעדים את האורך. כל תת־מערך הוא זוג אחד של נקודת התחלה ונקודת סיום, ולכן אף אחד מהם לא מתפספס. ממשיכים להתקדם גם אחרי שהסכום עובר את k: מאינדקס 2 בדוגמה הראשונה, הסכומים הם 3, 4, 0, 2, 5, וה־-4 הוא הסיבה לכך שהאחרון מגיע ל־5.
יש בערך n²/2 זוגות: 2 × 10^8 חיבורים עבור n = 2 × 10^4.
אלגוריתם
- הגדירי את
bestל־0. - עבור כל
start, הגדירי אתtotalל־0. - העבירי את
endמ־startועד לאינדקס האחרון, והוסיפי אתnums[end]ל־total. - כאשר
totalשווה ל־k, הגדירי אתbestלערך הגדול יותר מביןbestו־end - start + 1. - החזירי את
best.
def longestSubarrayWithSumK(nums, k):
n = len(nums)
best = 0
for start in range(n):
total = 0
for end in range(start, n):
total += nums[end]
# A match may come after the total has passed k, so never stop early
if total == k and end - start + 1 > best:
best = end - start + 1
return bestסכומים מצטברים עם האינדקס הראשון של כל סכום
האינטואיציה
רצף שמסתיים באינדקס i מסתכם ב-k בדיוק כאשר אחד הסכומים הקודמים שווה ל-prefix - k. אם כמה סכומים קודמים מתאימים, הראשון שבהם מתחיל את הרצף הארוך ביותר, ולכן המפה שומרת כל סכום רק בפעם הראשונה שהוא מופיע. עותק מאוחר יותר יכול להתחיל רק רצף קצר יותר. אתחל את המפה בסכום 0 באינדקס -1, כדי שהתאמה שמתחילה באינדקס 0 תקבל אורך i + 1.
בדוגמה הראשונה הסכומים הם 2, 1, 4, 5, 1, 3, 6. באינדקס 6, 6 - 5 = 1 הופיע לראשונה באינדקס 1, מה שנותן אורך 5. הסכום 1 מופיע שוב באינדקס 4, ושמירת האינדקס 1 היא שמאפשרת למצוא את הרצף הזה.
חיפוש אחד לכל איבר נותן זמן O(n), והמפה מכילה לכל היותר n + 1 סכומים.
אלגוריתם
- צרו מפה בשם
firstשמחזיקה את הסכום 0 באינדקס -1, והגדירו אתprefixואתbestל-0. - עבור כל אינדקס
i, הוסיפו אתnums[i]ל-prefix. - אם
prefix - kנמצא במפה באינדקסstart, הגדירו אתbestלערך הגדול יותר מביןbestו-i - start. - אם
prefixעדיין לא נמצא במפה, שמרו אותו בה עם האינדקסi. - החזירו את
best.
def longestSubarrayWithSumK(nums, k):
# first[p] is the index where the prefix sum p first appears; the empty prefix sits at -1
first = {0: -1}
prefix = 0
best = 0
for i, num in enumerate(nums):
prefix += num
# The earliest prefix equal to prefix - k starts the longest match that ends at i
start = first.get(prefix - k)
if start is not None and i - start > best:
best = i - start
# Keep the first index only: a later one could only start a shorter subarray
if prefix not in first:
first[prefix] = i
return best
מלכודות ומקרי קצה
- החלפת האינדקס השמור בכל פעם שסכום חוזר שומרת את נקודת ההתחלה האחרונה ומוצאת התאמה קצרה יותר. הדוגמה הראשונה מחזירה 4 במקום 5, כי הסכום 1 עובר מאינדקס 1 לאינדקס 4.
- חלון שמתכווץ כל עוד הסכום שלו גדול מ־
kמחזיר גם הוא 4 בדוגמה הראשונה. בסופו הוא מכיל את[-1, 3, 1, -4, 2, 3]שסכומו 4, והוא לעולם אינו מסיר את -1. - השמטת הסכום 0 באינדקס -1 מחמיצה כל התאמה שמתחילה באינדקס 0:
[5, 0, 0, 0]עםk = 5מחזיר 0 במקום 4. - האינדקס השמור הוא המיקום שלפני תת־המערך, ולכן האורך הוא
i - start. הוספת 1 סופרת איבר אחד יותר מדי.
שאלות נפוצות3
מהי סיבוכיות הזמן של תת־המערך הארוך ביותר שסכומו K?
זמן O(n) ושטח נוסף של O(n) באמצעות סכומים מצטברים ומפה של האינדקסים הראשונים: חיפוש אחד ולכל היותר הוספה אחת לכל איבר. הרחבת כל תת־מערך מכל נקודת התחלה עולה זמן O(n²) ושטח O(1).
למה חלון הזזה נכשל כשהמערך מכיל מספרים שליליים?
חלון הזזה מניח שהוספת איבר מגדילה את הסכום והסרת איבר מקטינה אותו. כשיש ערכים שליליים, חלון שסכומו גדול מדי עדיין יכול להגיע ל־k כשמגיע איבר שלילי, וחלון שסכומו קטן מדי עשוי להזדקק להסרת איבר שלילי שמשמאלו. החלון לא יכול לדעת איזה מהלכים נכון לבצע.
האם אפשר לפתור את בעיית תת־המערך הארוך ביותר שסכומו K תוך שימוש ב־O(1) מקום נוסף?
רק כאשר אין ערכים שליליים. אז הסכום אף פעם לא יורד ככל שהחלון גדל, ושני מצביעים מוצאים את החלון הארוך ביותר שסכומו k בזמן O(n) ובמקום O(1). עם ערכים שליליים צריך להשתמש במפה של סכומים מצטברים, או בסריקה של O(n²).
בעיות דומות
בעיות שמשתמשות באותם רעיונות. פתרון של שתיים או שלוש מהן מקבע את הדפוס.
def longestSubarrayWithSumK(nums, k):
# כתבו כאן את הקודבדיקה #1
בדיקה #2
בדיקה #3
ארגומנטים
nums = [2, -1, 3, 1, -4, 2, 3] k = 5
הפלט הצפוי
5