678. Valid Parenthesis String
Problem Description
You are given a string s
that contains only three types of characters: '('
, ')'
, and '*'
. Your task is to determine if the string can be considered valid according to specific rules.
A valid string must satisfy these conditions:
- Every left parenthesis
'('
must have a corresponding right parenthesis')'
that comes after it - Every right parenthesis
')'
must have a corresponding left parenthesis'('
that comes before it - The parentheses must be properly ordered (left before right)
The special character '*'
adds flexibility - it can be treated as:
- A left parenthesis
'('
- A right parenthesis
')'
- An empty string (essentially ignored)
You need to return true
if there exists at least one way to interpret all the '*'
characters such that the resulting string has valid parentheses matching, or false
otherwise.
For example:
"()"
is valid (simple matching)"(*)"
is valid (*
can be empty)"(*))"
is valid (*
can be'('
)")(*"
is invalid (no valid interpretation exists)
Intuition
The key insight is to think about this problem in terms of checking if substrings can form valid parentheses sequences. Since we need to match parentheses and handle wildcards, we can break down the problem into smaller subproblems.
Consider any substring s[i...j]
. For this substring to be valid, we need to think about what makes a parentheses string valid:
- A single
'*'
can be valid (as an empty string) - If we have
'('
at positioni
and')'
at positionj
, and everything in between is valid, then the whole substring is valid - We can split the substring at some point
k
where both partss[i...k]
ands[k+1...j]
are independently valid
This naturally leads to a dynamic programming approach where dp[i][j]
represents whether substring s[i...j]
can be made valid.
For the base case, a single character substring s[i][i]
is only valid if it's a '*'
(which can be treated as empty).
For larger substrings, we have two scenarios:
- The substring forms a pair:
s[i]
can be'('
(actual or'*'
acting as one) ands[j]
can be')'
(actual or'*'
acting as one), with the middle part being valid - The substring can be split into two valid parts at some position
k
By building up from smaller substrings to larger ones, we can determine if the entire string s[0...n-1]
is valid. The trick is recognizing that we need to check all possible ways a substring could be valid - either as a matched pair with valid content inside, or as a concatenation of two valid substrings.
Learn more about Stack, Greedy and Dynamic Programming patterns.
Solution Approach
We implement a bottom-up dynamic programming solution where dp[i][j]
represents whether the substring s[i...j]
can form a valid parentheses string.
Initialization:
- Create a 2D boolean array
dp
of sizen × n
wheren
is the length of the string - Set base cases:
dp[i][i] = True
only ifs[i] == '*'
(a single'*'
can be treated as empty)
Building the DP Table: We fill the table from smaller substrings to larger ones by iterating:
i
fromn-2
down to0
(starting position)j
fromi+1
ton-1
(ending position)
For each substring s[i...j]
, we check two conditions:
-
Matched Pair Pattern:
- Check if
s[i]
can be'('
(either actual'('
or'*'
) - Check if
s[j]
can be')'
(either actual')'
or'*'
) - If both are true, check if the middle part is valid:
- If
i+1 == j
(adjacent characters), it's automatically valid - Otherwise, check
dp[i+1][j-1]
(the substring inside the pair)
- If
- Check if
-
Split Pattern:
- Try splitting the substring at every possible position
k
wherei ≤ k < j
- Check if both parts are valid:
dp[i][k]
anddp[k+1][j]
- If any split results in two valid parts, the whole substring is valid
- Try splitting the substring at every possible position
The expression dp[i][j] = dp[i][j] or any(dp[i][k] and dp[k+1][j] for k in range(i, j))
implements this splitting check efficiently.
Final Result:
Return dp[0][n-1]
, which tells us whether the entire string can be made valid.
Time Complexity: O(n³)
- We have O(n²)
states in our DP table, and for each state, we potentially check O(n)
split positions.
Space Complexity: O(n²)
- For storing the DP table.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the solution with the string s = "(*))"
Step 1: Initialize the DP table
- String length n = 4
- Create a 4×4 DP table initialized to False
- Set base cases:
dp[0][0] = True
(s[0]='(' is not ''),dp[1][1] = True
(s[1]='' can be empty),dp[2][2] = False
(s[2]=')'),dp[3][3] = False
(s[3]=')')
Initial DP table:
0 1 2 3 0 [False True ? ? ] 1 [ - True ? ? ] 2 [ - - False ? ] 3 [ - - - False]
Step 2: Fill length-2 substrings (i to i+1)
-
dp[0][1]
: Check substring "(∗)"- Matched pair: s[0]='(' can be '(', s[1]='*' can be ')', adjacent → valid ✓
dp[0][1] = True
-
dp[1][2]
: Check substring "∗)"- Matched pair: s[1]='*' can be '(', s[2]=')' is ')', adjacent → valid ✓
dp[1][2] = True
-
dp[2][3]
: Check substring "))"- Matched pair: s[2]=')' cannot be '(', fails ✗
- Split at k=2: dp[2][2]=False, so fails ✗
dp[2][3] = False
Step 3: Fill length-3 substrings
-
dp[0][2]
: Check substring "(∗)"- Matched pair: s[0]='(' can be '(', s[2]=')' is ')', check middle dp[1][1]=True → valid ✓
dp[0][2] = True
-
dp[1][3]
: Check substring "∗))"- Matched pair: s[1]='*' can be '(', s[3]=')' is ')', check middle dp[2][2]=False → fails ✗
- Split at k=1: dp[1][1]=True AND dp[2][3]=False → fails ✗
- Split at k=2: dp[1][2]=True AND dp[3][3]=False → fails ✗
dp[1][3] = False
Step 4: Fill length-4 substring (entire string)
dp[0][3]
: Check substring "(∗))"- Matched pair: s[0]='(' can be '(', s[3]=')' is ')', check middle dp[1][2]=True → valid ✓
dp[0][3] = True
Final DP table:
0 1 2 3 0 [False True True True] 1 [ - True True False] 2 [ - - False False] 3 [ - - - False]
Result: dp[0][3] = True
, so the string "(∗))" is valid.
The wildcard '*' at position 1 can be interpreted as '(' to make the string "(())" which is valid.
Solution Implementation
1class Solution:
2 def checkValidString(self, s: str) -> bool:
3 """
4 Check if a string with parentheses and wildcards can be valid.
5 '*' can be treated as '(', ')', or empty string.
6
7 Args:
8 s: Input string containing '(', ')', and '*'
9
10 Returns:
11 True if the string can form valid parentheses, False otherwise
12 """
13 n = len(s)
14
15 # dp[i][j] represents whether substring s[i:j+1] can be valid
16 # Initialize a 2D DP table with False values
17 dp = [[False] * n for _ in range(n)]
18
19 # Base case: single character substrings
20 # Only '*' can be valid (as empty string)
21 for i, char in enumerate(s):
22 dp[i][i] = (char == '*')
23
24 # Fill the DP table for substrings of increasing length
25 # Process from right to left for starting position
26 for i in range(n - 2, -1, -1):
27 # Process from left to right for ending position
28 for j in range(i + 1, n):
29 # Case 1: Check if s[i] and s[j] can form a matching pair
30 # s[i] can be '(' or '*' (acting as '(')
31 # s[j] can be ')' or '*' (acting as ')')
32 # The substring between them should be valid (or empty)
33 is_matching_pair = (
34 s[i] in '(*' and
35 s[j] in '*)' and
36 (i + 1 == j or dp[i + 1][j - 1])
37 )
38
39 # Case 2: Try to split the substring at any position k
40 # Both parts should be independently valid
41 can_split = any(
42 dp[i][k] and dp[k + 1][j]
43 for k in range(i, j)
44 )
45
46 # Substring is valid if either case works
47 dp[i][j] = is_matching_pair or can_split
48
49 # Return whether the entire string can be valid
50 return dp[0][n - 1]
51
1class Solution {
2 /**
3 * Checks if a string containing '(', ')', and '*' characters is valid.
4 * '*' can represent '(', ')', or an empty string.
5 * A valid string has balanced parentheses.
6 *
7 * @param s the input string to validate
8 * @return true if the string can be made valid, false otherwise
9 */
10 public boolean checkValidString(String s) {
11 int length = s.length();
12
13 // dp[i][j] represents whether substring s[i..j] can be valid
14 boolean[][] dp = new boolean[length][length];
15
16 // Base case: single character substrings
17 // Only '*' can be valid as it can represent an empty string
18 for (int i = 0; i < length; i++) {
19 dp[i][i] = s.charAt(i) == '*';
20 }
21
22 // Fill the DP table for substrings of increasing length
23 // Process from right to left for start index
24 for (int startIndex = length - 2; startIndex >= 0; startIndex--) {
25 // Process from left to right for end index
26 for (int endIndex = startIndex + 1; endIndex < length; endIndex++) {
27 char startChar = s.charAt(startIndex);
28 char endChar = s.charAt(endIndex);
29
30 // Check if current substring can form a valid pair
31 // First and last characters must be able to form '(' and ')'
32 boolean canFormPair = (startChar == '(' || startChar == '*') &&
33 (endChar == '*' || endChar == ')');
34
35 // For a valid pair, either it's just two characters,
36 // or the middle substring must also be valid
37 boolean isValidPair = canFormPair &&
38 (startIndex + 1 == endIndex || dp[startIndex + 1][endIndex - 1]);
39
40 dp[startIndex][endIndex] = isValidPair;
41
42 // If not valid as a single group, try splitting into two valid substrings
43 for (int splitPoint = startIndex; splitPoint < endIndex && !dp[startIndex][endIndex]; splitPoint++) {
44 // Check if we can split at position k into two valid parts
45 dp[startIndex][endIndex] = dp[startIndex][splitPoint] &&
46 dp[splitPoint + 1][endIndex];
47 }
48 }
49 }
50
51 // Return whether the entire string is valid
52 return dp[0][length - 1];
53 }
54}
55
1class Solution {
2public:
3 bool checkValidString(string s) {
4 int n = s.size();
5
6 // dp[i][j] represents whether substring s[i...j] can form a valid parentheses string
7 // where '*' can be treated as '(', ')' or empty string
8 vector<vector<bool>> dp(n, vector<bool>(n, false));
9
10 // Base case: single character can only be valid if it's '*' (treated as empty)
11 for (int i = 0; i < n; ++i) {
12 dp[i][i] = (s[i] == '*');
13 }
14
15 // Fill the dp table for substrings of increasing length
16 // Process from bottom to top, left to right in the dp table
17 for (int i = n - 2; i >= 0; --i) {
18 for (int j = i + 1; j < n; ++j) {
19 char leftChar = s[i];
20 char rightChar = s[j];
21
22 // Case 1: Check if s[i] and s[j] can form a matching pair "()"
23 // s[i] can be '(' if it's '(' or '*'
24 // s[j] can be ')' if it's ')' or '*'
25 // The middle part (if exists) must also be valid
26 bool canFormPair = (leftChar == '(' || leftChar == '*') &&
27 (rightChar == ')' || rightChar == '*') &&
28 (i + 1 == j || dp[i + 1][j - 1]);
29
30 dp[i][j] = canFormPair;
31
32 // Case 2: Try to split the substring at different positions
33 // If we can find a split point k where both parts are valid,
34 // then the whole substring is valid
35 for (int k = i; k < j && !dp[i][j]; ++k) {
36 dp[i][j] = dp[i][k] && dp[k + 1][j];
37 }
38 }
39 }
40
41 // Return whether the entire string can form valid parentheses
42 return dp[0][n - 1];
43 }
44};
45
1function checkValidString(s: string): boolean {
2 const n: number = s.length;
3
4 // dp[i][j] represents whether substring s[i...j] can form a valid parentheses string
5 // where '*' can be treated as '(', ')' or empty string
6 const dp: boolean[][] = Array(n).fill(null).map(() => Array(n).fill(false));
7
8 // Base case: single character can only be valid if it's '*' (treated as empty)
9 for (let i = 0; i < n; i++) {
10 dp[i][i] = (s[i] === '*');
11 }
12
13 // Fill the dp table for substrings of increasing length
14 // Process from bottom to top, left to right in the dp table
15 for (let i = n - 2; i >= 0; i--) {
16 for (let j = i + 1; j < n; j++) {
17 const leftChar: string = s[i];
18 const rightChar: string = s[j];
19
20 // Case 1: Check if s[i] and s[j] can form a matching pair "()"
21 // s[i] can be '(' if it's '(' or '*'
22 // s[j] can be ')' if it's ')' or '*'
23 // The middle part (if exists) must also be valid
24 const canFormPair: boolean = (leftChar === '(' || leftChar === '*') &&
25 (rightChar === ')' || rightChar === '*') &&
26 (i + 1 === j || dp[i + 1][j - 1]);
27
28 dp[i][j] = canFormPair;
29
30 // Case 2: Try to split the substring at different positions
31 // If we can find a split point k where both parts are valid,
32 // then the whole substring is valid
33 for (let k = i; k < j && !dp[i][j]; k++) {
34 dp[i][j] = dp[i][k] && dp[k + 1][j];
35 }
36 }
37 }
38
39 // Return whether the entire string can form valid parentheses
40 return dp[0][n - 1];
41}
42
Time and Space Complexity
Time Complexity: O(n³)
The algorithm uses dynamic programming with a 2D table where dp[i][j]
represents whether the substring from index i
to j
can form a valid parentheses string.
- The outer loop iterates from
n-2
to0
, giving usO(n)
iterations - The middle loop iterates from
i+1
ton
, which in the worst case isO(n)
iterations - Inside the nested loops, there's an
any()
function that iterates through all possible split pointsk
fromi
toj-1
, which isO(n)
in the worst case - The operations inside (checking
dp[i][k]
anddp[k+1][j]
) areO(1)
Therefore, the total time complexity is O(n) × O(n) × O(n) = O(n³)
Space Complexity: O(n²)
- The algorithm creates a 2D DP table
dp
of sizen × n
, which requiresO(n²)
space - The recursive call stack is not used as this is an iterative approach
- Other variables like
i
,j
,k
use constant spaceO(1)
Therefore, the total space complexity is O(n²)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Base Case Handling for Empty Strings
A critical pitfall is not properly handling the edge case when the input string is empty. The current implementation will crash with an IndexError
when s = ""
because it tries to access dp[0][n-1]
where n = 0
.
Why this happens: Empty strings are technically valid (no unmatched parentheses), but the code assumes at least one character exists.
Solution:
def checkValidString(self, s: str) -> bool:
n = len(s)
# Handle empty string edge case
if n == 0:
return True
# Rest of the implementation remains the same...
2. Misunderstanding the DP State Definition
Developers often misinterpret what dp[i][j]
represents, thinking it means "substring from index i to index j" when it actually means "substring from index i to index j inclusive". This can lead to off-by-one errors when checking adjacent characters.
The issue: When checking if i+1 == j
, developers might think this means a substring of length 1, but it actually means two adjacent characters.
Correct understanding:
dp[i][i]
= single character at position idp[i][i+1]
= two characters from position i to i+1- When
i+1 == j
, we have exactly two characters (positions i and j)
3. Inefficient Split Checking
Using any()
with a generator expression for split checking can be less efficient than necessary because it doesn't short-circuit properly in some Python implementations.
More efficient approach:
# Instead of:
can_split = any(dp[i][k] and dp[k + 1][j] for k in range(i, j))
# Use explicit loop with early termination:
can_split = False
for k in range(i, j):
if dp[i][k] and dp[k + 1][j]:
can_split = True
break
dp[i][j] = is_matching_pair or can_split
4. Not Considering All Valid Two-Character Combinations
A subtle pitfall is forgetting that certain two-character combinations like "**"
are valid (both stars can be empty), but the current logic correctly handles this through the split pattern check.
Key insight: The code correctly handles this because when checking dp[i][j]
where both s[i]
and s[j]
are '*'
, it will:
- Try the matching pair pattern (one
*
as'('
, other as')'
) - Try the split pattern where each
*
can independently be empty
This redundancy ensures all valid interpretations are covered.
Which data structure is used in a depth first search?
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!