Longest Repeating Character Replacement
ניתנת לך מחרוזת s של אותיות אנגליות גדולות ומספר שלם k. באפשרותך לבחור לכל היותר k מיקומים ב-s ולשנות את האות בכל אחד מהם לכל אות גדולה אחרת.
החזר את האורך של תת-המחרוזת הארוכה ביותר, כלומר רצף של אותיות סמוכות, שמכילה אות חוזרת אחת בלבד לאחר השינויים שלך.
פונקציה
- sstring
- המחרוזת של אותיות גדולות
- kinteger
- מספר האותיות המרבי שאפשר לשנות
- מחזירהinteger
- אורך תת-המחרוזת הארוכה ביותר של אות אחת שחוזרת על עצמה שאפשר ליצור
אילוצים
1 ≤ s.length ≤ 5 × 104sמכילה רק אותיות אנגליות גדולות.0 ≤ k ≤ s.length
דוגמאות
- קלט
- s = "BAAACAB"k = 1
- פלט
- 5
- הסבר
- שינוי
Cל־Aגורם לכך שהאינדקסים 1 עד 5 יקראוAAAAA. בשני החלונות בני שש האותיות ישBואתC, ולכן נדרשים שני שינויים.
- קלט
- s = "AABBBAB"k = 2
- פלט
- 6
- הסבר
- במיקומים 1 עד 6 מופיעה
ABBBAB, ושינוי שתי האותיותAנותןBBBBBB. במחרוזת כולה יש רק ארבע אותיותBמתוך שבע, ולכן נדרשים שלושה שינויים.
- קלט
- s = "WXYZ"k = 0
- פלט
- 1
- הסבר
- מכיוון שאסור לבצע שינויים, התשובה היא הרצף הארוך ביותר שכבר קיים במחרוזת. כל אות שונה מהאותיות הסמוכות לה, ולכן הרצף הזה הוא בן אות אחת.
שאלת המשך
מה משתנה אם s יכולה להכיל כל תו במקום את 26 האותיות הגדולות?
רמזים
פתחו אותם אחד אחד. כל רמז חושף קצת יותר.
עבור תת־מחרוזת קבועה אחת, לאיזו אות צריכה להפוך כל אחת מהאותיות האחרות, וכמה שינויים זה עולה?
תת־מחרוזת ניתנת להשגה כאשר אורכה פחות מספר המופעים של האות הנפוצה ביותר בה הוא לכל היותר
k. מצאו את החלון הארוך ביותר שעומד בכלל הזה באמצעות הזזת שני הקצוות קדימה לאורך המחרוזת.שמור 26 מונים ואת המונה הגבוה ביותר
top. הוסף אות אחת מימין; אם החלון דורש עכשיו יותר מ-kשינויים, הסר אות אחת משמאל כדי שהאורך יישאר זהה. אין צורך לכווץ את החלון אף פעם, והערך שלtopאף פעם לא צריך לרדת.
פתרון
משאירים את האות הנפוצה ביותר בתת־מחרוזת ומשנים את השאר: תת־מחרוזת באורך len שבה האות הנפוצה ביותר מופיעה top פעמים דורשת len - top שינויים. חלון הזזה מוצא במעבר אחד את תת־המחרוזת הארוכה ביותר שעבורה len - top ≤ k, כי אין צורך לכווץ את החלון, ו־top לעולם לא צריך לרדת.
בדקו כל תת־מחרוזת
נכונה, אבל לא מסתיימת בבדיקות הגדולות ביותר
האינטואיציה
עבור כל התחלה, הגדילו את הסוף באות אחת בכל פעם, עדכנו מונה לכל אות ואת top, ובדקו len - top ≤ k. כל מחרוזת משנה חדשה דורשת עדכון אחד במקום ספירה מחדש, וכל מחרוזת משנה נבדקת, כך שלא ניתן לפספס את הארוכה ביותר שאפשר להגיע אליה.
במחרוזת באורך n יש בערך n²/2 מחרוזות משנה: 1.25 × 10^9 בדיקות עבור n = 5 × 10^4.
אלגוריתם
- הגדירו את
bestבתור 0. - עבור כל אינדקס התחלה, אפסו את 26 הספירות ואת
topל־0. - התקדמו עם
endמאינדקס ההתחלה ועד לאינדקס האחרון. הוסיפו אתs[end]לספירה שלו והגדילו אתtopאם הספירה הזו היא כעת הגבוהה ביותר. - אם
end - start + 1 - top ≤ k, שמרו את האורך אם הוא גדול מ־best. - החזירו את
best.
def characterReplacement(s, k):
n = len(s)
best = 0
for start in range(n):
count = [0] * 26 # letters in s[start..end]
top = 0 # count of the most common letter there
for end in range(start, n):
c = ord(s[end]) - ord('A')
count[c] += 1
top = max(top, count[c])
# Every letter that is not the most common one must change.
if end - start + 1 - top <= k:
best = max(best, end - start + 1)
return bestחלון הזזה אחד לכל אות מטרה
האינטואיציה
בחרו תחילה את האות הסופית. אם בסופו של דבר כל הריצה היא A, צריך למצוא את תת־המחרוזת הארוכה ביותר שיש בה לכל היותר k אותיות שאינן A.
הזיזו את right לאורך המחרוזת וספרו את האותיות שאינן אות היעד בחלון. כשהספירה עוברת את k, הזיזו את left קדימה עד שהיא חוזרת ל־k. חלון שיקר מדי נשאר יקר מדי כשהוא גדל, לכן left לעולם אינו זז אחורה, ובכל right החלון הוא החלון התקין הארוך ביותר שמסתיים שם.
הריצו זאת עבור כל 26 האותיות ושמרו את התוצאה הטובה ביותר: 26 מעברים ליניאריים על עד 5 × 10^4 אותיות. הסיבוכיות נשארת O(n) רק משום שאלפבית האותיות קטן.
אלגוריתם
- עבור כל אות יעד מ־
AעדZ, התחילו חלון עםleft = 0ועםothers = 0. - הזיזו את
rightלאורך המחרוזת. אםs[right]אינה אות היעד, הוסיפו 1 ל־others. - כל עוד
others > k, הזיזו אתleftקדימה, והחסירו 1 אם האות שיוצאת אינה אות היעד. - שמרו את
right - left + 1אם הוא גדול מ־best. - לאחר בדיקת כל 26 האותיות, החזירו את
best.
def characterReplacement(s, k):
best = 0
for target in "ABCDEFGHIJKLMNOPQRSTUVWXYZ":
left = 0
others = 0 # letters in s[left..right] that are not target
for right in range(len(s)):
if s[right] != target:
others += 1
# Too many letters to change: drop letters from the left.
while others > k:
if s[left] != target:
others -= 1
left += 1
best = max(best, right - left + 1)
return bestחלון אחד שלעולם אינו מתכווץ
האינטואיציה
שמרו 26 ספירות עבור החלון ועבור top, הספירה הגבוהה ביותר. כשמוסיפים את s[right] והחלון דורש יותר מדי שינויים, הסירו אות אחת משמאל: החלון זז ושומר על אורכו, כי רק חלון ארוך יותר יכול לשבור את השיא. אחרת הוא גדל באחת. אורכו הוא תמיד הטוב ביותר עד כה, ולכן התשובה היא n - left.
כשאות יוצאת מהחלון, השאירו את top ללא שינוי, גם אם הוא כעת גבוה מהספירה האמיתית. אחרי הזזה, האורך הוא בדיוק top + k, ולכן כדי לגדול אות כלשהי צריכה להופיע top + 1 פעמים, ובאותו רגע top גדל בהתאם. ערך top מיושן יכול רק לגרום לחלון לזוז, והזזה לא יכולה להפסיד את השיא.
במחרוזת BAAACAB עם k = 1, המחרוזת BAAAC דורשת 2 שינויים, והחלון זז ל-AAAC. ה-A הבאה מעלה את top ל-4 והחלון גדל ל-AAACA, שאורכו 5.
אלגוריתם
- שמרו ספירות עבור 26 אותיות,
left = 0ו-top = 0. - הזיזו את
rightלאורך המחרוזת: הוסיפו אתs[right]לספירה שלו והגדילו אתtopאם הספירה הזו גבוהה יותר כעת. - אם
right - left + 1 - top > k, הסירו אתs[left]מהספירות והזיזו אתleftצעד אחד. לעולם אל תקטינו אתtop. - החזירו את אורך החלון הסופי,
n - left.
def characterReplacement(s, k):
count = [0] * 26 # letters inside the window s[left..right]
left = 0
top = 0 # the highest count any letter has reached in a window
for right in range(len(s)):
c = ord(s[right]) - ord('A')
count[c] += 1
top = max(top, count[c])
# Needs more than k changes: slide the window instead of growing it.
if right - left + 1 - top > k:
count[ord(s[left]) - ord('A')] -= 1
left += 1
# The window only grew when a longer valid substring was found.
return len(s) - left
מלכודות ומקרי קצה
- הוספת
kלרצף הארוך ביותר. ב־AAABעםk = 3מתקבל 6, יותר מאורך המחרוזת. ב־BAAACABעםk = 1מתקבל 4, אבל שינוי ה־Cמחבר שני רצפים לרצף באורך 5. - ספירת השינויים ביחס לאות הראשונה בחלון במקום לאות הנפוצה ביותר בו. ב־
BAAAנדרש שינוי אחד, לא שלושה. - החזרת
n - leftמלולאה שמצמצמת את החלון באמצעותwhileומחשבת מחדש את המקסימום בפועל. החלון הזה הולך ומתקצר, לכן יש לשמורbestנפרד. - חישוב אורך החלון בתור
right - left. שני הקצוות נמצאים בתוכו, לכן יש להוסיף אחד.
שאלות נפוצות3
מהי סיבוכיות הזמן של החלפת התו החוזר הארוך ביותר?
הפתרון עם החלון האחד רץ בזמן O(n), כאשר n הוא האורך של s: right מבקר בכל אות פעם אחת ו-left זז לכל היותר פעם אחת בכל צעד. הוא משתמש במקום נוסף O(1): 26 מונים וכמה מספרים שלמים.
במה זה שונה ממציאת תת־המחרוזת הארוכה ביותר ללא תווים חוזרים?
בשניהם מזיזים שתי נקודות קצה לאורך המחרוזת, אבל הכלל לחלון תקין שונה. שם, אסור שתו כלשהו יחזור, ולכן החלון מצטמצם עד שהחזרה נעלמת. כאן, האורך פחות הספירה הגבוהה ביותר חייב להיות לכל היותר k, ולכן החלון יכול להחליק באורך קבוע.
האם אפשר לפתור את הבעיה הזאת באמצעות חיפוש בינארי?
כן. אם אפשר להגיע לתת־מחרוזת באורך L, אפשר להגיע גם לכל תת־מחרוזת קצרה יותר שבתוכה, ולכן אפשר לבצע חיפוש בינארי על L. עבור כל L, מזיזים חלון קבוע באורך הזה ובודקים אם יש מיקום כלשהו שדורש לכל היותר k שינויים. הסיבוכיות היא O(n log n), וזה איטי יותר מפתרון החלון היחיד.
בעיות דומות
בעיות שמשתמשות באותם רעיונות. פתרון של שתיים או שלוש מהן מקבע את הדפוס.
def characterReplacement(s, k):
# כתבו כאן את הקודבדיקה #1
בדיקה #2
בדיקה #3
ארגומנטים
s = "BAAACAB" k = 1
הפלט הצפוי
5