Valid Parenthesis String adds a wildcard to the classic balanced-parentheses problem. The * can be one of three things, which seems to explode into too many cases — until you stop tracking a single open count and start tracking a range of possible open counts.
Problem. Given a string s containing only (, ), and *, decide whether it can be made valid.
A * may be treated as a (, a ), or an empty string. Return true if some choice of those makes
every parenthesis balanced.
Example: s = '(*))' → true. Read the * as empty (or as ( then drop one ) mentally) and the
parens balance.
The slow way first
The brute-force idea: every * has 3 choices, so try all of them and check each resulting string for balance. With k stars that is 3ᵏ strings — exponential, hopeless for a long input.
The question to ask: do I really need to commit to what each * is? No. While scanning left to right, the only thing that matters for validity is how many open parens are currently unmatched. Because * is flexible, that count is not a single number — it is a range.
The idea: track lo and hi
Walk the string once, keeping two numbers:
lo— the smallest number of open parens still possible (assume every*so far closed or vanished).hi— the largest number of open parens still possible (assume every*so far opened).
For each character: ( pushes both up, ) pulls both down, and * widens the range (lo - 1, hi + 1). If lo dips below 0 we clamp it to 0 — a * we were reading as ) can simply be read as empty instead. If hi ever goes negative there are flat-out too many ) and we fail. At the end, the string is valid exactly when lo can be 0.
The key insight: clamping lo at 0 lets the wildcards quietly "undo" an over-eager close, while hi going negative is the hard limit no choice of * can rescue.
Walk through it
Step through the animation on s = '(*))'. The pointer c scans left to right while lo and hi update beneath it. Watch lo try to go negative twice and get clamped back to 0 — that clamp is the whole trick. At the end lo == 0, so the answer is true.
Pseudocode
lo = 0 # fewest open parens still possible
hi = 0 # most open parens still possible
for each char c in s:
if c == '(': lo += 1; hi += 1
elif c == ')': lo -= 1; hi -= 1
else: lo -= 1; hi += 1 # the wildcard widens the range
if lo < 0: lo = 0 # a '*' read as ')' can be empty instead
if hi < 0: return False # too many ')', no choice saves it
return lo == 0 # valid iff 0 open is reachableThe Python solution
def check_valid_string(s):
lo = 0
hi = 0
for c in s:
if c == '(':
lo += 1
hi += 1
elif c == ')':
lo -= 1
hi -= 1
else:
lo -= 1
hi += 1
if lo < 0:
lo = 0
if hi < 0:
return False
return lo == 0loandhibracket the range of open-paren counts still achievable given the*s seen so far.(and)move both bounds together; only*widens the gap between them.- Clamping
loto 0 (line 14) reflects that a wildcard can always be read as empty instead of). hi < 0(line 16) means even reading every*as(cannot cover the)count — a true failure.return lo == 0(line 17): if 0 unmatched opens is inside the final range, some choice balances the string.
Complexity
| Case | Time | Notes |
|---|---|---|
| Brute force (try every star) | O(3^k) (moderate) | k stars, exponential |
| lo/hi range (this solution) | O(n) (moderate) | one pass, two counters |
O(1) (fast)We collapse an exponential search into a single linear pass with two integers — O(1) extra space. Instead of committing each * to a value, we carry the whole interval of possibilities at once.
When this pattern shows up
When a choice makes a running quantity ambiguous, track its range (min and max) in one pass instead of branching on every choice. The same lo/hi or low/high band trick appears in interval feasibility, greedy reachability (jump game), and "can this expression evaluate to X" problems.
Two easy mistakes: forgetting to clamp lo at 0 (it must never go negative, or you under-count what the
wildcards can fix), and checking hi < 0 to bail early. Drop either guard and the answer breaks on inputs
like '(*))' or ')'.
Practice
For s = '(*))', after processing the first ')' (the third character), lo would be -1. What do we do, and why is that allowed?
1. What do lo and hi represent during the scan?
2. How does a '*' change the range?
3. Why do we clamp lo to 0 when it goes negative?
4. When is the string valid at the end of the scan?