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
- 説明
- < p>
CをAに変更すると、インデックス1から5はAAAAAになります。6文字の場合は2箇所の変更が必要です。インデックス0から5にはBとCがあり、インデックス1から6にはCと最後のBがあります。
- 入力
- s = "AABBBAB"k = 2
- 出力
- 6
- 説明
ABBBABでは、インデックス1から6までの範囲で、Bではない文字は2つのAだけなので、2回変更すればBBBBBBになります。文字列全体にはAが3つ、Bが4つ含まれているため、3回変更する必要があります。
- 入力
- s = "WXYZ"k = 0
- 出力
- 1
- 説明
- 変更が許されない場合、答えは文字列内にすでにある最長の連続部分です。すべての文字は隣の文字と異なるため、その連続部分は1文字です。
提出時に隠しテスト+17件
発展問題
26個の大文字だけでなく、sに任意の文字を格納できる場合、何が変わりますか?
ヒント
1つずつ開いてください。開くたびに少しずつ答えに近づきます。
固定された部分文字列ごとに、他のすべての文字をどの文字に変えるべきで、その変更には何回かかりますか?
部分文字列の長さから最も多く出現する文字の個数を引いた値が
k以下の場合、その部分文字列は条件を満たします。文字列上で両端を前方に移動させながら、この条件を満たす最長のウィンドウを見つけましょう。26 個のカウントと最大カウント
topを保持します。右側に1文字追加します。ウィンドウを変更するのにk回を超える変更が必要になった場合は、左側から1文字削除して長さを一定に保ちます。ウィンドウを縮める必要はなく、topを減らす必要もありません。
解説
部分文字列1つのコストは明快です。長さから、最も多く出現する文字の個数を引いた値です。難しいのは、n²個すべての部分文字列のコストを計算しないことです。スライディングウィンドウを使えば文字列を1回走査するだけで済み、最善の方法は2つの事実に基づいています。ウィンドウを縮める必要はなく、最大の文字出現数が減ることもありません。
すべての部分文字列を確認する
正しいが、最大のテストでは終わらない
考え方
部分文字列を1つ修正します。どの文字にすればよいでしょうか?最も多く現れる文字です。他の文字はすべて変更する必要があるからです。したがって、最も多く現れる文字がtop回出現する長さlenの部分文字列を変更するには、len - top回の変更が必要で、それがk以下なら到達可能です。
すべての部分文字列を試します。各開始位置について、終端を1文字ずつ伸ばしながら文字ごとの出現回数を数え、進めるたびにtopを更新します。こうすれば、新しい部分文字列ごとに数え直す必要はなく、更新1回で済みます。すべての部分文字列を調べるため、到達可能な最長のものを見逃すことはありません。
文字列の長さが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対象の文字ごとに1つのスライディングウィンドウ
考え方
問題を逆向きに考え、まず文字を選びます。最終的な並びがすべて 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縮まないウィンドウ
考え方
1つのウィンドウ内のすべての文字を扱います。その中の26文字それぞれの個数と、最も大きい個数であるtopを数えます。ウィンドウを変更するにはlength - top回の変更が必要なので、それがk以下なら条件を満たします。
まず1つ目の事実です。ウィンドウを縮める必要はありません。これまでに見つかった最長の長さを超えることだけが目的なので、s[right]を追加してウィンドウの変更コストが大きくなりすぎたら、左端の文字を1つ削除します。ウィンドウは1つ分スライドし、長さは変わりません。変更コストが大きくなりすぎなければ、長さは1つ増えます。したがって、ウィンドウの長さは常にそれまでに見つかった最長の長さであり、最後の答えはn - leftです。
2つ目の事実です。topを減らす必要はありません。左端から文字が外れてもtopはそのままにしておくため、ウィンドウ内の実際の個数より大きくなることがあります。それでも問題ありません。スライドした後、ウィンドウの長さはちょうどtop + kなので、長さを増やすには、ウィンドウ内にtop + 1回現れる文字が必要です。そのときtopもそれに合わせて増えます。古いままのtopによってウィンドウがスライドすることはあっても、誤って拡大することはありません。また、より長いウィンドウだけが記録を更新できるため、スライドしても問題ありません。
k = 1のBAAACABでは、ウィンドウはBAAAまで拡大し、その後BAAACにするには2回の変更が必要なので、AAACまでスライドします。次のAを追加するとtopが4に増え、ウィンドウは長さ5のAAACAまで拡大します。最後のBで再び1回スライドするので、答えは5です。
アルゴリズム
- 26 個のカウント、
left = 0、top = 0を保持します。 - 文字列上で
rightを進めます。s[right]をそのカウントに加え、そのカウントが現在より大きくなった場合はtopを増やします。 right - left + 1 - top > kの場合、ウィンドウ内で変更が必要な文字数が多すぎます。カウントからs[left]を除き、leftを 1 つ進めます。ウィンドウはスライドし、長さを維持します。- 文字がウィンドウから外れても、
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になりますが、正しい変更箇所は中央にあり、2つの連続部分をつなげて5にします。 - ウィンドウ内で最も多い文字ではなく、最初の文字を基準に変更数を数える。ウィンドウ
BAAAで必要な変更は3回ではなく1回です。 - ウィンドウが縮む可能性のある実装で
n - leftを返す。この近道が成り立つのは、ここにある1ウィンドウのコードのように、ウィンドウが決して短くならない場合だけです。ループでwhileを使ってウィンドウを縮め、実際の最大値を再計算するなら、別途bestを保持してください。 - ウィンドウの長さを
right - leftとして計算する。両端もウィンドウに含まれるので、1を足してください。 k = 0を特別扱いする。変更がなくても、ウィンドウのルールによって最長の同じ文字の連続部分がすでに返されます。
よくある質問4
最長反復文字置換の時間計算量はどれくらいですか?
1つのウィンドウを使う解法は、s の長さを n とすると、O(n) 時間で実行されます。right は各文字を1回ずつ確認し、left は各ステップで最大1回だけ移動します。追加の領域は O(1) で、26個のカウントといくつかの整数を使用します。
ウィンドウがスライドするとき、なぜ最大頻度を更新する必要がないのでしょうか?
ウィンドウは自分自身の記録を更新しようとしているだけです。スライド後の長さは top + k なので、より長い条件を満たすウィンドウには、ある文字が top 回を超えて出現する必要があり、その場合は top も上がります。高すぎる top はウィンドウをその長さに保つだけで、本来伸びてはいけないときに伸ばすことはありません。
これは「重複する文字を含まない最長部分文字列」とどう違いますか?
どちらも文字列上を2つの端で移動しますが、ウィンドウが良いとされる条件は異なります。あちらでは、文字が重複していないときにウィンドウは良いとされ、重複がなくなるまで縮める必要があります。こちらでは、ウィンドウの長さから最も出現回数の多い文字の出現回数を引いた値が k 以下ならウィンドウは良いとされるため、縮める代わりに一定の長さのままスライドできます。
この問題は二分探索で解けますか?
はい。長さ L の部分文字列が到達可能なら、その中にあるそれより短い部分文字列もすべて到達可能なので、L に対して二分探索できます。各 L について、その長さの固定ウィンドウをスライドさせ、変更が k 回以下で済む位置があるかどうかを確認します。これは O(n log n) で、ウィンドウを1つだけ使う解法より遅いですが、十分に妥当な回答です。
Python
def characterReplacement(s, k):
# ここにコードを書いてくださいケース1
ケース2
ケース3
入力
s = "BAAACAB" k = 1
期待値
5