2116. Check if a Parentheses String Can Be Valid
Problem Description
You are given two strings of equal length n
:
s
: a parentheses string containing only'('
and')'
characterslocked
: a binary string containing only'0'
and'1'
characters
The locked
string determines which characters in s
can be modified:
- If
locked[i] = '1'
, the characters[i]
is locked and cannot be changed - If
locked[i] = '0'
, the characters[i]
is unlocked and can be changed to either'('
or')'
A valid parentheses string must satisfy one of these conditions:
- It is exactly
"()"
- It can be formed by concatenating two valid parentheses strings (like
AB
where bothA
andB
are valid) - It can be written as
(A)
whereA
is a valid parentheses string
Your task is to determine if it's possible to modify the unlocked characters in s
to make it a valid parentheses string. Return true
if it's possible, false
otherwise.
For example:
- If
s = "))()))"
andlocked = "010100"
, you can changes[1]
to'('
ands[4]
to'('
to get"()()()"
, which is valid, so returntrue
- If
s = "()"
andlocked = "11"
, both characters are locked and the string is already valid, so returntrue
- If
s = ")"
andlocked = "0"
, even though you can change the character, a single character cannot form a valid parentheses string, so returnfalse
Intuition
First, we need to recognize that any valid parentheses string must have an even length. Each opening parenthesis needs a closing one to match with it, so if the length is odd, we can immediately return false
.
The key insight is that we need to ensure two things:
- Every
'('
can find a matching')'
to its right - Every
')'
can find a matching'('
to its left
For unlocked positions (where locked[i] = '0'
), we have flexibility - we can choose either '('
or ')'
. This means an unlocked position can serve as either type of parenthesis when needed.
Let's think about scanning from left to right. As we go through the string, we count how many unmatched '('
we have. When we see a '('
or an unlocked position, we can potentially use it as an opening parenthesis, so we increment our counter. When we see a ')'
, we need to match it with a previous '('
or unlocked position. If we have any available (counter > 0), we decrement the counter. If we don't have any available, it means this ')'
cannot be matched, so we return false
.
But wait - this only ensures that every ')'
has a potential match to its left. What about ensuring every '('
has a match to its right?
That's where the second pass comes in. We do the same process but from right to left, treating ')'
and unlocked positions as potentially available closing parentheses. This ensures that every '('
can find a matching ')'
to its right.
If both passes succeed, we know that:
- All
')'
characters can be matched with'('
or unlocked positions to their left - All
'('
characters can be matched with')'
or unlocked positions to their right - The unlocked positions can be assigned appropriately to balance everything out
This greedy two-pass approach works because we're essentially checking if there's enough "flexibility" in both directions to create a valid matching.
Solution Approach
The implementation uses a greedy two-pass approach with a simple counter variable to track unmatched parentheses.
Step 1: Check for odd length
if n & 1: return False
We first check if the length is odd using bitwise AND (n & 1
). If the last bit is 1, the number is odd, and we immediately return false
since valid parentheses strings must have even length.
Step 2: First pass (left to right)
x = 0
for i in range(n):
if s[i] == '(' or locked[i] == '0':
x += 1
elif x:
x -= 1
else:
return False
We traverse from left to right with a counter x
:
- When we encounter a
'('
or an unlocked position (locked[i] == '0'
), we incrementx
. These positions can potentially serve as opening parentheses. - When we encounter a locked
')'
(wheres[i] == ')'
andlocked[i] == '1'
), we need to match it:- If
x > 0
, we have available opening parentheses or unlocked positions to match with, so we decrementx
- If
x == 0
, there's no way to match this')'
, so we returnfalse
- If
Step 3: Second pass (right to left)
x = 0
for i in range(n - 1, -1, -1):
if s[i] == ')' or locked[i] == '0':
x += 1
elif x:
x -= 1
else:
return False
We traverse from right to left with the same logic but reversed:
- When we encounter a
')'
or an unlocked position, we incrementx
. These can serve as closing parentheses. - When we encounter a locked
'('
, we need to match it:- If
x > 0
, we have available closing parentheses or unlocked positions to match with, so we decrementx
- If
x == 0
, there's no way to match this'('
, so we returnfalse
- If
Step 4: Return true
If both passes complete without returning false
, it means all parentheses can be properly matched, and we return true
.
The algorithm uses O(n)
time complexity for the two passes through the string and O(1)
space complexity as we only use a single counter variable. The greedy approach works because we're maximizing flexibility at each step - treating unlocked positions as wildcards that can be either type of parenthesis as needed.
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 example where s = "))()))"
and locked = "010100"
.
Initial Setup:
- String length n = 6 (even, so we can proceed)
- We can modify positions 1 and 4 (where locked[i] = '0')
First Pass (Left to Right):
We track available opening parentheses with counter x = 0
:
i | s[i] | locked[i] | Action | x after |
---|---|---|---|---|
0 | ')' | '0' | Unlocked, can be '(' | x = 1 |
1 | ')' | '1' | Locked ')', need match | x = 0 |
2 | '(' | '0' | Unlocked, can be '(' | x = 1 |
3 | ')' | '1' | Locked ')', need match | x = 0 |
4 | ')' | '0' | Unlocked, can be '(' | x = 1 |
5 | ')' | '1' | Locked ')', need match | x = 0 |
First pass succeeds! Every locked ')' found a potential match.
Second Pass (Right to Left):
We track available closing parentheses with counter x = 0
:
i | s[i] | locked[i] | Action | x after |
---|---|---|---|---|
5 | ')' | '1' | Locked ')', available | x = 1 |
4 | ')' | '0' | Unlocked, can be ')' | x = 2 |
3 | ')' | '1' | Locked ')', available | x = 3 |
2 | '(' | '0' | Unlocked, can be ')' | x = 4 |
1 | ')' | '1' | Locked ')', available | x = 5 |
0 | ')' | '0' | Unlocked, can be ')' | x = 6 |
Second pass also succeeds! No locked '(' needed matching (there were none).
Result: Both passes succeed, so we return true
.
What actually happens: The unlocked positions at indices 1 and 4 can be changed to '(' to create "()()()"
, which is valid. Our algorithm detected this was possible without explicitly constructing the solution.
Solution Implementation
1class Solution:
2 def canBeValid(self, s: str, locked: str) -> bool:
3 # Get the length of the string
4 n = len(s)
5
6 # If the string has odd length, it cannot form valid parentheses
7 if n & 1: # Bitwise AND with 1 checks if n is odd
8 return False
9
10 # First pass: left to right
11 # Check if we can match all closing parentheses
12 open_count = 0
13 for i in range(n):
14 # Count opening parentheses or unlocked positions (which can become opening)
15 if s[i] == '(' or locked[i] == '0':
16 open_count += 1
17 # For closing parentheses, try to match with previous opening
18 elif open_count > 0:
19 open_count -= 1
20 else:
21 # Too many closing parentheses that cannot be matched
22 return False
23
24 # Second pass: right to left
25 # Check if we can match all opening parentheses
26 close_count = 0
27 for i in range(n - 1, -1, -1):
28 # Count closing parentheses or unlocked positions (which can become closing)
29 if s[i] == ')' or locked[i] == '0':
30 close_count += 1
31 # For opening parentheses, try to match with previous closing
32 elif close_count > 0:
33 close_count -= 1
34 else:
35 # Too many opening parentheses that cannot be matched
36 return False
37
38 # Both passes succeeded, the string can be made valid
39 return True
40
1class Solution {
2 public boolean canBeValid(String s, String locked) {
3 int length = s.length();
4
5 // A valid parentheses string must have even length
6 if (length % 2 == 1) {
7 return false;
8 }
9
10 // First pass: left to right
11 // Check if we can match all closing parentheses with opening ones
12 int openAvailable = 0;
13 for (int i = 0; i < length; i++) {
14 // Count characters that can act as opening parentheses
15 // Either it's already '(' or it's unlocked (can be changed)
16 if (s.charAt(i) == '(' || locked.charAt(i) == '0') {
17 openAvailable++;
18 } else {
19 // This is a locked closing parenthesis ')'
20 // Try to match it with an available opening
21 if (openAvailable > 0) {
22 openAvailable--;
23 } else {
24 // No opening parenthesis available to match this closing one
25 return false;
26 }
27 }
28 }
29
30 // Second pass: right to left
31 // Check if we can match all opening parentheses with closing ones
32 int closeAvailable = 0;
33 for (int i = length - 1; i >= 0; i--) {
34 // Count characters that can act as closing parentheses
35 // Either it's already ')' or it's unlocked (can be changed)
36 if (s.charAt(i) == ')' || locked.charAt(i) == '0') {
37 closeAvailable++;
38 } else {
39 // This is a locked opening parenthesis '('
40 // Try to match it with an available closing
41 if (closeAvailable > 0) {
42 closeAvailable--;
43 } else {
44 // No closing parenthesis available to match this opening one
45 return false;
46 }
47 }
48 }
49
50 // Both passes succeeded, the string can be made valid
51 return true;
52 }
53}
54
1class Solution {
2public:
3 bool canBeValid(string s, string locked) {
4 int n = s.size();
5
6 // A valid parentheses string must have even length
7 if (n & 1) {
8 return false;
9 }
10
11 // First pass: left to right
12 // Check if we can match all closing parentheses
13 int openAvailable = 0;
14 for (int i = 0; i < n; ++i) {
15 // Count positions that can serve as opening parentheses
16 // Either it's already '(' or it's unlocked (can be changed)
17 if (s[i] == '(' || locked[i] == '0') {
18 ++openAvailable;
19 }
20 // It's a locked closing parenthesis, try to match it
21 else if (openAvailable > 0) {
22 --openAvailable;
23 }
24 // Cannot match this closing parenthesis
25 else {
26 return false;
27 }
28 }
29
30 // Second pass: right to left
31 // Check if we can match all opening parentheses
32 int closeAvailable = 0;
33 for (int i = n - 1; i >= 0; --i) {
34 // Count positions that can serve as closing parentheses
35 // Either it's already ')' or it's unlocked (can be changed)
36 if (s[i] == ')' || locked[i] == '0') {
37 ++closeAvailable;
38 }
39 // It's a locked opening parenthesis, try to match it
40 else if (closeAvailable > 0) {
41 --closeAvailable;
42 }
43 // Cannot match this opening parenthesis
44 else {
45 return false;
46 }
47 }
48
49 return true;
50 }
51};
52
1/**
2 * Checks if a parentheses string can be made valid by flipping unlocked characters
3 * @param s - The parentheses string containing '(' and ')' characters
4 * @param locked - Binary string where '0' means unlocked (can flip) and '1' means locked
5 * @returns true if the string can be made valid, false otherwise
6 */
7function canBeValid(s: string, locked: string): boolean {
8 const length: number = s.length;
9
10 // Odd length strings cannot form valid parentheses
11 if (length & 1) {
12 return false;
13 }
14
15 // First pass: left to right
16 // Check if we can balance the string by treating unlocked chars as '(' when needed
17 let openCount: number = 0;
18 for (let i = 0; i < length; ++i) {
19 // Count this position as an opening if it's already '(' or if it's unlocked
20 if (s[i] === '(' || locked[i] === '0') {
21 ++openCount;
22 } else if (openCount > 0) {
23 // It's a locked ')', decrease the open count
24 --openCount;
25 } else {
26 // Too many locked ')' without matching '(' or unlocked positions
27 return false;
28 }
29 }
30
31 // Second pass: right to left
32 // Check if we can balance the string by treating unlocked chars as ')' when needed
33 let closeCount: number = 0;
34 for (let i = length - 1; i >= 0; --i) {
35 // Count this position as a closing if it's already ')' or if it's unlocked
36 if (s[i] === ')' || locked[i] === '0') {
37 ++closeCount;
38 } else if (closeCount > 0) {
39 // It's a locked '(', decrease the close count
40 --closeCount;
41 } else {
42 // Too many locked '(' without matching ')' or unlocked positions
43 return false;
44 }
45 }
46
47 return true;
48}
49
Time and Space Complexity
Time Complexity: O(n)
The algorithm performs two separate passes through the string:
- First pass: iterates from index
0
ton-1
(forward direction) - Second pass: iterates from index
n-1
to0
(backward direction)
Each pass visits every character exactly once, performing constant-time operations (O(1)
) at each position:
- Checking character values (
s[i]
andlocked[i]
) - Incrementing or decrementing the counter
x
- Conditional checks
Total time = O(n) + O(n) = O(n)
, where n
is the length of the input string.
Space Complexity: O(1)
The algorithm uses only a fixed amount of extra space:
- Variable
n
to store the string length - Variable
x
as a counter (reused for both passes) - Variable
i
for loop iteration
No additional data structures are created that scale with the input size. The space usage remains constant regardless of the input string length, resulting in O(1)
space complexity.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Thinking One Pass is Sufficient
The Mistake: A common misconception is that checking only from left to right would be sufficient. You might think that if you can match all parentheses going left to right, the string is valid.
Why It's Wrong: Consider s = "(("
with locked = "00"
. A left-to-right pass would succeed (counting two potential opening parentheses), but the string cannot be made valid because there's no way to create matching closing parentheses.
The Solution: Both passes are essential:
- Left-to-right ensures no excess closing parentheses appear before their matching opening ones
- Right-to-left ensures no excess opening parentheses appear after their potential closing ones
Pitfall 2: Incorrect Handling of Unlocked Positions
The Mistake: Treating unlocked positions as fixed characters or trying to decide their type too early in the algorithm.
# WRONG approach - trying to fix unlocked positions prematurely
def canBeValid(self, s: str, locked: str) -> bool:
# Convert unlocked positions to what we "think" they should be
result = list(s)
for i in range(len(s)):
if locked[i] == '0':
# Deciding too early what the character should be
if i < len(s) // 2:
result[i] = '('
else:
result[i] = ')'
# Then check if valid...
Why It's Wrong: The optimal assignment of unlocked characters depends on the locked characters around them. For example, if you have many locked ')'
at the beginning, you need unlocked positions there to become '('
regardless of their position.
The Solution: Treat unlocked positions as wildcards that can be either type. During the left-to-right pass, count them as potential opening parentheses. During the right-to-left pass, count them as potential closing parentheses. This maximizes flexibility.
Pitfall 3: Forgetting the Odd Length Check
The Mistake: Jumping straight into the validation logic without checking if the length is even.
# WRONG - missing length check
def canBeValid(self, s: str, locked: str) -> bool:
open_count = 0
for i in range(len(s)):
# ... validation logic
Why It's Wrong: Valid parentheses strings must have equal numbers of opening and closing parentheses, which means the total length must be even. Without this check, you might waste computation on strings that can never be valid, or worse, incorrectly return true
for odd-length strings.
The Solution: Always check if n & 1: return False
at the beginning. This is an O(1) operation that can save unnecessary computation and ensure correctness.
Depth first search is equivalent to which of the tree traversal order?
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
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!