Longest Repeating Character Replacement
대문자 영어 문자로 이루어진 문자열 s와 정수 k가 주어집니다. s에서 최대 k개의 위치를 선택하고, 각 위치의 문자를 다른 대문자로 바꿀 수 있습니다.
변경을 마친 후 같은 문자가 반복되는 부분 문자열, 즉 서로 인접한 문자들의 연속 구간 중 가장 긴 것의 길이를 반환하세요.
함수
- sstring
- 대문자 문자열
- kinteger
- 변경할 수 있는 최대 글자 수
- 반환값integer
- 만들 수 있는 동일한 문자가 반복되는 가장 긴 부분 문자열의 길이
제약 조건
1 ≤ s.length ≤ 5 × 104s에는 영어 대문자만 들어 있습니다.0 ≤ k ≤ s.length
예제
- 입력
- s = "BAAACAB"k = 1
- 출력
- 5
- 설명
C를A로 바꾸면 인덱스 1부터 5까지는AAAAA로 읽힙니다. 문자 6개를 바꾸려면 두 번 변경해야 합니다. 인덱스 0부터 5까지에는B와C가 있고, 인덱스 1부터 6까지에는C와 마지막B가 있습니다.
- 입력
- s = "AABBBAB"k = 2
- 출력
- 6
- 설명
ABBBAB에서 인덱스 1부터 6까지 보면, 두 개의A만B가 아닌 문자이므로 두 번 바꾸면BBBBBB가 됩니다. 전체 문자열에는A가 세 개,B가 네 개 있으므로 세 번 바꿔야 합니다.
- 입력
- s = "WXYZ"k = 0
- 출력
- 1
- 설명
- 변경이 허용되지 않으므로, 답은 문자열에 이미 있는 가장 긴 연속 구간입니다. 모든 문자는 양옆의 문자와 다르므로, 해당 연속 구간의 길이는 한 글자입니다.
제출 시 숨은 테스트 +17개
후속 질문
s가 26개의 대문자뿐만 아니라 모든 문자를 담을 수 있다면 무엇이 달라질까요?
힌트
하나씩 열어 보세요. 열 때마다 조금 더 알려 줍니다.
고정된 부분 문자열 하나에 대해, 나머지 각 문자는 어떤 문자로 바뀌어야 하며, 그렇게 바꾸는 데 드는 비용은 얼마인가요?
부분 문자열의 길이에서 가장 많이 등장하는 문자의 개수를 뺀 값이
k이하이면 해당 부분 문자열에 도달할 수 있습니다. 문자열의 양쪽 경계를 앞으로 이동하면서 이 규칙을 만족하는 가장 긴 구간을 찾으세요.26개의 개수와 가장 높은 개수인
top을 유지합니다. 오른쪽에 문자 하나를 추가하고, 이제 창을 바꾸는 데k번보다 더 많이 변경해야 한다면 왼쪽에서 문자 하나를 제거하여 길이를 동일하게 유지합니다. 창의 크기를 줄일 필요는 없으며,top은 낮출 필요가 없습니다.
풀이
부분 문자열 하나의 비용은 쉽게 알 수 있습니다. 길이에서 가장 많이 등장하는 문자의 개수를 빼면 됩니다. 어려운 점은 n²개의 모든 부분 문자열에 대한 비용을 지불하지 않는 것입니다. 슬라이딩 윈도우는 문자열을 한 번만 읽으며, 가장 효율적인 방식은 두 가지 사실에 기반합니다. 윈도우는 줄어들 필요가 없고, 가장 높은 문자 개수는 줄어들 필요가 없습니다.
모든 부분 문자열을 확인하세요
정답이지만 가장 큰 테스트에서는 끝나지 않음
핵심 아이디어
부분 문자열 하나를 수정합니다. 어떤 문자로 바꿔야 할까요? 이미 가장 많이 나타나는 문자로 바꾸면 됩니다. 다른 모든 문자를 바꿔야 하기 때문입니다. 따라서 가장 많이 나타나는 문자가 top번 등장하는 길이 len의 부분 문자열은 len - top번 수정해야 하며, 그 횟수가 k 이하라면 만들 수 있습니다.
모든 부분 문자열을 시도해 봅니다. 시작 위치마다 끝 위치를 한 글자씩 늘리면서 각 문자의 개수를 세고, 진행하면서 top을 갱신합니다. 그러면 새 부분 문자열마다 개수를 처음부터 다시 세는 대신 한 번만 갱신하면 됩니다. 모든 부분 문자열을 확인하므로 만들 수 있는 가장 긴 부분 문자열을 놓칠 수 없습니다.
문자열의 길이가 n일 때 부분 문자열은 약 n²/2개이므로 느립니다. n = 5 × 10^4이면 확인 횟수가 1.25 × 10^9회에 달해 제한 시간 안에 처리하기에는 훨씬 많습니다.
알고리즘
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라면, 질문은 다음과 같이 바뀝니다. A가 아닌 문자가 최대 k개 포함된 가장 긴 부분 문자열은 무엇일까요? 이는 전형적인 슬라이딩 윈도우 문제입니다.
문자열에서 right를 이동하며 윈도우 안에 있는 목표 문자가 아닌 문자의 개수를 셉니다. 그 개수가 k를 초과하면 다시 k가 될 때까지 left를 앞으로 이동합니다. 윈도우를 키우면 바꿔야 하는 문자만 추가될 수 있으므로, 비용이 너무 큰 윈도우는 커져도 계속 비용이 너무 크며 left를 뒤로 이동할 필요는 없습니다. 각 right에 대해 유지하는 윈도우는 해당 위치에서 끝나는 조건을 만족하는 가장 긴 윈도우입니다.
26개 문자 모두에 대해 이 과정을 실행하고 가장 긴 길이를 유지합니다. 각 실행은 O(n)이므로 총 26회 반복하며, n = 5 × 10^4일 때 약 1.3 × 10^6단계입니다. 이는 선형 시간 알고리즘이지만 문자열을 26번 읽으며, 알파벳이 작기 때문에 가능한 방법입니다.
알고리즘
A부터Z까지 각 대상 문자에 대해left = 0과others = 0으로 윈도우를 시작합니다.- 문자열을 따라
right를 이동합니다.s[right]가 대상 문자가 아니면others를 1 증가시킵니다. others > k인 동안left를 앞으로 이동하고, 빠져나가는 문자가 대상 문자가 아니면others를 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을 유지합니다. 윈도우에는 length - top번의 변경이 필요하므로, 그 값이 k 이하이면 유효합니다.
첫 번째 사실: 윈도우는 줄어들 필요가 없습니다. 지금까지 찾은 최장 길이보다 더 길어지는 경우에만 관심이 있으므로, s[right]를 추가했을 때 윈도우를 유효하게 만들기 위한 변경 횟수가 너무 많아지면 왼쪽에서 문자 하나를 제거합니다. 윈도우는 한 칸 이동하고 길이는 그대로 유지됩니다. 윈도우가 유효하면 길이가 1 증가합니다. 따라서 윈도우의 길이는 항상 지금까지 찾은 최장 길이이며, 마지막에 답은 n - left입니다.
두 번째 사실: top은 줄어들 필요가 없습니다. 왼쪽에서 문자가 빠져나갈 때 top은 그대로 두므로, 윈도우 안의 실제 개수보다 클 수도 있습니다. 그래도 괜찮습니다. 한 칸 이동한 후 윈도우 길이는 정확히 top + k이므로, 윈도우를 늘리려면 윈도우 안에 top + 1번 나타나는 문자가 필요하고, 그 순간 top도 함께 증가합니다. 오래된 top 값 때문에 윈도우가 이동할 수는 있지만, 잘못해서 길어지지는 않습니다. 이동해도 손해가 없는 이유는 기록을 깨려면 더 긴 윈도우가 필요하기 때문입니다.
BAAACAB에서 k = 1일 때, 윈도우는 BAAA까지 늘어난 다음 BAAAC는 변경 2번이 필요하므로 AAAC까지 이동합니다. 다음 A를 추가하면 top이 4로 증가하고, 윈도우는 길이가 5인 AAACA까지 늘어납니다. 마지막 B가 윈도우를 한 번 더 이동하게 하므로 답은 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를 더하는 경우입니다.k = 3인AAAB에서는 문자열보다 긴 6이 나옵니다.k = 1인BAAACAB에서는 4가 나오지만, 올바르게 바꿔야 하는 문자는 가운데에 있어 두 구간을 합쳐 5를 만듭니다. - 윈도우에서 가장 많이 등장하는 문자가 아니라 첫 번째 문자를 기준으로 변경 횟수를 세는 경우입니다.
BAAA윈도우에는 변경이 세 번이 아니라 한 번 필요합니다. - 윈도우가 줄어들 수 있는 버전에서
n - left를 반환하는 경우입니다. 이 지름길은 여기의 단일 윈도우 코드처럼 윈도우가 절대 짧아지지 않을 때만 유효합니다.while로 윈도우를 줄이고 실제 최댓값을 다시 계산하는 루프라면 별도의best를 유지하세요. - 윈도우 길이를
right - left로 계산하는 경우입니다. 양 끝이 모두 윈도우에 포함되므로 1을 더해야 합니다. k = 0을 특별한 경우로 처리하는 경우입니다. 변경 횟수가 0이면 윈도우 규칙만으로도 이미 가장 긴 단일 문자 연속 구간을 반환합니다.
자주 묻는 질문4
Longest Repeating Character Replacement의 시간 복잡도는 무엇인가요?
하나의 윈도우를 사용하는 해결 방법은 s의 길이인 n에 대해 O(n) 시간에 실행됩니다. right는 각 문자를 한 번씩 방문하고 left는 각 단계에서 최대 한 번 이동합니다. 추가 공간은 O(1)만 사용하며, 26개의 카운트와 몇 개의 정수가 필요합니다.
윈도우가 이동할 때 최대 빈도를 업데이트하지 않아도 되는 이유는 무엇인가요?
윈도우는 자기 자신의 기록을 깨려고 할 뿐입니다. 슬라이드가 끝난 후 윈도우의 길이는 top + k이므로, 더 긴 유효한 윈도우가 되려면 어떤 문자가 top번보다 더 많이 나타나야 하고, 그러면 어차피 top도 증가합니다. top이 너무 높으면 윈도우의 길이를 그대로 유지할 뿐이며, 늘어나서는 안 될 때 윈도우를 늘리는 일은 없습니다.
이것은 중복 문자가 없는 가장 긴 부분 문자열과 어떻게 다른가요?
둘 다 문자열 위에서 두 개의 경계를 이동하지만, 유효한 윈도우의 규칙은 다릅니다. 앞의 경우에는 문자가 반복되지 않을 때 윈도우가 유효하며, 반복이 사라질 때까지 윈도우를 줄여야 합니다. 여기서는 윈도우의 길이에서 가장 많이 나온 문자의 개수를 뺀 값이 k 이하일 때 윈도우가 유효하므로, 윈도우를 줄이는 대신 고정된 길이로 슬라이드할 수 있습니다.
이 문제를 이진 탐색으로 해결할 수 있나요?
맞습니다. 길이 L인 부분 문자열에 도달할 수 있다면, 그 안에 있는 더 짧은 모든 부분 문자열에도 도달할 수 있으므로 L에 대해 이분 탐색할 수 있습니다. 각 L에 대해 그 길이의 고정된 윈도우를 이동시키면서, 최대 k번의 변경이 필요한 위치가 있는지 확인하세요. 이는 O(n log n)으로, 윈도우 하나를 사용하는 해법보다 느리지만 충분히 타당한 답변입니다.
비슷한 문제
같은 아이디어를 쓰는 문제입니다. 두세 개를 풀면 패턴이 몸에 익습니다.
Python
def characterReplacement(s, k):
# 여기에 코드를 작성하세요케이스 1
케이스 2
케이스 3
입력
s = "BAAACAB" k = 1
기대값
5