Is Subsequence
Даны две строки: s и t. Верните true, если можно превратить t в s, удалив некоторые её буквы (возможно, ни одной) и сохранив порядок оставшихся букв, и false в противном случае. Например, ace является подпоследовательностью abcde, а aec — нет.
Функция
- sstring
- строка для поиска
- tstring
- строка, из которой нужно удалить буквы
- Возвращаетboolean
- true, если s можно прочитать внутри t по порядку, возможно, с пропусками
Ограничения
1 ≤ s.length ≤ 3 × 1041 ≤ t.length ≤ 5 × 104sиtсодержат только строчные английские буквы.
Примеры
- Ввод
- s = "ace"t = "abcde"
- Вывод
- true
- Пояснение
- Удалите
bиdизabcde, и останетсяaceв том же порядке.
- Ввод
- s = "aec"t = "abcde"
- Вывод
- false
- Пояснение
- В
tесть все три буквы, но единственнаяcстоит перед единственнойe. После того как ты используешьeс индексом 4, справа от неё не останется ни однойc.
- Ввод
- s = "moon"t = "monsoon"
- Вывод
- true
- Пояснение
- Используй
mс индексом 0,oс индексами 1 и 4 иnс индексом 6 в словеmonsoon. Буквы между ними удаляются.
+20 скрытых тестов при отправке
Дополнительный вопрос
Предположим, t остаётся неизменным, а тебе нужно проверить относительно него миллион разных строк s. Как подготовить t, чтобы каждая проверка выполнялась быстрее, чем повторное чтение всего t?
Подсказки
Открывайте по одной. Каждая подсказывает чуть больше.
Посмотри на первую букву в
s. Какую её копию вtнужно использовать?Используй самую раннюю копию. Выбор более поздней копии может только оставить меньше
tдля оставшейся частиs, поэтому самый ранний выбор никогда не хуже.Храни один индекс для
sи один дляt. Продвигайся поtпо одной букве за раз, увеличивай индекс дляsпри каждом совпадении и в конце проверь, достиг ли он концаs.
Решение
Подпоследовательность может пропускать буквы t в любом месте, поэтому может показаться, что нужно попробовать много способов разместить s внутри t. Это не так. Сопоставлять каждую букву s в самом раннем возможном месте всегда не хуже, чем делать любой другой выбор, и это превращает поиск в один проход слева направо с двумя указателями.
Динамическое программирование по префиксам
Верно, но не успевает на самых больших тестах
Идея
Задай вопрос поменьше: входят ли первые i букв s в первые j букв t? Назовём ответ dp[i][j]. Если они входят в t[:j-1], то входят и в t[:j], поскольку можно удалить t[j-1]. Если s[i-1] равно t[j-1], можно также использовать эту букву, и тогда первые i-1 букв s должны входить в t[:j-1]. Значит, dp[i][j] = dp[i][j-1] or (s[i-1] == t[j-1] and dp[i-1][j-1]), а пустой префикс s входит куда угодно.
Строка i использует только строку i-1, поэтому достаточно двух строк длины m+1. Ответ — последняя ячейка последней строки.
Это та же таблица, что строится для наибольшей общей подпоследовательности. Она даёт правильный ответ, но заполняется каждая ячейка. При длине s в 25 000 букв и длине t в 50 000 букв получится 1.25 × 10^9 ячеек — гораздо больше, чем нужно для одного прохода по двум строкам.
Алгоритм
- Создайте строку
prevизm+1значений, все они —true: пустаяsподходит для каждого префиксаt. - Для каждого
iот 1 доnсоздайте строкуcurсcur[0] = false. - Для каждого
jот 1 доmзадайтеcur[j]значениеcur[j-1]илиprev[j-1], еслиs[i-1]равноt[j-1]. - Замените
prevнаcur. - Верните
prev[m].
def isSubsequence(s, t):
n, m = len(s), len(t)
# prev[j]: the first i-1 letters of s fit inside t[:j]. An empty s fits anywhere.
prev = [True] * (m + 1)
for i in range(1, n + 1):
cur = [False] * (m + 1)
for j in range(1, m + 1):
cur[j] = cur[j - 1] or (s[i - 1] == t[j - 1] and prev[j - 1])
prev = cur
return prev[m]Два указателя с жадным сопоставлением
Идея
Просматривайте t слева направо и храните указатель i на следующую букву s, которая вам ещё нужна. Когда t[j] совпадает с s[i], используйте её и передвиньте i вперёд. В любом случае передвиньте j вперёд. Если i доходит до конца s, значит, для каждой буквы нашлось место в нужном порядке.
Почему безопасно брать первое совпадение? Предположим, что в некотором допустимом размещении используется более поздняя копия s[i]. Если заменить её самой ранней копией, порядок сохранится, а справа останется больше букв t для остальных букв s. Поэтому жадный выбор никогда не лишает нас существующего размещения. Для moon в monsoon указатель берёт o с индексом 1, пропускает n и s, берёт o с индексом 4 и останавливается на n с индексом 6.
j посещает каждую букву t один раз, а i только движется вперёд, поэтому цикл выполняется не более m раз. Для этого алгоритму нужны всего два индекса.
Алгоритм
- Задай
i = 0дляsиj = 0дляt. - Пока оба индекса находятся внутри своих строк, сравнивай
s[i]сt[j]. - Если они равны, увеличь
i. - В любом случае увеличь
j. - Верни, равен ли
iдлинеs.
def isSubsequence(s, t):
i = j = 0
while i < len(s) and j < len(t):
if s[i] == t[j]:
i += 1
j += 1
return i == len(s)
Ловушки и крайние случаи
Цикл с двумя указателями короткий, а ошибки в нём возникают на краях.
- Поиск каждой буквы из
sгде угодно вt, а не после предыдущего совпадения. Так строкаaecсчитается подпоследовательностью строкиabcde, хотя порядок нарушен. - Повторное использование одной и той же буквы.
noonне является подпоследовательностьюmoon: вmoonесть только однаn, с индексом 3, и она не может быть одновременно первой и последней буквойnoon. - Возврат значения, показывающего, достиг ли
jконцаt. Цикл часто заканчивается на этом месте независимо от того, найдена лиs; это показывает толькоi. - Забывать, что
sможет быть длиннееt. Дляabcиabнужно вернутьfalse, и цикл делает это, если останавливается, когда вtзаканчиваются символы. - Чтение
s[i]после того, какiдостиг концаs. В Python или Java такое чтение вызывает исключение, поэтому перед сравнением проверьтеi.
Частые вопросы4
Какова временная сложность задачи Is Subsequence?
Решение с двумя указателями работает за время O(n + m), где n и m — длины s и t, и использует O(1) дополнительной памяти. На практике цикл останавливается не более чем через m шагов. Построение таблицы префиксов занимает O(n × m) времени.
Почему жадный подход с двумя указателями работает для задачи Is Subsequence?
Сопоставление буквы из s с её самым ранним возможным вхождением в t оставляет максимально возможную оставшуюся часть t для остальных букв. Любое размещение, в котором используется более позднее вхождение, можно изменить, используя более раннее, не нарушая порядок, поэтому, если существует какое-либо размещение, жадный алгоритм его найдёт.
Как быстро проверить много строк на соответствие одному и тому же t?
Подготовь t один раз: для каждой буквы сохрани отсортированный список индексов, где она встречается. Чтобы разместить s[i], выполни двоичный поиск в списке этой буквы, чтобы найти первый индекс после предыдущего совпадения. Тогда каждая проверка будет выполняться за O(n log m) вместо O(m).
В чём разница между подпоследовательностью и подстрокой?
Подстрока — это блок идущих подряд букв, а подпоследовательность может пропускать буквы, если их порядок остается прежним. ace — подпоследовательность abcde, но не подстрока. Каждая подстрока является подпоследовательностью, но не наоборот.
Похожие задачи
Задачи на те же идеи. Решите две или три, и приём запомнится.
Python
def isSubsequence(s, t):
# Напишите код здесьСлучай 1
Случай 2
Случай 3
Ввод
s = "ace" t = "abcde"
Ожидается
true