Minimum Window Substring
Даны две строки: s и t. Найдите самую короткую подстроку s — последовательность идущих подряд символов, — которая содержит все символы из t с учётом повторений: если в t одна и та же буква встречается дважды, подстрока должна содержать её как минимум дважды. Порядок не имеет значения, подстрока также может содержать другие символы.
Если несколько подстрок имеют одинаковую минимальную длину, верните самую левую. Если ни одна подстрока s не содержит все символы из 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, поэтому ответ — пустая строка.
+17 скрытых тестов при отправке
Дополнительный вопрос
Когда t состоит всего из нескольких букв, а s — длинная строка, большая часть s никогда не будет иметь значения. Можешь сделать так, чтобы окно перемещалось только между позициями, в которых находится буква из t?
Подсказки
Открывайте по одной. Каждая подсказывает чуть больше.
Окно, которое содержит всю строку
t, продолжит её содержать, если его удлинить, а окно, в котором чего-то не хватает, по-прежнему будет этого не содержать, если его укоротить. Используй это, чтобы не перебирать каждую начальную позицию с каждой конечной.Перемещай правую границу вперёд, пока окно не будет содержать
t. Затем перемещай левую границу вперёд, пока окно всё ещё содержитt, каждый раз записывая её. Ни одну из границ не нужно перемещать назад.Ведите таблицу, показывающую, сколько ещё копий каждого символа нужно окну, и одну переменную
missing, обозначающую общее количество недостающих копий. Символ, который входит в окно, уменьшаетmissing, только если он всё ещё нужен, а символ, который выходит из окна, увеличивает его, только если символа в окне не хватает. Окно содержитtв точности тогда, когдаmissingравно 0.
Решение
Ответ зависит от того, сколько символов каждого вида содержит окно, а не от их порядка; при этом лучшее окно может начинаться где угодно. Если перебирать каждую начальную позицию с каждой конечной, получится O(n²) окон. Решение — использовать окно, границы которого движутся только вперёд: правая граница расширяет его, пока оно не покроет t, левая сужает его, пока оно продолжает покрывать t, а один счётчик недостающих символов позволяет за один шаг определить, покрывает ли оно t.
Расширяйте окно, начиная с каждой позиции
Верно, но не успевает на самых больших тестах
Идея
Зафиксируй начало подстроки. Затем увеличивай её на один символ за раз, подсчитывая количество каждого символа внутри, и после каждого шага проверяй, покрывает ли она t: для каждой из u разных букв, используемых в t, окно должно содержать не меньше экземпляров, чем есть в t. Первый конец, при котором условие выполняется, задаёт самое короткое покрывающее окно с этим началом, поскольку все более короткие варианты с тем же началом уже проверялись и не подошли. На этом остановись.
Повтори это для каждого начала и сохрани самое короткое окно. Начала проверяются слева направо, а окно заменяет лучший результат, только если оно строго короче, поэтому среди окон одинаковой длины остаётся самое левое.
Этот алгоритм работает медленно, когда окна длинные или подходящего окна нет. Если единственная Z в s находится в самом конце, а в t требуется одна такая буква, при каждом начале алгоритм просматривает строку до конца: примерно n²/2 шагов, то есть 1.25 × 10^9 при n = 5 × 10^4, и на каждом шаге выполняет проверку по 52 буквам. То же самое происходит, когда подходящего окна вообще нет.
Алгоритм
- Посчитайте, сколько раз встречается каждый символ, который требуется в
t, и составьте список используемых букв. - Для каждого
startочистите таблицу счётчиков и перемещайтеendотstartдо концаs, добавляяs[end]в таблицу. - После каждого добавления проверьте каждую букву из
t. Если окно содержит достаточное количество каждой буквы, сравните его длину с лучшей на данный момент, сохраните его, если оно строго короче, и прекратите расширять окно. - После перебора всех начальных позиций верните лучшее окно или
"", если ни одно из них не содержало все символы из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]Скользящее окно, которое проверяет каждую букву
Идея
Два факта позволяют обойтись без перезапуска. Если добавить символы в окно, которое покрывает t, оно продолжит его покрывать; если удалить символы из окна, в котором чего-то не хватает, этого по-прежнему будет не хватать. Поэтому, когда начало сдвигается вправо, конец самого короткого окна, покрывающего t, может либо оставаться на месте, либо сдвигаться вправо. Оба края могут двигаться вперёд одновременно, и ни один из них никогда не движется назад.
Перемещайте right по строке s, добавляя каждый символ в таблицу счётчиков. Когда окно покрывает t, оно становится кандидатом: запишите его, если оно короче лучшего, затем удалите s[left] и сдвиньте left вперёд, после чего проверьте ещё раз. Повторяйте, пока окно не перестанет покрывать t, а затем снова расширяйте его вправо.
Ни одно окно не будет пропущено. Возьмём лучшее окно от L до R. Если бы left прошёл дальше L, прежде чем right достиг R, то какое-то окно от L, заканчивающееся до R, покрывало бы t и было бы короче лучшего. Поэтому, когда right достигает R, цикл сужения передвигает left до L и записывает лучшее окно. Каждый край перемещается не более n раз, но при каждой проверке считывается до u счётчиков — по одному для каждой буквы, используемой в t, — хотя с момента предыдущей проверки изменился только один счётчик.
Алгоритм
- Подсчитайте, сколько раз встречается каждая буква в
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.
Обновление занимает один шаг. Когда s[right] входит в окно и значение need для него больше 0, оно заполняет пробел, поэтому missing уменьшается на единицу; в любом случае need уменьшается на единицу и может стать меньше 0, обозначая избыток. Когда s[left] покидает окно, need увеличивается на единицу, и если теперь оно больше 0, окно потеряло вхождение, необходимое строке t, поэтому missing увеличивается на единицу. Избыток появляется и исчезает, не затрагивая missing.
Проследим за s = banana, t = aan: изначально need равно 2 для a и 1 для n, а missing равно 3. b не нужна. Первая a уменьшает missing до 2, n — до 1, вторая a — до 0, поэтому bana покрывает t. Сужение убирает лишнюю b, оставляя ana длиной три символа — это новый лучший результат. Удаление этой a снова устанавливает missing в 1. Последняя a снова обеспечивает покрытие, и окно nana сужается до второй ana. Она не короче, поэтому остаётся самая левая ana.
Каждый символ s один раз входит в окно и не более одного раза покидает его, а каждое перемещение требует фиксированного объёма работы. При построении need строка t просматривается один раз. Весь процесс выполняется за 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. Уменьшайте его только если значениеneedбыло больше 0. - Увеличение
missingдля каждого удалённого символа. Удаление лишнего символа не мешает окну покрыватьt; увеличивайте его, только если значениеneedстановится больше 0. - Фиксация окна после цикла сужения. К этому моменту оно уже не покрывает
t. Фиксируйте его внутри цикла, прежде чем удалитьs[left]. - Замена лучшего окна, когда новое окно такой же длины. В результате будет возвращено самое правое из кратчайших окон; используйте строгое сравнение «меньше».
- Использование
nв качестве длины для случая «не найдено». Если ответ — вся строкаs, её длина тоже равнаn. Начинайте сn+1, чтобы эти два случая различались. - Таблица из 26 ячеек с индексами
c - 'a'. Заглавные буквы в неё не помещаются. Используйте отдельную ячейку для каждого кода символа.
Частые вопросы4
Какова временная сложность задачи «Минимальная подстрока, содержащая все символы»?
Алгоритм скользящего окна со счётчиком недостающих символов работает за время O(n + m), где n и m — длины s и t. Для построения таблицы строка t считывается один раз, а каждый символ строки s входит в окно и выходит из него не более одного раза, при фиксированных затратах на каждое перемещение. Дополнительная память — таблица с одним счётчиком на каждый код символа; её размер не растёт вместе с размером входных данных.
Почему левый край никогда не сдвигается назад?
Левая граница проходит позицию только после того, как окно, начинающееся там, охватило t, и это было самое короткое покрывающее окно с такой начальной позицией. Любое окно, которое начинается там и заканчивается позже, длиннее, поэтому возврат назад никогда не помог бы найти лучший ответ. Именно поэтому обе границы проходят вперёд по одному разу, а объём работы остаётся линейным.
Что считает отсутствующий счётчик?
Это число копий символов, которые запрашивает t, но которых пока нет в окне, — сумма положительных значений в need. Изначально оно равно длине t и становится равным 0 ровно тогда, когда окно содержит все символы из t. Лишние копии его не меняют, поэтому вместо перебора всех букв достаточно одного сравнения.
Чем задача Minimum Window Substring отличается от поиска анаграммы в строке?
Анаграмма содержит ровно те же буквы, что и t, и никаких других, поэтому окно имеет фиксированную длину m и сдвигается на один шаг за раз. Здесь в окне могут быть лишние символы, поэтому его длина является частью ответа: оно растёт вправо, пока не охватит t, и сжимается слева, пока это остаётся возможным.
Похожие задачи
Задачи на те же идеи. Решите две или три, и приём запомнится.
Python
def minWindow(s, t):
# Напишите код здесьСлучай 1
Случай 2
Случай 3
Ввод
s = "mappingtheplan" t = "nap"
Ожидается
"plan"