A string of (, ), and * is valid if the stars can be replaced so parentheses balance. Track a [low, high] range of possible open-paren counts — if 0 is ever reachable at the end, the string is valid.
Given a string containing only (, ), and *, return true if it can be a valid parenthesis sequence. * can represent an open paren, a close paren, or an empty string. Concrete example: s="(*) " → true (replace * with ) giving (), or with empty giving () — either works). Another: s="(*)" → true. But s=")(*)" → false.
*, track the interval [low, high] of open-paren counts that are still achievable. low is the minimum (treat every * as ) or empty); high is the maximum (treat every * as (). The string is valid iff 0 falls in the reachable range at the end — i.e. low === 0 after processing everything.low = 0, high = 0 — before seeing any char, exactly 0 opens are open.(. Both bounds increase: every live scenario gains one open paren. low++; high++.). Both bounds decrease. Then clamp lowat 0 (an open count can't be negative — no valid scenario has a deficit). If high < 0, even the most optimistic scenario has more closes than opens: return false immediately.*. The wildcard widens the window: high++ (best case: use it as () and low-- (best case: use it as ) or empty). Clamp low at 0 for the same reason as step 2.low === 0. If the minimum achievable open count is 0, we can always close everything.low === 0 but forget to clamp low to 0 during the scan. Without the clamp, low can go negative, meaning the range always contains 0 and the function never returns false — even for strings like ")".You can also solve this with two stacks: one holding indices of unmatched ( and one for *. After the scan, greedily match leftover ( with * to the right of each. Both approaches are O(n) time but the stack uses O(n) space, while the range approach uses O(1).
[low=0, high=0]. We scan each character and widen or narrow the window.1function checkValidString(s: string): boolean {2▶ let low = 0; // minimum possible open-paren count3▶ let high = 0; // maximum possible open-paren count45 for (const ch of s) {6 if (ch === '(') {7 low++;8 high++;9 } else if (ch === ')') {10 low--;11 high--;12 } else { // ch === '*'13 low--; // treat '*' as ')' or empty: shrink floor14 high++; // treat '*' as '(': grow ceiling15 }16 if (high < 0) return false; // even best case has too many ')'17 if (low < 0) low = 0; // floor can't go below 018 }1920 return low === 0; // best case closes all '('21}
function checkValidString(s: string): boolean {
let low = 0; // minimum possible open-paren count
let high = 0; // maximum possible open-paren count
for (const ch of s) {
if (ch === '(') {
low++;
high++;
} else if (ch === ')') {
low--;
high--;
} else { // ch === '*'
low--; // treat '*' as ')' or empty: shrink floor
high++; // treat '*' as '(': grow ceiling
}
if (high < 0) return false; // even best case has too many ')'
if (low < 0) low = 0; // floor can't go below 0
}
return low === 0; // best case closes all '('
}low and high both start at 0: before seeing any character, the only reachable open-paren count is 0.(: both bounds grow by 1 (this is a forced open). For ): both shrink by 1 (forced close). For *: high grows (optimistic: treat as () and low shrinks (pessimistic: treat as ) or empty).high < 0, even the most favorable assignment of wildcards can't prevent the close count from exceeding the open count. Return false immediately.low represents the minimum feasible open-paren count. It can never be negative (no valid execution has a deficit of opens), so clamp it: if (low < 0) low = 0. Forgetting this clamp is the single most common bug.low === 0 means there exists at least one assignment of * characters that leaves zero unmatched open parens — i.e. a valid sequence. If low > 0, even the most close-heavy assignment still leaves opens unmatched.( indices and another for * indices. After scanning, pair leftover ( with a * that appears to its right. O(n) time, O(n) space.*, so it behaves identically to knowing which one to choose — but you'd lose the "empty" escape hatch. Handle by adjusting the wildcard rule in the range update.* in two passes: forward to detect where a close is needed, backward to confirm opens — then reconstruct the assignment.)(immediately sets high < 0 → false), and an unclosed (with no wildcards (low stays > 0 → false).) finds no match.function checkValidString(s: string): boolean {
let low = 0; // minimum possible open-paren count
let high = 0; // maximum possible open-paren count
for (const ch of s) {
if (ch === '(') {
low++;
high++;
} else if (ch === ')') {
low--;
high--;
} else { // ch === '*'
low--; // treat '*' as ')' or empty: shrink floor
high++; // treat '*' as '(': grow ceiling
}
if (high < 0) return false; // even best case has too many ')'
if (low < 0) low = 0; // floor can't go below 0
}
return low === 0; // best case closes all '('
}Recurse character by character, tracking the running open count; at each * branch into all three choices — (, ), or empty — and accept if any branch ends balanced.
function checkValidString(s: string): boolean {
// open = number of unmatched '(' so far. Recurse over each character.
const dfs = (i: number, open: number): boolean => {
if (open < 0) return false; // a ')' had no matching '('
if (i === s.length) return open === 0; // valid iff nothing left open
const ch = s[i];
if (ch === '(') return dfs(i + 1, open + 1);
if (ch === ')') return dfs(i + 1, open - 1);
// ch === '*': treat as '(', ')' or empty.
return dfs(i + 1, open + 1) || dfs(i + 1, open - 1) || dfs(i + 1, open);
};
return dfs(0, 0);
}* triples the branching, so a string of all stars explodes to 3ⁿ leaves. Memoizing on (i, open) collapses it to O(n²); the greedy [low, high] range does the same job in a single O(n) pass with O(1) space.| "( ) and * wildcard — is it balanced?" | [low, high] open-count range |
| wildcard can be one of several chars | track a feasible interval, not a single value |
| parentheses validity with optional chars | greedy range + high<0 early exit |
| "minimum insertions / swaps for balance" | open-count balance variable + deficit accumulator |
let low = 0, high = 0;
for (const ch of s) {
if (ch === '(') { low++; high++; }
else if (ch === ')') { low--; high--; }
else { low--; high++; } // '*'
if (high < 0) return false;
if (low < 0) low = 0;
}
return low === 0;low: minimum possible open-paren count (treating * as ) / empty). high: maximum (treating * as ().high < 0 during the scan tell you?s="(*)", what are [low, high] after processing all three characters?[low, high] approach?s=")" — what happens on the first character?return low === 0. Why not low <= 0?low === 0 at the end means?