Minimum Window Substring
Вам даны две строки: s и t. Найдите самую короткую подстроку строки s, которая содержит все символы из t с учётом повторений: если в t одна и та же буква встречается дважды, подстрока должна содержать её как минимум дважды. Порядок не имеет значения, также могут встречаться и другие символы.
Если есть несколько самых коротких подстрок, верните самую левую. Если ни одна подстрока не содержит все символы из t, верните пустую строку.
Функция
- sstring
- строка, в которой нужно искать
- tstring
- символы, которые должно содержать окно, включая повторения
- Возвращаетstring
- самая короткая, а при равной длине — самая левая подстрока s, содержащая все символы из t, или пустая строка
Ограничения
1 ≤ s.length ≤ 5 × 1041 ≤ t.length ≤ 104sиtсодержат только английские буквы. Заглавные и строчные буквы — это разные символы.- Если несколько подстрок имеют минимальную длину, ответом будет самая левая из них; если таких нет, это
"".
Примеры
- Ввод
- s = "mappingtheplan"t = "nap"
- Вывод
- "plan"
- Пояснение
- Первое окно слева, в котором есть
n,aиp, — этоappin, пять символов. Вplanвсе три есть в четырёх символах, а ни одна последовательность из трёх символов не содержит их все.
- Ввод
- s = "banana"t = "aan"
- Вывод
- "ana"
- Пояснение
tтребует две копииaи однуn.anaс индексом 1 содержит именно это. Втораяanaначинается с индекса 3, и выбирается самая левая.
- Ввод
- s = "Coddy"t = "cd"
- Вывод
- ""
- Пояснение
- Буква
CвCoddy— заглавная, это другой символ, чемc. Ни одна подстрока не содержит строчнуюc, поэтому ответ — пустая строка.
Дополнительный вопрос
Когда в t мало букв, а s длинная, большая часть s вообще не имеет значения. Можешь сделать так, чтобы окно перемещалось только между позициями, в которых находится буква из t?
Подсказки
Открывайте по одной. Каждая подсказывает чуть больше.
Окно, которое содержит всё содержимое
t, продолжит его содержать, если сделать его длиннее, а окно, в котором чего-то не хватает, по-прежнему будет неполным, если сделать его короче. Используйте это, чтобы не перебирать каждое начало с каждым концом.Перемещай правую границу вперёд, пока окно не охватит
t. Затем перемещай левую границу вперёд, пока окно всё ещё охватываетt, каждый раз записывая результат. Ни одну из границ не нужно перемещать назад.Ведите таблицу, в которой указано, сколько ещё копий каждого символа не хватает окну, и храните одно число
missing, показывающее общее количество недостающих копий. Когда символ входит в окно, значениеmissingуменьшается, только если этот символ всё ещё нужен; когда символ покидает окно, значение увеличивается, только если этого символа в окне становится недостаточно. Окно содержитtв точности тогда, когдаmissingравно 0.
Решение
Важны только количества символов в окне, а не их порядок. Передвигай правую границу, пока окно не будет содержать t, затем передвигай левую, пока оно всё ещё содержит t; ни одна из границ не движется назад. Счётчик недостающих символов за один шаг показывает, содержит ли окно t.
Расширьте окно от каждой начальной позиции
Верно, но не успевает на самых больших тестах
Идея
Зафиксируй начало и расширяй окно по одному символу за раз, подсчитывая количество каждого символа. После каждого шага проверяй каждую из u букв, используемых в t. Первый конец, который подходит, задаёт самое короткое окно, покрывающее все символы, для этого начала, поэтому остановись на нём. Начала рассматриваются слева направо, и только строго более короткое окно заменяет лучшее, поэтому при равенстве длины остаётся самое левое.
Если подходящего окна нет, при каждом начале чтение продолжается до конца s: около n²/2 шагов, 1.25 × 10^9 при n = 5 × 10^4.
Алгоритм
- Подсчитай, сколько раз встречается каждая буква, которую запрашивает
t. - Для каждого
startочищай таблицу подсчётов и двигайendвправо, добавляяs[end]. - Когда окно содержит достаточно каждой буквы, сохраняй его, если оно строго короче лучшего, и переходи к следующему началу.
- Верни лучшее окно или
"", если ни одно не содержалоt.
def minWindow(s, t):
need = [0] * 128 # copies of each character code that t asks for
for ch in t:
need[ord(ch)] += 1
letters = [c for c in range(128) if need[c] > 0]
best_start, best_len = 0, len(s) + 1
for start in range(len(s)):
have = [0] * 128 # counts inside s[start..end]
for end in range(start, len(s)):
have[ord(s[end])] += 1
if all(have[c] >= need[c] for c in letters):
# The first end that covers t gives the shortest window from this start.
if end - start + 1 < best_len:
best_start, best_len = start, end - start + 1
break
if best_len > len(s):
return ""
return s[best_start:best_start + best_len]Скользящее окно, которое проверяет каждую букву
Идея
Добавление символов в покрывающее окно сохраняет его покрывающим, а удаление символов из окна, в котором чего-то не хватает, сохраняет эту нехватку. Поэтому по мере перемещения начала вправо конец самого короткого покрывающего окна никогда не перемещается влево.
Ни одно окно не будет пропущено. Возьмём лучшее окно от L до R. Чтобы left прошёл дальше L до того, как right достигнет R, более короткое окно, начинающееся с L, должно было бы покрывать t. Поэтому, когда right достигает R, сужение перемещает left до L и фиксирует это окно.
Каждая граница перемещается не более n раз, но при каждой проверке считывается до u значений счётчиков: O(n·u).
Алгоритм
- Посчитай количество копий, которое требуется для
t; установиleft = 0, а лучшую длину —n+1. - Для каждого
rightдобавляйs[right]к счётчикам окна. - Пока каждой буквы из
tдостаточно, сохраняй окно, если оно строго короче, удаляйs[left]и передвигайleftвперёд. - Верни лучшее окно или
"", если лучшая длина всё ещё равнаn+1.
def minWindow(s, t):
need = [0] * 128 # copies of each character code that t asks for
for ch in t:
need[ord(ch)] += 1
letters = [c for c in range(128) if need[c] > 0]
have = [0] * 128 # counts inside s[left..right]
def covers():
for c in letters:
if have[c] < need[c]:
return False
return True
left = 0
best_start, best_len = 0, len(s) + 1
for right in range(len(s)):
have[ord(s[right])] += 1 # expand on the right
while covers(): # shrink from the left while the window still covers t
if right - left + 1 < best_len:
best_start, best_len = left, right - left + 1
have[ord(s[left])] -= 1
left += 1
if best_len > len(s):
return ""
return s[best_start:best_start + best_len]Скользящее окно со счётчиком пропусков
Идея
Замените просмотр одним числом. Пусть need[c] — количество вхождений c, которое требуется в t, минус количество вхождений в окне; отрицательное значение означает избыток. Пусть missing — количество символов, которых всё ещё не хватает окну, изначально равное длине t. Окно точно покрывает t, когда missing равно 0.
Символ, который входит в окно, когда его need больше 0, заполняет пробел, поэтому missing уменьшается. Символ, удаление которого увеличивает need до значения больше 0, создаёт пробел, поэтому missing увеличивается. Избыток на него не влияет. Для banana с t = aan значение missing впервые достигает 0 на bana, а удаление лишнего b оставляет ana, которое по-прежнему покрывает строку.
Каждый символ входит и выходит один раз с постоянной стоимостью: время O(n + m), одна таблица из 128 счётчиков.
Алгоритм
- Заполни
needколичествами вхождений символов изt; установиmissingравным длинеt,left = 0, а лучшую длину — равнойn+1. - Для каждого
right: еслиneed[s[right]]больше 0, уменьшиmissing; затем уменьшиneed[s[right]]. - Пока
missingравно 0, сохрани окно, если оно строго короче. Увеличьneed[s[left]]; если теперь оно больше 0, увеличьmissing. Сдвиньleftвперёд. - Верни лучшее окно или
"", если лучшая длина всё ещё равнаn+1.
def minWindow(s, t):
# need[c]: copies of c that t asks for minus copies inside the window.
# Positive means the window still lacks c; negative means it holds spares.
need = [0] * 128
for ch in t:
need[ord(ch)] += 1
missing = len(t) # characters of t the window does not cover yet
left = 0
best_start, best_len = 0, len(s) + 1
for right in range(len(s)):
c = ord(s[right])
if need[c] > 0: # this copy fills a gap
missing -= 1
need[c] -= 1
while missing == 0: # the window covers t: record it, then shrink
if right - left + 1 < best_len:
best_start, best_len = left, right - left + 1
c = ord(s[left])
need[c] += 1
if need[c] > 0: # gave away a copy t needs
missing += 1
left += 1
if best_len > len(s):
return ""
return s[best_start:best_start + best_len]
Ловушки и крайние случаи
- Подсчёт букв вместо их количества. Для
t = aanнужны две буквыa, поэтомуbanне подходит. - Изменение значения
missingпри добавлении или удалении каждого символа. Тогда лишняяaзасчитывается, и значениеmissingможет стать равным 0, хотя в окне нетn. - Замена лучшего окна на окно той же длины. Во втором примере это приводит к возврату
anaс индексом 3. - Использование
nв качестве длины, означающей «не найдено». Если ответом является вся строкаs, её длина тоже равнаn; начни сn+1. - Таблица из 26 ячеек с индексами
c - 'a'. Заглавные буквы, как в третьем примере, в неё не помещаются.
Частые вопросы3
Какова временная сложность задачи «Минимальная подстрока, содержащая все символы»?
Алгоритм с окном и счётчиком недостающих символов работает за время O(n + m), где n и m — длины s и t. При построении таблицы t считывается один раз, а каждый символ s входит в окно и выходит из него не более одного раза. Дополнительная память — один счётчик на каждый код символа.
Почему левый край никогда не сдвигается назад?
Левый край проходит позицию только после того, как окно, начинающееся в этой позиции, охватило t, и это окно было самым коротким из покрывающих с этой позиции. Любое окно, заканчивающееся дальше, будет длиннее, поэтому, возвращаясь назад, никогда нельзя найти более подходящий ответ.
Чем задача о минимальном окне подстроки отличается от поиска анаграммы в строке?
Анаграмма содержит ровно те же буквы, что и t, поэтому её окно имеет фиксированную длину m и сдвигается на один шаг за раз. Здесь допускаются лишние символы, поэтому длина входит в ответ: окно растёт, пока не охватит t, и сужается, пока оно всё ещё его охватывает.
Похожие задачи
Задачи на те же идеи. Решите две или три, и приём запомнится.
def minWindow(s, t):
# Напишите код здесьТест #1
Тест #2
Тест #3
Аргументы
s = "mappingtheplan" t = "nap"
Ожидаемый вывод
"plan"