AlgoMaster Logo

Valid Parenthesis String

mediumFrequencyUpdated September 21, 2026

Understanding the Problem

This is the classic valid parentheses problem with one addition: the wildcard *. Without *, the answer comes from a single counter: increment on (, decrement on ), and check that it never goes negative and ends at zero.

The * complicates this because at each * position there are three choices: treat it as (, as ), or as nothing. Trying all combinations explodes exponentially. The real question is whether there exists some assignment of wildcards that makes the string valid, without enumerating every assignment.

We don't need the exact assignment. We only need to know whether the range of possible open-parenthesis counts at the end includes zero. By tracking the minimum and maximum possible count of unmatched ( at each position, validity falls out of a single pass.

Key Constraints:

  • 1 <= s.length <= 100: n is small enough that an O(n^2) DP passes, and an O(n) greedy is comfortable. The small bound is what lets the brute force and DP approaches below run in time at all.
  • s[i] is '(', ')' or '*': only three character types, so there are no other cases to handle.

Approach 1: Brute Force (Recursion)

Intuition

Try every possibility. At each *, branch into three recursive calls that treat it as (, as ), or as the empty string. For ( and ), adjust the open-parenthesis counter as usual.

If any branch reaches a valid state (counter equals zero at the end and never went negative), the string is valid. This is an exhaustive search over all wildcard assignments.

Algorithm

  1. Define a recursive function that takes the current index and the count of unmatched open parentheses.
  2. If the count ever goes negative, return false (too many closing parens).
  3. If we reach the end of the string, return true only if the count is exactly zero.
  4. For (, recurse with count + 1.
  5. For ), recurse with count - 1.
  6. For *, try all three: recurse with count + 1, count - 1, and count unchanged. Return true if any branch returns true.

Visualization and Code

Loading animation...

The state at any point is fully determined by the current index and the open-parenthesis count, yet this recursion recomputes the same (index, openCount) states many times. Caching those results removes the repeated work.

Approach 2: Dynamic Programming (Memoization)

Intuition

The recursion from Approach 1 has overlapping subproblems. The state is (index, openCount), and since index ranges from 0 to n and openCount ranges from 0 to n, there are at most O(n^2) distinct states. Caching their results removes the exponential blowup.

Each state answers one question: starting at position index with openCount unmatched open parens, can the rest of the string be completed validly? Once that question is answered, every later call with the same state reads the stored answer instead of recursing again.

Algorithm

  1. Create a memoization table (hash map or 2D array) indexed by (index, openCount).
  2. Use the same recursive logic as Approach 1.
  3. Before computing, check if the state (index, openCount) is already in the memo. If so, return the cached result.
  4. After computing, store the result in the memo before returning.

Visualization and Code

Loading animation...

The next approach replaces the table entirely. Instead of tracking every reachable open count individually, it tracks only the range of reachable open counts, which collapses the state down to two variables.

Approach 3: Greedy (Min-Max Range)

Intuition

This problem is solvable in O(n) time and O(1) space by tracking only two numbers instead of the full set of reachable open counts: the minimum and maximum possible count of unmatched open parentheses.

Without wildcards, there is exactly one open count at each position. With wildcards, there is a range, and that range stays contiguous because every reachable count between the minimum and maximum is also reachable (any single * you flipped from ( to ) shifts the total by exactly 1). So we maintain low (minimum possible open count) and high (maximum possible open count):

  • ( increases both by 1
  • ) decreases both by 1
  • * decreases low by 1 and increases high by 1

Two checks guard the range. If high drops below 0, even treating every * as ( leaves an unmatched ), so return false. And low can never represent fewer than zero unmatched opens, so we clamp it at 0. At the end, if low is 0, zero is within the range and a valid assignment exists.

Algorithm

  1. Initialize low = 0 and high = 0.
  2. For each character in the string:
    • If (: increment both low and high.
    • If ): decrement both low and high.
    • If *: decrement low by 1, increment high by 1.
    • If high < 0: return false (too many ) even with all * as ().
    • Clamp low to at least 0 (can't have negative unmatched ().
  3. Return low == 0.

Visualization and Code

Loading animation...