Самая длинная подстрока, являющаяся валидной скобочной последовательностью
Описание задачи #
Напишите функцию для поиска самой длинной подстроки, являющейся правильной скобочной последовательностью.
Входные данные: строка s.
Выходные данные: длина самой большой подстроки, являющейся валидной скобочной последовательностью.
Ограничения #
- Длина каждой строки от 0 до 30000 символов
- Строка состоит из символов
(и)
Примеры #
-
Входные данные:
s = "(()"Ответ:
2Пояснения: Самая длинная правильная скобочная последовательность -
() -
Входные данные:
s = ")()())"Ответ:
4Пояснения: Самая длинная правильная скобочная последовательность -
()() -
Входные данные:
s = ""Ответ:
0
Решение через скользящее окно #
Первая идея решения задачи, как обычно, — это брутфорс. Поскольку нам нужно найти самую длинную подстроку, мы можем начать с подстрок максимальной длины и постепенно переходить к более коротким, пока не найдем первую подходящую подстроку и не вернем её длину.
Для этого нам поможет метод скользящего окна: устанавливаем левую границу на первый элемент, а правую — на длину текущей подстроки. Двигаем окно слева направо, одновременно увеличивая левую и правую границы. Если не будет найдено ни одной подходящей подстроки заданной длины, уменьшаем длину на единицу и снова начинаем проверять все возможные подстроки.
Проверку на валидность можно реализовать с помощью простого счетчика (так как по условиям задачи у нас только один вид скобок) или же с использованием стека.
Реализация #
-
package longest_valid_parentheses func longestValidParenthesesWindowing(s string) int { return recursiveLongestValidParenthesesWindowing(s, len(s)) } func recursiveLongestValidParenthesesWindowing(s string, substrLen int) int { if substrLen == 0 { return 0 } left := 0 right := substrLen for right <= len(s) { if isValidParenthesesByCounter(s[left:right]) { return right - left } left++ right++ } return recursiveLongestValidParenthesesWindowing(s, substrLen-1) } func isValidParenthesesByCounter(s string) bool { counter := 0 for _, char := range s { if char == '(' { counter++ continue } if counter == 0 { return false } counter-- } return counter == 0 } -
export function longestValidParenthesesWindowing(s: string): number { return recursiveLongestValidParenthesesWindowing(s, s.length) } function recursiveLongestValidParenthesesWindowing(s: string, substrLen: number): number { if (substrLen === 0) { return 0; } let left = 0 let right = substrLen while (right <= s.length) { if (isValidParenthesesByCounter(s.substring(left, right))) { return right - left } left++ right++ } return recursiveLongestValidParenthesesWindowing(s, substrLen - 1); } function isValidParenthesesByCounter(s: string): boolean { let counter = 0 for (const char of s) { if (char === '(') { counter++ continue } if (counter === 0) { return false } counter-- } return counter === 0 }
Оценка сложности #
По времени
Сложность по времени равна O(n^2), поскольку на каждом уровне рекурсии выполняется линейный проход по строке, и таких
уровней n.
По памяти
Основным фактором сложности по памяти является рекурсивная глубина.
Поскольку на каждом уровне рекурсии используется константное количество дополнительной памяти, общая пространственная
сложность функции составляет O(n).
Важно понимать, что это крайне неоптимальный алгоритм. Например, в JS он вызовет ошибку переполнения стека.
Решение через стек #
На самом деле задача решается достаточно легко: максимальная валидная скобочная последовательность — это максимальное расстояние между невалидными подстроками.
То есть, если мы сможем «вырезать» из строки валидные скобочные последовательности, сохранив индексы элементов, то самая большая разница между двумя соседними индексами и будет искомым числом. В этом нам поможет структура данных стек.
Для решения задачи мы будем идти посимвольно по исходной строке:
- Если текущий символ — открывающая скобка, помещаем индекс символа в стек.
- Если текущий символ — закрывающая скобка, достаем из стека последний индекс. Если при этом стек становится пустым, добавляем текущий индекс в стек, так как потенциально текущая позиция может служить началом для следующей подстроки. Если же стек остается непустым, вычисляем разницу между текущим индексом и индексом на вершине стека (чтобы не нужно было дополнительно проходиться по стеку).
Исходно в стек помещаем число -1, которое служит для обработки случая, когда вся строка является корректной подстрокой скобок.
Реализация #
-
package longest_valid_parentheses func longestValidParenthesesStack(s string) int { stack := make([]int, 0, len(s)) stack = append(stack, -1) res := 0 for i := 0; i < len(s); i++ { if s[i] == '(' { stack = append(stack, i) continue } stack = stack[:len(stack)-1] if len(stack) == 0 { stack = append(stack, i) continue } res = max(res, i-stack[len(stack)-1]) } return res } -
export function longestValidParenthesesStack(s: string): number { let stack: number[] = [-1]; let res = 0; for (let i = 0; i < s.length; i++) { if (s[i] === '(') { stack.push(i); continue; } stack.pop(); if (stack.length === 0) { stack.push(i); continue; } res = Math.max(res, i - stack[stack.length - 1]); } return res; }
Оценка сложности #
По времени
Для этого решения нам необходимо один раз пройтись по переданной нам строке. То есть сложность по
времени — O(n).
По памяти
В этом решении мы выделили дополнительную память на стек.
В худшем случае в стек попадут все символы строки, то есть сложность по памяти — O(n).