678. Valid Parenthesis String - Explanation
Description
You are given a string s which contains only three types of characters: '(', ')' and '*'.
Return true if s is valid, otherwise return false.
A string is valid if it follows all of the following rules:
- Every left parenthesis
'('must have a corresponding right parenthesis')'. - Every right parenthesis
')'must have a corresponding left parenthesis'('. - Left parenthesis
'('must go before the corresponding right parenthesis')'. - A
'*'could be treated as a right parenthesis')'character or a left parenthesis'('character, or as an empty string"".
Example 1:
Input: s = "((**)"
Output: trueExplanation: One of the '*' could be a ')' and the other could be an empty string.
Example 2:
Input: s = "(((*)"
Output: falseExplanation: The string is not valid because there is an extra '(' at the beginning, regardless of the extra '*'.
Constraints:
1 <= s.length <= 100
Topics
Recommended Time & Space Complexity
You should aim for a solution as good or better than O(n) time and O(n) space, where n is the length of the input string.
Hint 1
A brute force approach would try all possibilities when encountering a '*' and recursively solve the problem, leading to an exponential approach. Can you think of a better way? Maybe a data structure commonly used in parenthesis problems could be useful.
Hint 2
We can solve the problem using a stack-based approach. We maintain two stacks: one for tracking the indices of left parentheses and another for star indices. As we iterate through the string from left to right, we push indices onto their respective stacks when encountering a left parenthesis '(' or a star '*'. Can you determine the logic for the right parentesis case?
Hint 3
If the left parenthesis stack is not empty, we pop from it. Otherwise, we pop from the star stack, treating the star as a left parenthesis to keep the string valid. After iterating the string, the stacks might be non-empty? Can you determine the logic for this case?
Hint 4
Now, we try to match the remaining left parentheses with stars, ensuring the stars appear after the left parentheses in the string. We simultaneously pop from both stacks, and if the index of a left parenthesis is greater than that of a star, the string is invalid as there is no matching right parenthesis. In this case, we return false.
Prerequisites
Before attempting this problem, you should be comfortable with:
- Recursion - The brute force approach explores all possible interpretations using recursive backtracking
- Dynamic Programming (Memoization) - Optimizing the recursive solution by caching subproblem results
- Stack Data Structure - Used in one approach to track unmatched parentheses and wildcards
- Greedy Algorithms - The optimal solution uses a greedy approach to track min/max possible open counts
1. Recursion
Intuition
This problem asks whether a string containing '(', ')', and '*' can be interpreted as a valid parentheses string.
The tricky part is '*', because it can represent:
'('')'- an empty string
''
Using recursion, we try all valid interpretations of the string while keeping track of how many opening parentheses are currently unmatched.
The recursive function answers:
"Is it possible to make the substring starting at index i valid, given that we currently have open unmatched '('?"
Important rules:
- The number of open parentheses (
open) should never be negative - At the end of the string, all open parentheses must be closed (
open == 0)
Algorithm
- Define a recursive function
dfs(i, open):iis the current index in the stringopenis the number of unmatched'('seen so far
- If
open < 0:- Too many closing parentheses, return
false
- Too many closing parentheses, return
- If
ireaches the end of the string:- Return
trueonly ifopen == 0
- Return
- If
s[i] == '(':- Recurse with
dfs(i + 1, open + 1)
- Recurse with
- If
s[i] == ')':- Recurse with
dfs(i + 1, open - 1)
- Recurse with
- If
s[i] == '*':- Try all three possibilities:
- treat
'*'as empty →dfs(i + 1, open) - treat
'*'as'('→dfs(i + 1, open + 1) - treat
'*'as')'→dfs(i + 1, open - 1)
- treat
- If any option returns
true, returntrue
- Try all three possibilities:
- Start the recursion with
dfs(0, 0) - Return the final result
class Solution:
def checkValidString(self, s: str) -> bool:
def dfs(i, open):
if open < 0:
return False
if i == len(s):
return open == 0
if s[i] == '(':
return dfs(i + 1, open + 1)
elif s[i] == ')':
return dfs(i + 1, open - 1)
else:
return (dfs(i + 1, open) or
dfs(i + 1, open + 1) or
dfs(i + 1, open - 1))
return dfs(0, 0)Time & Space Complexity
- Time complexity:
- Space complexity:
2. Dynamic Programming (Top-Down)
Intuition
We need to decide if a string containing '(', ')', and '*' can be turned into a valid parentheses string.
The character '*' is flexible and can act as:
'('')'- empty
''
A brute-force recursion tries all possibilities, but it repeats the same work for the same positions and open counts.
To avoid that, we use top-down dynamic programming (memoization).
We track two things:
i: where we are in the stringopen: how many'('are currently unmatched
The function dfs(i, open) answers:
"Can the substring s[i:] be made valid if we currently have open unmatched opening parentheses?"
Rules:
openmust never go below0- when we reach the end, we need
open == 0
Algorithm
- Let
n = len(s). - Create a 2D memo table
memowhere:memo[i][open]stores whetherdfs(i, open)istrueorfalse
- Define a recursive function
dfs(i, open):- If
open < 0, returnfalse(too many')') - If
i == n, returntrueonly ifopen == 0 - If
memo[i][open]is already computed, return it
- If
- Transition based on
s[i]:- If
'(': move forward withopen + 1 - If
')': move forward withopen - 1 - If
'*': try all three options:- treat as empty →
dfs(i + 1, open) - treat as
'('→dfs(i + 1, open + 1) - treat as
')'→dfs(i + 1, open - 1) - if any option works, the result is
true
- treat as empty →
- If
- Store the result in
memo[i][open]and return it - Start with
dfs(0, 0)and return the final answer
class Solution:
def checkValidString(self, s: str) -> bool:
n = len(s)
memo = [[None] * (n + 1) for _ in range(n + 1)]
def dfs(i, open):
if open < 0:
return False
if i == n:
return open == 0
if memo[i][open] is not None:
return memo[i][open]
if s[i] == '(':
result = dfs(i + 1, open + 1)
elif s[i] == ')':
result = dfs(i + 1, open - 1)
else:
result = (dfs(i + 1, open) or
dfs(i + 1, open + 1) or
dfs(i + 1, open - 1))
memo[i][open] = result
return result
return dfs(0, 0)Time & Space Complexity
- Time complexity:
- Space complexity:
3. Dynamic Programming (Bottom-Up)
Intuition
We need to check if a string containing '(', ')', and '*' can be interpreted as a valid parentheses string.
A helpful way to solve this is to track how many opening parentheses are currently unmatched:
open= number of'('we still need to close
The tricky character is '*', because it can act as:
'('(increaseopen)')'(decreaseopen)- empty (keep
openthe same)
In bottom-up DP, we build answers for smaller suffixes first.
We define:
dp[i][open]= whether it is possible to makes[i:]valid if we currently haveopenunmatched'('
We fill this table from the end of the string back to the start.
Algorithm
- Let
n = len(s). - Create a 2D table
dpof size(n + 1) × (n + 1)initialized tofalse. - Base case:
dp[n][0] = true
(when we are past the end of the string, it is valid only if there are no unmatched'(')
- Fill the table backwards:
- iterate
ifromn - 1down to0 - iterate
openfrom0up ton - 1
- iterate
- For each state
(i, open), compute whether we can matchs[i]:- If
s[i] == '*':- treat as
'('→ checkdp[i + 1][open + 1] - treat as
')'→ checkdp[i + 1][open - 1](only ifopen > 0) - treat as empty → check
dp[i + 1][open] - if any is
true, setdp[i][open] = true
- treat as
- If
s[i] == '(':- must increase open → check
dp[i + 1][open + 1]
- must increase open → check
- If
s[i] == ')':- must decrease open → only possible if
open > 0, checkdp[i + 1][open - 1]
- must decrease open → only possible if
- If
- The final answer is
dp[0][0]:- starting from the beginning with
0unmatched'('
- starting from the beginning with
- Return
dp[0][0]
class Solution:
def checkValidString(self, s: str) -> bool:
n = len(s)
dp = [[False] * (n + 1) for _ in range(n + 1)]
dp[n][0] = True
for i in range(n - 1, -1, -1):
for open in range(n):
res = False
if s[i] == '*':
res |= dp[i + 1][open + 1]
if open > 0:
res |= dp[i + 1][open - 1]
res |= dp[i + 1][open]
else:
if s[i] == '(':
res |= dp[i + 1][open + 1]
elif open > 0:
res |= dp[i + 1][open - 1]
dp[i][open] = res
return dp[0][0]Time & Space Complexity
- Time complexity:
- Space complexity:
4. Dynamic Programming (Space Optimized)
Intuition
We need to check if a string containing '(', ')', and '*' can be interpreted as a valid parentheses string.
A common DP idea is to track how many opening parentheses are currently unmatched:
open= number of'('that still need to be closed
In the 2D bottom-up DP version:
dp[i][open]told us whethers[i:]can be valid withopenunmatched'('
But notice something important:
- to compute values for position
i, we only need values from positioni + 1
So we don’t need the full 2D table. We can keep just one 1D array for the “next row” and build a new one for the “current row”.
Here:
dp[open]represents the answer for the suffix starting ati + 1new_dp[open]represents the answer for the suffix starting ati
Algorithm
- Let
n = len(s). - Create a boolean array
dpof sizen + 1:dp[open]represents whethers[i+1:]can be valid withopenunmatched'('
- Initialize the base case (when we are past the end of the string):
dp[0] = true(empty string is valid ifopen == 0)- all other
dp[open]arefalse
- Iterate
ifromn - 1down to0:- Create a fresh array
new_dpof sizen + 1initialized tofalse
- Create a fresh array
- For each possible
openfrom0ton - 1, update based ons[i]:- If
s[i] == '*', we try all three options:- treat as
'('→ checkdp[open + 1] - treat as
')'→ checkdp[open - 1](only ifopen > 0) - treat as empty → check
dp[open] - set
new_dp[open]totrueif any option works
- treat as
- If
s[i] == '(':- it increases the unmatched count → check
dp[open + 1]
- it increases the unmatched count → check
- If
s[i] == ')':- it decreases the unmatched count → only possible if
open > 0, checkdp[open - 1]
- it decreases the unmatched count → only possible if
- If
- After filling
new_dp, assigndp = new_dp - The final answer is
dp[0](start from the beginning with zero unmatched'(') - Return
dp[0]
class Solution:
def checkValidString(self, s: str) -> bool:
n = len(s)
dp = [False] * (n + 1)
dp[0] = True
for i in range(n - 1, -1, -1):
new_dp = [False] * (n + 1)
for open in range(n):
if s[i] == '*':
new_dp[open] = (dp[open + 1] or
(open > 0 and dp[open - 1]) or
dp[open])
elif s[i] == '(':
new_dp[open] = dp[open + 1]
elif open > 0:
new_dp[open] = dp[open - 1]
dp = new_dp
return dp[0]Time & Space Complexity
- Time complexity:
- Space complexity:
5. Stack
Intuition
We want to check whether a string containing '(', ')', and '*' can be interpreted as a valid parentheses string.
The character '*' is flexible and can act as:
'('')'- or an empty string
A stack-based approach works well because:
- parentheses validity depends on order
'*'can be used later to fix mismatches if needed
The key idea is to:
- keep track of indices of unmatched
'(' - keep track of indices of
'*' - use
'*'as a backup when we encounter an unmatched')'
At the end, we must also ensure that any remaining '(' can be matched with a '*' that appears after it.
Algorithm
- Initialize two stacks:
left→ stores indices of'('star→ stores indices of'*'
- Traverse the string from left to right:
- For each character:
- If
'(':- push its index into
left
- push its index into
- If
'*':- push its index into
star
- push its index into
- If
')':- If
leftis not empty:- pop one
'('fromleftto match this')'
- pop one
- Else if
staris not empty:- pop one
'*'and treat it as'('
- pop one
- Else:
- no way to match this
')', returnfalse
- no way to match this
- If
- If
- After processing all characters:
- Some
'('may still be unmatched
- Some
- Try to match remaining
'('with'*':- While both stacks are non-empty:
- pop one index from
leftand one fromstar - if the
'('index is greater than the'*'index:'*'appears before'('and cannot act as')'- return
false
- pop one index from
- While both stacks are non-empty:
- If there are still unmatched
'('left:- return
false
- return
- Otherwise:
- return
true
- return
class Solution:
def checkValidString(self, s: str) -> bool:
left = []
star = []
for i, ch in enumerate(s):
if ch == '(':
left.append(i)
elif ch == '*':
star.append(i)
else:
if not left and not star:
return False
if left:
left.pop()
else:
star.pop()
while left and star:
if left.pop() > star.pop():
return False
return not leftTime & Space Complexity
- Time complexity:
- Space complexity:
6. Greedy
Intuition
We want to check whether a string containing '(', ')', and '*' can be interpreted as a valid parentheses string.
Instead of trying all possibilities or using stacks, we can solve this greedily by tracking a range of possible unmatched '(' counts.
Think of it this way:
- At any point, we don’t need the exact number of open parentheses
- We only need to know the minimum and maximum number of
'('that could be open
Why this works:
'('always increases the number of open parentheses')'always decreases it'*'is flexible and can:- decrease open count (act as
')') - increase open count (act as
'(') - keep it unchanged (act as empty)
- decrease open count (act as
So we maintain:
leftMin→ the minimum possible number of unmatched'('leftMax→ the maximum possible number of unmatched'('
If at any point the maximum possible opens becomes negative, the string is invalid.
At the end, if the minimum possible opens is zero, the string can be valid.
Algorithm
- Initialize two counters:
leftMin = 0leftMax = 0
- Traverse the string character by character:
- For each character
c:- If
c == '(':- increment both
leftMinandleftMax
- increment both
- If
c == ')':- decrement both
leftMinandleftMax
- decrement both
- If
c == '*':- treat it flexibly:
leftMin -= 1(as')')leftMax += 1(as'(')
- treat it flexibly:
- If
- If
leftMax < 0at any point:- return
false(too many closing parentheses)
- return
- If
leftMin < 0:- reset
leftMin = 0(we can treat extra closings as empty)
- reset
- After processing the entire string:
- return
trueifleftMin == 0, elsefalse
- return
class Solution:
def checkValidString(self, s: str) -> bool:
leftMin, leftMax = 0, 0
for c in s:
if c == "(":
leftMin, leftMax = leftMin + 1, leftMax + 1
elif c == ")":
leftMin, leftMax = leftMin - 1, leftMax - 1
else:
leftMin, leftMax = leftMin - 1, leftMax + 1
if leftMax < 0:
return False
if leftMin < 0:
leftMin = 0
return leftMin == 0Time & Space Complexity
- Time complexity:
- Space complexity:
Common Pitfalls
Forgetting That Wildcards Have Three Options
A common mistake is treating '*' as only representing '(' or ')'. Remember that '*' can also represent an empty string. This third option is crucial for cases like "(*)" where the '*' should be treated as empty to form a valid string.
Not Checking for Negative Open Count
When processing ')' or treating '*' as ')', you must ensure the open parenthesis count never goes negative. A negative count means there are more closing parentheses than opening ones at that point, which is invalid regardless of what comes later. Always check open >= 0 before proceeding.
Ignoring the Position of Wildcards in Stack Approach
In the stack-based solution, simply counting unmatched '(' and '*' is not enough. You must also verify that each remaining '(' has a '*' that appears after it (at a higher index). A '*' appearing before a '(' cannot act as a matching ')' for it.
Sign in to join the discussion