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.
Company Tags
Please upgrade to NeetCode Pro to view company tags.
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)
classSolution{public:boolcheckValidString(string s){returndfs(0,0, s);}private:booldfs(int i,int open,const string& s){if(open <0)returnfalse;if(i == s.size())return open ==0;if(s[i]=='('){returndfs(i +1, open +1, s);}elseif(s[i]==')'){returndfs(i +1, open -1, s);}else{returndfs(i +1, open, s)||dfs(i +1, open +1, s)||dfs(i +1, open -1, s);}}};
classSolution{/**
* @param {string} s
* @return {boolean}
*/checkValidString(s){functiondfs(i, open){if(open <0)returnfalse;if(i === s.length)return open ===0;if(s[i]==='('){returndfs(i +1, open +1);}elseif(s[i]===')'){returndfs(i +1, open -1);}else{return(dfs(i +1, open)||dfs(i +1, open +1)||dfs(i +1, open -1));}}returndfs(0,0);}}
publicclassSolution{publicboolCheckValidString(string s){returnDfs(0,0, s);}privateboolDfs(int i,int open,string s){if(open <0)returnfalse;if(i == s.Length)return open ==0;if(s[i]=='('){returnDfs(i +1, open +1, s);}elseif(s[i]==')'){returnDfs(i +1, open -1, s);}else{returnDfs(i +1, open, s)||Dfs(i +1, open +1, s)||Dfs(i +1, open -1, s);}}}
funccheckValidString(s string)bool{var dfs func(i, open int)bool
dfs =func(i, open int)bool{if open <0{returnfalse}if i ==len(s){return open ==0}if s[i]=='('{returndfs(i+1, open+1)}elseif s[i]==')'{returndfs(i+1, open-1)}else{return(dfs(i+1, open)||dfs(i+1, open+1)||dfs(i+1, open-1))}}returndfs(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:
'('
')'
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 string
open: 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:
open must never go below 0
when we reach the end, we need open == 0
Algorithm
Let n = len(s).
Create a 2D memo table memo where:
memo[i][open] stores whether dfs(i, open) is true or false
classSolution{public:boolcheckValidString(string s){int n = s.size();
memo =vector<vector<int>>(n +1,vector<int>(n +1,-1));returndfs(0,0, s);}private:
vector<vector<int>> memo;booldfs(int i,int open,const string& s){if(open <0)returnfalse;if(i == s.size())return open ==0;if(memo[i][open]!=-1)return memo[i][open]==1;bool result;if(s[i]=='('){
result =dfs(i +1, open +1, s);}elseif(s[i]==')'){
result =dfs(i +1, open -1, s);}else{
result =(dfs(i +1, open, s)||dfs(i +1, open +1, s)||dfs(i +1, open -1, s));}
memo[i][open]= result ?1:0;return result;}};
classSolution{/**
* @param {string} s
* @return {boolean}
*/checkValidString(s){const n = s.length;const memo = Array.from({length: n +1},()=>Array(n +1).fill(null),);functiondfs(i, open){if(open <0)returnfalse;if(i === n)return open ===0;if(memo[i][open]!==null)return memo[i][open];let result;if(s[i]==='('){
result =dfs(i +1, open +1);}elseif(s[i]===')'){
result =dfs(i +1, open -1);}else{
result =dfs(i +1, open)||dfs(i +1, open +1)||dfs(i +1, open -1);}
memo[i][open]= result;return result;}returndfs(0,0);}}
publicclassSolution{publicboolCheckValidString(string s){int n = s.Length;bool?[,] memo =newbool?[n +1, n +1];returnDfs(0,0, s, memo);}privateboolDfs(int i,int open,string s,bool?[,] memo){if(open <0)returnfalse;if(i == s.Length)return open ==0;if(memo[i, open].HasValue)return memo[i, open].Value;bool result;if(s[i]=='('){
result =Dfs(i +1, open +1, s, memo);}elseif(s[i]==')'){
result =Dfs(i +1, open -1, s, memo);}else{
result =Dfs(i +1, open, s, memo)||Dfs(i +1, open +1, s, memo)||Dfs(i +1, open -1, s, memo);}
memo[i, open]= result;return result;}}
funccheckValidString(s string)bool{
memo :=make([][]int,len(s)+1)for i :=range memo {
memo[i]=make([]int,len(s)+1)for j :=range memo[i]{
memo[i][j]=-1}}var dfs func(i, open int)bool
dfs =func(i, open int)bool{if open <0{returnfalse}if i ==len(s){return open ==0}if memo[i][open]!=-1{return memo[i][open]==1}
result :=falseif s[i]=='('{
result =dfs(i+1, open+1)}elseif s[i]==')'{
result =dfs(i+1, open-1)}else{
result =(dfs(i+1, open)||dfs(i+1, open+1)||dfs(i+1, open-1))}
memo[i][open]=1if!result {
memo[i][open]=0}return result
}returndfs(0,0)}
class Solution {funcheckValidString(s: String): Boolean {val memo =Array(s.length +1){IntArray(s.length +1){-1}}fundfs(i: Int,open: Int): Boolean {if(open<0)returnfalseif(i == s.length)returnopen==0if(memo[i][open]!=-1)return memo[i][open]==1val result =when(s[i]){'('->dfs(i +1,open+1)')'->dfs(i +1,open-1)else->(dfs(i +1,open)||dfs(i +1,open+1)||dfs(i +1,open-1))}
memo[i][open]=if(result)1else0return result
}returndfs(0,0)}}
classSolution{funccheckValidString(_ s:String)->Bool{let n = s.count
let sArr =Array(s)var memo =Array(
repeating:Array<Bool?>(repeating:nil, count: n +1),
count: n +1)funcdfs(_ i:Int,_open:Int)->Bool{ifopen<0{returnfalse}if i == n {returnopen==0}iflet memoized = memo[i][open]{return memoized
}let result:Boolif sArr[i]=="("{
result =dfs(i +1,open+1)}elseif sArr[i]==")"{
result =dfs(i +1,open-1)}else{
result =dfs(i +1,open)||dfs(i +1,open+1)||dfs(i +1,open-1)}
memo[i][open]= result
return result
}returndfs(0,0)}}
Time & Space Complexity
Time complexity: O(n2)
Space complexity: O(n2)
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:
'(' (increase open)
')' (decrease open)
empty (keep 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.
Algorithm
Let n = len(s).
Create a 2D table dp of size (n + 1) × (n + 1) initialized to false.
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 i from n - 1 down to 0
iterate open from 0 up to n - 1
For each state (i, open), compute whether we can match s[i]:
If s[i] == '*':
treat as '(' → check dp[i + 1][open + 1]
treat as ')' → check dp[i + 1][open - 1] (only if open > 0)
treat as empty → check dp[i + 1][open]
if any is true, set dp[i][open] = true
If s[i] == '(':
must increase open → check dp[i + 1][open + 1]
If s[i] == ')':
must decrease open → only possible if open > 0, check dp[i + 1][open - 1]
classSolution:defcheckValidString(self, s:str)->bool:
n =len(s)
dp =[[False]*(n +1)for _ inrange(n +1)]
dp[n][0]=Truefor i inrange(n -1,-1,-1):foropeninrange(n):
res =Falseif s[i]=='*':
res |= dp[i +1][open+1]ifopen>0:
res |= dp[i +1][open-1]
res |= dp[i +1][open]else:if s[i]=='(':
res |= dp[i +1][open+1]elifopen>0:
res |= dp[i +1][open-1]
dp[i][open]= res
return dp[0][0]
publicclassSolution{publicbooleancheckValidString(String s){int n = s.length();boolean[][] dp =newboolean[n +1][n +1];
dp[n][0]=true;for(int i = n -1; i >=0; i--){for(intopen=0;open< n;open++){boolean res =false;if(s.charAt(i)=='*'){
res |= dp[i +1][open+1];if(open>0) res |= dp[i +1][open-1];
res |= dp[i +1][open];}else{if(s.charAt(i)=='('){
res |= dp[i +1][open+1];}elseif(open>0){
res |= dp[i +1][open-1];}}
dp[i][open]= res;}}return dp[0][0];}}
classSolution{public:boolcheckValidString(string s){int n = s.size();
vector<vector<bool>>dp(n +1,vector<bool>(n +1,false));
dp[n][0]=true;for(int i = n -1; i >=0;--i){for(int open =0; open < n;++open){bool 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];}elseif(open >0){
res |= dp[i +1][open -1];}}
dp[i][open]= res;}}return dp[0][0];}};
classSolution{/**
* @param {string} s
* @return {boolean}
*/checkValidString(s){const n = s.length;const dp = Array.from({length: n +1},()=>Array(n +1).fill(false),);
dp[n][0]=true;for(let i = n -1; i >=0; i--){for(let open =0; open < n; open++){let 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];}elseif(open >0){
res ||= dp[i +1][open -1];}}
dp[i][open]= res;}}return dp[0][0];}}
publicclassSolution{publicboolCheckValidString(string s){int n = s.Length;bool[,] dp =newbool[n +1, n +1];
dp[n,0]=true;for(int i = n -1; i >=0; i--){for(int open =0; open < n; open++){bool 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];}elseif(open >0){
res |= dp[i +1, open -1];}}
dp[i, open]= res;}}return dp[0,0];}}
funccheckValidString(s string)bool{
n :=len(s)
dp :=make([][]bool, n+1)for i :=range dp {
dp[i]=make([]bool, n+1)}
dp[n][0]=truefor i := n -1; i >=0; i--{for open :=0; open < n; open++{
res :=falseif s[i]=='*'{
res = dp[i+1][open+1]if open >0{
res = res || dp[i+1][open-1]}
res = res || dp[i+1][open]}else{if s[i]=='('{
res = dp[i+1][open+1]}elseif open >0{
res = dp[i+1][open-1]}}
dp[i][open]= res
}}return dp[0][0]}
class Solution {funcheckValidString(s: String): Boolean {val n = s.length
val dp =Array(n +1){BooleanArray(n +1)}
dp[n][0]=truefor(i in n -1 downTo 0){for(openin0 until n){var res =falseif(s[i]=='*'){
res = dp[i +1][open+1]if(open>0){
res = res || dp[i +1][open-1]}
res = res || dp[i +1][open]}else{if(s[i]=='('){
res = dp[i +1][open+1]}elseif(open>0){
res = dp[i +1][open-1]}}
dp[i][open]= res
}}return dp[0][0]}}
classSolution{funccheckValidString(_ s:String)->Bool{let n = s.count
var dp =Array(repeating:Array(repeating:false, count: n +1), count: n +1)
dp[n][0]=truelet chars =Array(s)for i instride(from: n -1, through:0, by:-1){foropenin0..<n {var res =falseif chars[i]=="*"{
res = res || dp[i +1][open+1]ifopen>0{
res = res || dp[i +1][open-1]}
res = res || dp[i +1][open]}else{if chars[i]=="("{
res = res || dp[i +1][open+1]}elseifopen>0{
res = res || dp[i +1][open-1]}}
dp[i][open]= res
}}return dp[0][0]}}
Time & Space Complexity
Time complexity: O(n2)
Space complexity: O(n2)
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 whether s[i:] can be valid with open unmatched '('
But notice something important:
to compute values for position i, we only need values from position i + 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 at i + 1
new_dp[open] represents the answer for the suffix starting at i
Algorithm
Let n = len(s).
Create a boolean array dp of size n + 1:
dp[open] represents whether s[i+1:] can be valid with open unmatched '('
Initialize the base case (when we are past the end of the string):
dp[0] = true (empty string is valid if open == 0)
all other dp[open] are false
Iterate i from n - 1 down to 0:
Create a fresh array new_dp of size n + 1 initialized to false
For each possible open from 0 to n - 1, update based on s[i]:
If s[i] == '*', we try all three options:
treat as '(' → check dp[open + 1]
treat as ')' → check dp[open - 1] (only if open > 0)
treat as empty → check dp[open]
set new_dp[open] to true if any option works
If s[i] == '(':
it increases the unmatched count → check dp[open + 1]
If s[i] == ')':
it decreases the unmatched count → only possible if open > 0, check dp[open - 1]
After filling new_dp, assign dp = new_dp
The final answer is dp[0] (start from the beginning with zero unmatched '(')
classSolution:defcheckValidString(self, s:str)->bool:
left =[]
star =[]for i, ch inenumerate(s):if ch =='(':
left.append(i)elif ch =='*':
star.append(i)else:ifnot left andnot star:returnFalseif left:
left.pop()else:
star.pop()while left and star:if left.pop()> star.pop():returnFalsereturnnot left
publicclassSolution{publicbooleancheckValidString(String s){Stack<Integer> left =newStack<>();Stack<Integer> star =newStack<>();for(int i =0; i < s.length(); i++){char ch = s.charAt(i);if(ch =='('){
left.push(i);}elseif(ch =='*'){
star.push(i);}else{if(left.isEmpty()&& star.isEmpty())returnfalse;if(!left.isEmpty()){
left.pop();}else{
star.pop();}}}while(!left.isEmpty()&&!star.isEmpty()){if(left.pop()> star.pop())returnfalse;}return left.isEmpty();}}
classSolution{public:boolcheckValidString(string s){
stack<int> left, star;for(int i =0; i < s.size();++i){if(s[i]=='('){
left.push(i);}elseif(s[i]=='*'){
star.push(i);}else{if(left.empty()&& star.empty())returnfalse;if(!left.empty()){
left.pop();}else{
star.pop();}}}while(!left.empty()&&!star.empty()){if(left.top()> star.top())returnfalse;
left.pop();
star.pop();}return left.empty();}};
classSolution{/**
* @param {string} s
* @return {boolean}
*/checkValidString(s){const left =[];const star =[];for(let i =0; i < s.length; i++){const ch = s[i];if(ch ==='('){
left.push(i);}elseif(ch ==='*'){
star.push(i);}else{if(left.length ===0&& star.length ===0){returnfalse;}if(left.length >0){
left.pop();}else{
star.pop();}}}while(left.length >0&& star.length >0){if(left.pop()> star.pop())returnfalse;}return left.length ===0;}}
publicclassSolution{publicboolCheckValidString(string s){Stack<int> left =newStack<int>();Stack<int> star =newStack<int>();for(int i =0; i < s.Length; i++){char ch = s[i];if(ch =='('){
left.Push(i);}elseif(ch =='*'){
star.Push(i);}else{if(left.Count ==0&& star.Count ==0)returnfalse;if(left.Count >0){
left.Pop();}else{
star.Pop();}}}while(left.Count >0&& star.Count >0){if(left.Pop()> star.Pop())returnfalse;}return left.Count ==0;}}
funccheckValidString(s string)bool{var left, star []intfor i, ch :=range s {if ch =='('{
left =append(left, i)}elseif ch =='*'{
star =append(star, i)}else{iflen(left)==0&&len(star)==0{returnfalse}iflen(left)>0{
left = left[:len(left)-1]}else{
star = star[:len(star)-1]}}}forlen(left)>0&&len(star)>0{if left[len(left)-1]> star[len(star)-1]{returnfalse}
left = left[:len(left)-1]
star = star[:len(star)-1]}returnlen(left)==0}
class Solution {funcheckValidString(s: String): Boolean {val left = ArrayDeque<Int>()val star = ArrayDeque<Int>()for((i, ch)in s.withIndex()){when(ch){'('-> left.addLast(i)'*'-> star.addLast(i)')'->{if(left.isEmpty()&& star.isEmpty())returnfalseif(left.isNotEmpty()) left.removeLast()else star.removeLast()}}}while(left.isNotEmpty()&& star.isNotEmpty()){if(left.last()> star.last())returnfalse
left.removeLast()
star.removeLast()}return left.isEmpty()}}
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)
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 = 0
leftMax = 0
Traverse the string character by character:
For each character c:
If c == '(':
increment both leftMin and leftMax
If c == ')':
decrement both leftMin and leftMax
If c == '*':
treat it flexibly:
leftMin -= 1 (as ')')
leftMax += 1 (as '(')
If leftMax < 0 at any point:
return false (too many closing parentheses)
If leftMin < 0:
reset leftMin = 0 (we can treat extra closings as empty)
classSolution{funccheckValidString(_ s:String)->Bool{var leftMin =0var leftMax =0for c in s {if c =="("{
leftMin +=1
leftMax +=1}elseif c ==")"{
leftMin -=1
leftMax -=1}else{
leftMin -=1
leftMax +=1}if leftMax <0{returnfalse}if leftMin <0{
leftMin =0}}return leftMin ==0}}
Time & Space Complexity
Time complexity: O(n)
Space complexity: O(1)
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.