Самая длинная подстрока, являющаяся валидной скобочной последовательностью
Описание задачи #
Напишите функцию для поиска самой длинной подстроки, являющейся правильной скобочной последовательностью.
Входные данные: строка 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)
.