diff --git a/DIRECTORY.md b/DIRECTORY.md index 9b1fa80b..9d7cf5ed 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -386,6 +386,8 @@ * [Test Decimal To Binary](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/stack/decimal_to_binary/test_decimal_to_binary.py) * Decode String * [Test Decode String](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/stack/decode_string/test_decode_string.py) + * Longest Valid Parentheses + * [Test Longest Valid Parentheses](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/stack/longest_valid_parentheses/test_longest_valid_parentheses.py) * Minimum String Length After Removing Substrings * [Test Min Str Length After Removing Substrings](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/stack/minimum_string_length_after_removing_substrings/test_min_str_length_after_removing_substrings.py) * Nextgreater diff --git a/algorithms/stack/longest_valid_parentheses/README.md b/algorithms/stack/longest_valid_parentheses/README.md new file mode 100644 index 00000000..c3c3a151 --- /dev/null +++ b/algorithms/stack/longest_valid_parentheses/README.md @@ -0,0 +1,85 @@ +# Longest Valid Parentheses + +You are given a string composed entirely of ‘(’ and ‘)’ characters. Your goal is to identify the longest contiguous segment (substring) within this string that represents a “well-formed” or “valid” sequence of parentheses. + +A substring is considered valid if: + +1. Every opening parenthesis ‘(’ has a corresponding closing parenthesis ‘)’. +2. The pairs of parentheses are correctly nested. + +Return the length of this longest valid substring. + +## Constraints + +- 0 <= s.length <= 3 * 10^4 +- s[i] is '(', or ')'. + +## Examples + +Example 1: +```text +Input: s = "(()" +Output: 2 +Explanation: The longest valid parentheses substring is "()". +``` + +Example 2: +```text +Input: s = ")()())" +Output: 4 +Explanation: The longest valid parentheses substring is "()()". +``` + +Example 3: +```text +Input: s = "" +Output: 0 +``` + +## Topics + +- String +- Dynamic Programming +- Stack + +## Solution + +The problem asks for the length of the longest valid (well-formed) parenthesis substring. This type of matching problem, +where you need to pair opening brackets with their corresponding closing brackets, is a classic example of a problem that +lends itself to a stack pattern. The stack’s last in, first out (LIFO) property naturally models the nesting structure of +parentheses: the last opening parenthesis must be the first one to be closed. + +The essence of this algorithm is to use the stack to keep track of the indexes of parentheses that have not yet been +matched. Instead of just pushing the characters themselves, pushing their indexes allows us to calculate lengths. The +stack maintains a “base” index at the bottom, which marks the start of a potential valid substring. When a closing +parenthesis, ‘)’, finds a matching opening parenthesis, ‘(’, (by popping its index from the stack), the length of the +newly formed valid substring is calculated as the difference between the current index and the new top of the stack. + +The following steps can be performed to implement the algorithm above: + +1. Initialize a stack, indexStack, and push an initial index of -1. This value acts as a sentinel or a “base” for the + first potential valid substring. +2. Next, iterate over the input string from the current index to the right (length of the input string), character by + character. + - **Opening parenthesis (**: If we encounter an opening parenthesis, we push its index onto the stack. + - **Closing parenthesis )**: If we encounter a closing parenthesis: + - We pop the top element from the indexStack. + - If the stack becomes empty after popping, it means the current ‘)’ does not have a matching ‘(’. In this case, we + push the current ‘)’s index onto the stack to serve as the new “base” for any future valid substrings. + - Otherwise, if the stack is not empty, this means a valid pair has been formed. The length of this valid substring + is the difference between the currentIndex and the index at the new top of the stack (which is the index right + before the start of this valid pair). Compare this length with the maximum length, currentLength, found so far, + and update it if necessary. +3. After the iteration ends, the maximum length, as recorded using max(maxLength, currentLength), is the answer. + +### Time Complexity + +The solution involves a single pass through the input string s. The loop runs exactly n times, where n is the length of +the string, and inside the loop, all operations (push, pop, top on the stack) take constant time, i.e., O(1). Therefore, +the total time complexity is dominated by the loop, resulting in O(n). + +### Space Complexity + +In the worst-case scenario, the input string, s, may consist entirely of opening parenthesis (e.g., “((((...”). In this +case, the index of every character will be pushed onto the stack, where the size of the stack can grow up to n, where n +is the length of the string. Therefore, the space complexity is O(n). diff --git a/algorithms/stack/longest_valid_parentheses/__init__.py b/algorithms/stack/longest_valid_parentheses/__init__.py new file mode 100644 index 00000000..5a519e64 --- /dev/null +++ b/algorithms/stack/longest_valid_parentheses/__init__.py @@ -0,0 +1,29 @@ +def longest_valid_parentheses(s: str) -> int: + # Stack to store indexes. We initialize it with -1 to serve as a + # sentinel value. This handles the edge case of a valid substring + # starting from index 0, allowing us to calculate its length correctly. + stack = [-1] + # Variable to store the maximum length of valid parentheses found so far. + max_length = 0 + + for index, char in enumerate(s): + # If the character is an opening parenthesis, push its index onto the stack. It represents a potential start of + # a valid sequence + if char == "(": + stack.append(index) + elif char == ")": + stack.pop() + # iF the stack is now empty, it means the current ')' has no matching '('. We push the current index to act + # as a new base or starting point for the next potential valid substring + if len(stack) == 0: + stack.append(index) + else: + # If the stack is not empty, a valid pair was just formed + # The new top of the stack holds the index of the character just 'before' the start of the current valid + # substring. The length is the difference between the current index and that 'base' index + current_length = index - stack[len(stack) - 1] + + # Update the overall maximum length if the current one is greater + max_length = max(max_length, current_length) + + return max_length diff --git a/algorithms/stack/longest_valid_parentheses/test_longest_valid_parentheses.py b/algorithms/stack/longest_valid_parentheses/test_longest_valid_parentheses.py new file mode 100644 index 00000000..dff495b9 --- /dev/null +++ b/algorithms/stack/longest_valid_parentheses/test_longest_valid_parentheses.py @@ -0,0 +1,30 @@ +import unittest +from parameterized import parameterized +from algorithms.stack.longest_valid_parentheses import longest_valid_parentheses + +LONGEST_VALID_PARENTHESES_TEST_CASES = [ + ("(()", 2), + (")()())", 4), + ("", 0), + ("())()", 2), + ("(())", 4), + (")(", 0), + ("(", 0), + ("()", 2), + ("))(((", 0), + ("(()())", 6), + (")()((()))((((", 8), + ("()(()))", 6), + ("(()))())(", 4), +] + + +class LongestValidParenthesesTestCase(unittest.TestCase): + @parameterized.expand(LONGEST_VALID_PARENTHESES_TEST_CASES) + def test_longest_valid_parentheses(self, s: str, expected: int): + actual = longest_valid_parentheses(s) + self.assertEqual(expected, actual) + + +if __name__ == "__main__": + unittest.main()