Facebook Pixel

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 ')' characters
  • locked: 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 character s[i] is locked and cannot be changed
  • If locked[i] = '0', the character s[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 both A and B are valid)
  • It can be written as (A) where A 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 = "))()))" and locked = "010100", you can change s[1] to '(' and s[4] to '(' to get "()()()", which is valid, so return true
  • If s = "()" and locked = "11", both characters are locked and the string is already valid, so return true
  • If s = ")" and locked = "0", even though you can change the character, a single character cannot form a valid parentheses string, so return false
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. Every '(' can find a matching ')' to its right
  2. 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.

Learn more about Stack and Greedy patterns.

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 increment x. These positions can potentially serve as opening parentheses.
  • When we encounter a locked ')' (where s[i] == ')' and locked[i] == '1'), we need to match it:
    • If x > 0, we have available opening parentheses or unlocked positions to match with, so we decrement x
    • If x == 0, there's no way to match this ')', so we return false

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 increment x. 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 decrement x
    • If x == 0, there's no way to match this '(', so we return false

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 Evaluator

Example 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:

is[i]locked[i]Actionx after
0')''0'Unlocked, can be '('x = 1
1')''1'Locked ')', need matchx = 0
2'(''0'Unlocked, can be '('x = 1
3')''1'Locked ')', need matchx = 0
4')''0'Unlocked, can be '('x = 1
5')''1'Locked ')', need matchx = 0

First pass succeeds! Every locked ')' found a potential match.

Second Pass (Right to Left): We track available closing parentheses with counter x = 0:

is[i]locked[i]Actionx after
5')''1'Locked ')', availablex = 1
4')''0'Unlocked, can be ')'x = 2
3')''1'Locked ')', availablex = 3
2'(''0'Unlocked, can be ')'x = 4
1')''1'Locked ')', availablex = 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 to n-1 (forward direction)
  • Second pass: iterates from index n-1 to 0 (backward direction)

Each pass visits every character exactly once, performing constant-time operations (O(1)) at each position:

  • Checking character values (s[i] and locked[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.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Depth first search is equivalent to which of the tree traversal order?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More