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:
'(' must have a corresponding right parenthesis ')'.')' must have a corresponding left parenthesis '('.'(' must go before the corresponding right parenthesis ')'.'*' 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
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.
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.
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?
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?
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.
This problem asks whether a string containing '(', ')', and '*' can be interpreted as a valid parentheses string.
The tricky part is '*', because it can represent:
'('')'''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:
open) should never be negativeopen == 0)dfs(i, open):i is the current index in the stringopen is the number of unmatched '(' seen so faropen < 0:falsei reaches the end of the string:true only if open == 0s[i] == '(':dfs(i + 1, open + 1)s[i] == ')':dfs(i + 1, open - 1)s[i] == '*':'*' as empty → dfs(i + 1, open)'*' as '(' → dfs(i + 1, open + 1)'*' as ')' → dfs(i + 1, open - 1)true, return truedfs(0, 0)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)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:
'('')'''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 unmatchedThe function dfs(i, open) answers:
"Can the substring s[i:] be made valid if we currently have open unmatched opening parentheses?"
Rules:
open must never go below 0open == 0n = len(s).memo where:memo[i][open] stores whether dfs(i, open) is true or falsedfs(i, open):open < 0, return false (too many ')')i == n, return true only if open == 0memo[i][open] is already computed, return its[i]:'(': move forward with open + 1')': move forward with open - 1'*': try all three options:dfs(i + 1, open)'(' → dfs(i + 1, open + 1)')' → dfs(i + 1, open - 1)truememo[i][open] and return itdfs(0, 0) and return the final answerclass 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)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 closeThe tricky character is '*', because it can act as:
'(' (increase open)')' (decrease open)open the same)In bottom-up DP, we build answers for smaller suffixes first.
We define:
dp[i][open] = whether it is possible to make s[i:] valid if we currently have open unmatched '('We fill this table from the end of the string back to the start.
n = len(s).dp of size (n + 1) × (n + 1) initialized to false.dp[n][0] = true'(')i from n - 1 down to 0open from 0 up to n - 1(i, open), compute whether we can match s[i]:s[i] == '*':'(' → check dp[i + 1][open + 1]')' → check dp[i + 1][open - 1] (only if open > 0)dp[i + 1][open]true, set dp[i][open] = trues[i] == '(':dp[i + 1][open + 1]s[i] == ')':open > 0, check dp[i + 1][open - 1]dp[0][0]:0 unmatched '('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]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 closedIn the 2D bottom-up DP version:
dp[i][open] told us whether s[i:] can be valid with open unmatched '('But notice something important:
i, we only need values from position i + 1So 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 at i + 1new_dp[open] represents the answer for the suffix starting at in = len(s).dp of size n + 1:dp[open] represents whether s[i+1:] can be valid with open unmatched '('dp[0] = true (empty string is valid if open == 0)dp[open] are falsei from n - 1 down to 0:new_dp of size n + 1 initialized to falseopen from 0 to n - 1, update based on s[i]:s[i] == '*', we try all three options:'(' → check dp[open + 1]')' → check dp[open - 1] (only if open > 0)dp[open]new_dp[open] to true if any option workss[i] == '(':dp[open + 1]s[i] == ')':open > 0, check dp[open - 1]new_dp, assign dp = new_dpdp[0] (start from the beginning with zero unmatched '(')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]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:
'('')'A stack-based approach works well because:
'*' can be used later to fix mismatches if neededThe key idea is to:
'(''*''*' 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.
left → stores indices of '('star → stores indices of '*''(':left'*':star')':left is not empty:'(' from left to match this ')'star is not empty:'*' and treat it as '('')', return false'(' may still be unmatched'(' with '*':left and one from star'(' index is greater than the '*' index:'*' appears before '(' and cannot act as ')'false'(' left:falsetrueclass 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 leftWe 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:
'(' that could be openWhy this works:
'(' always increases the number of open parentheses')' always decreases it'*' is flexible and can:')')'(')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.
leftMin = 0leftMax = 0c:c == '(':leftMin and leftMaxc == ')':leftMin and leftMaxc == '*':leftMin -= 1 (as ')')leftMax += 1 (as '(')leftMax < 0 at any point:false (too many closing parentheses)leftMin < 0:leftMin = 0 (we can treat extra closings as empty)true if leftMin == 0, else falseclass 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 == 0A 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.
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.
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.