678. Valid Parenthesis String
Problem Description
The problem is to determine if a given string s
is a valid string based on certain rules. The string s
consists of only three types of characters: '(', ')' and ''. The characters must form a valid sequence based on the following criteria: every '(' character must have a corresponding ')' character, every ')' must have a corresponding '(', the '(' must occur before its corresponding ')', and the '' character can be interpreted as either a '(', a ')', or an empty string. We need to assess whether we can rearrange the '*' characters in such a way that all the parentheses are correctly matched, and if so, we will return true; otherwise, we return false.
Intuition
Approaching the solution involves considering the flexibility that the '' character affords. The '' can potentially balance out any unmet parentheses if placed correctly, which is crucial to forming a valid string. To solve the problem, we have to deal with the ambiguity of the '*', determining when it should act as a '(', a ')', or be ignored as an empty string ' "" '.
The algorithm employs two passes to ensure that each parenthesis is paired up. In the first pass, we treat every '' as '(', increasing our "balance" or counter whenever we encounter a '(' or '', and decreasing it when we encounter a ')'. If our balance drops to zero, it means we have matched all parentheses thus far. However, if it drops below zero, there are too many ')' characters without a matching '(' or '*', so the string can't be valid.
On the second pass, we reverse the string and now treat every '' as ')'. We then perform a similar count, increasing the balance when we see a ')' or '', and decreasing it for '('. If the balance drops below zero this time, it means that there are too many '(' characters without a matching ')' or '*', and the string is again invalid.
If we complete both passes without the balance going negative, it means that for every '(' there is a corresponding ')', and the string is valid. We have effectively managed the ambiguity of '*' by checking that they can serve as a viable substitute for whichever parenthesis is needed to complete a valid set.
Our method ensures that all '(' have a corresponding ')', and vice versa, by treating '*' as placeholder for the correct parenthesis needed, thus validating the input string.
Learn more about Stack, Greedy and Dynamic Programming patterns.
Solution Approach
The reference solution approach suggests using dynamic programming to solve the problem. Dynamic programming is a method for solving complex problems by breaking them down into simpler subproblems. It is typically used when a problem has overlapping subproblems and a recursive structure that allows solutions to these subproblems to be reused.
However, the provided solution code takes a different, more efficient approach by making two single pass scans of the string: one from left to right, and another from right to left.
In the first pass (left to right), we utilize a counter x
to track the balance between the '(' and ')' characters. We increment x
when we encounter a '(' or a '', and decrement it when we encounter a ')'. If we encounter a ')' but x
is already 0
, it means there's no '(' or '' preceding it that can be used to make a valid pair, thus the string is invalid and we return false
.
x = 0 for c in s: if c in '(*': x += 1 elif x: x -= 1 else: return False
In the second pass (right to left), we reset the counter x
and treat '' as ')'. We increment x
for ')' or '', and decrement it for '('. Similar to the first pass, if we encounter a '(' but x
is 0
, it means there's no ')' or '*' to pair with it, so the string can't be valid.
x = 0 for c in s[::-1]: if c in '*)': x += 1 elif x: x -= 1 else: return False
After both scans, if our balance has never gone negative, we can conclude that for every '(' there is a matching ')' (after considering that '*' could count as either), so the string is valid.
This solution is more efficient than the dynamic programming approach mentioned in the reference solution approach. While dynamic programming would have a time complexity of O(n^3)
and a space complexity of O(n^2)
, the provided solution has a time complexity of O(n)
and a space complexity of O(1)
, since it simply uses a single integer for tracking and iterates through the string twice, without requiring any additional data structures.
Ready to land your dream job?
Unlock your dream job with a 2-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's take the string s = "(*))"
as an example to illustrate the solution approach.
First Pass (Left to Right):
We will initialize our balance counter x
to 0
. We iterate through the string one character at a time.
s[0] = '('
: Since it is a '(', we incrementx
to1
.s[1] = '*'
: Since it is a '*', it could be '(', ')', or empty. We treat it as '(', so we incrementx
to2
.s[2] = ')'
: We have a ')', so this could potentially pair with the previous '(' or '*'. We decrementx
to1
.s[3] = ')'
: Another ')'. It can pair with the '(' or '*' we assumed in step 2. We decrementx
to0
.
At the end of this pass, x
is not negative, which means that we have not encountered a ')' that could not be matched with a previous '(' or '*' treated as '('.
Second Pass (Right to Left):
Now, we will reset x
to 0
and traverse from right to left, treating '*' as ')'.
s[3] = ')'
: We incrementx
to1
since it could pair with an '(', or a '*', treated as '('.s[2] = ')'
: We incrementx
to2
—similar reasoning as step 1.s[1] = '*'
: Treating it as ')', we incrementx
to3
.s[0] = '('
: We have an '(', so we pair it with one of the ')' or '*' we treated as ')'. We decrementx
to2
.
After the second pass, x
is not negative, confirming that we have never encountered a '(' character that could not be matched with a subsequent ')' or '*' treated as ')'.
Since we finished both passes without the balance x
going negative, we can conclude that the string s = "(*))"
can be rearranged into a valid sequence. Therefore, our function would return true
for this input.
Solution Implementation
1class Solution:
2 def checkValidString(self, s: str) -> bool:
3 # Initialization of the balance counter for open parentheses.
4 open_balance = 0
5
6 # Forward iteration over string to check if it can be valid from left to right.
7 for char in s:
8 # If character is '(' or '*', it could count as a valid open parenthesis,
9 # so increase the balance counter.
10 if char in '(*':
11 open_balance += 1
12 # If the character is ')', decrease the balance counter as a ')' is closing
13 # an open parenthesis.
14 elif open_balance:
15 open_balance -= 1
16 # If there's no open parenthesis to balance the closing one, return False.
17 else:
18 return False
19
20 # Reinitialization of the balance counter for closed parentheses.
21 closed_balance = 0
22
23 # Backward iteration over string to check if it can be valid from right to left.
24 for char in s[::-1]:
25 # If character is ')' or '*', it could count as a valid closed parenthesis,
26 # so increase the closed_balance counter.
27 if char in '*)':
28 closed_balance += 1
29 # If the character is '(', decrease the closed_balance counter as '(' is
30 # potentially closing a prior ')'.
31 elif closed_balance:
32 closed_balance -= 1
33 # If there's no closing parenthesis to balance the opening one, return False.
34 else:
35 return False
36
37 # If all parentheses and asterisks can be balanced in both directions,
38 # the string is considered valid.
39 return True
40
1class Solution {
2
3 // Method to check if a string with parentheses and asterisks (*) is valid
4 public boolean checkValidString(String s) {
5 int balance = 0; // This will keep track of the balance of open parentheses
6 int n = s.length(); // Length of the input string
7
8 // First pass goes from left to right
9 for (int i = 0; i < n; ++i) {
10 char currentChar = s.charAt(i);
11 if (currentChar != ')') {
12 // Increment balance for '(' or '*'
13 ++balance;
14 } else if (balance > 0) {
15 // Decrement balance if there is an unmatched '(' before
16 --balance;
17 } else {
18 // A closing ')' without a matching '('
19 return false;
20 }
21 }
22
23 // Reset balance for the second pass
24 balance = 0;
25
26 // Second pass goes from right to left
27 for (int i = n - 1; i >= 0; --i) {
28 char currentChar = s.charAt(i);
29 if (currentChar != '(') {
30 // Increment balance for ')' or '*'
31 ++balance;
32 } else if (balance > 0) {
33 // Decrement balance if there is an unmatched ')' after
34 --balance;
35 } else {
36 // An opening '(' without a matching ')'
37 return false;
38 }
39 }
40
41 // If we did not return false so far, the string is valid
42 return true;
43 }
44}
45
1class Solution {
2public:
3 // Function to check if the string with parentheses and asterisks is valid
4 bool checkValidString(string s) {
5 int balance = 0; // Track the balance of the parentheses
6 int n = s.size(); // Store the size of the string
7
8 // Forward pass to ensure there aren't too many closing parentheses
9 for (int i = 0; i < n; ++i) {
10 // Increment balance for an opening parenthesis or an asterisk
11 if (s[i] != ')') {
12 ++balance;
13 }
14 // Decrement balance for a closing parenthesis if balance is positive
15 else if (balance > 0) {
16 --balance;
17 }
18 // If balance is zero, too many closing parentheses are encountered
19 else {
20 return false;
21 }
22 }
23
24 // If only counting opening parentheses and asterisks, balance might be positive
25 // So we check in the reverse order for the opposite scenario
26 balance = 0; // Reset balance for the backward pass
27 for (int i = n - 1; i >= 0; --i) {
28 // Increment balance for closing parenthesis or an asterisk
29 if (s[i] != '(') {
30 ++balance;
31 }
32 // Decrement balance for an opening parenthesis if balance is positive
33 else if (balance > 0) {
34 --balance;
35 }
36 // If balance is zero, too many opening parentheses are encountered
37 else {
38 return false;
39 }
40 }
41 // If the string passes both forward and backward checks, it's valid
42 return true;
43 }
44};
45
1// Global variable to track the balance of parentheses
2let balance: number;
3
4// Global variable to store the size of the string
5let n: number;
6
7// Function to check if the string with parentheses and asterisks is valid
8function checkValidString(s: string): boolean {
9 balance = 0; // Initialize balance
10 n = s.length; // Get the length of the string
11
12 // Forward pass to ensure there aren't too many closing parentheses
13 for (let i = 0; i < n; ++i) {
14 if (s[i] !== ')') {
15 // Increment balance for an opening parenthesis or an asterisk
16 balance++;
17 } else if (balance > 0) {
18 // Decrement balance for a closing parenthesis if balance is positive
19 balance--;
20 } else {
21 // If balance is zero, too many closing parentheses are encountered
22 return false;
23 }
24 }
25
26 if (balance === 0) {
27 // If balance is zero, it means we have exact matches, so return true
28 return true;
29 }
30
31 // Reset balance for the backward pass
32 balance = 0;
33 for (let i = n - 1; i >= 0; --i) {
34 if (s[i] !== '(') {
35 // Increment balance for closing parenthesis or an asterisk
36 balance++;
37 } else if (balance > 0) {
38 // Decrement balance for an opening parenthesis if balance is positive
39 balance--;
40 } else {
41 // If balance is zero, too many opening parentheses are encountered
42 return false;
43 }
44 }
45
46 // If the string passes both forward and backward checks, it's valid
47 return true;
48}
49
Time and Space Complexity
Time Complexity
The provided algorithm consists of two for-loops that scan through the string s
. Each loop runs independently from the beginning and from the end of the string, checking conditions and updating the variable x
accordingly.
The time complexity of each pass is O(n)
, where n
is the length of string s
, since each character is examined exactly once in each pass.
Since there are two passes through the string, the total time complexity of the algorithm is O(n) + O(n)
which simplifies to O(n)
.
Space Complexity
The space complexity of the algorithm depends on the additional space used by the algorithm, excluding the input itself.
In this case, only a single integer x
is used to keep track of the balance of parentheses and asterisks, which occupies constant space.
Hence, the space complexity of the algorithm is O(1)
, as it does not depend on the size of the input string s
and only uses a fixed amount of additional space.
Learn more about how to find time and space complexity quickly using problem constraints.
What data structure does Breadth-first search typically uses to store intermediate states?
Recommended Readings
Stack Intro Imagine you have a pile of books on your desk If you want to add a new book you place it on top If you want to read a book you take it from the top And if you simply want to see which book is on the top you
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Want a Structured Path to Master System Design Too? Don’t Miss This!