Facebook Pixel

2267. Check if There Is a Valid Parentheses String Path

Problem Description

You are given an m x n matrix where each cell contains either '(' or ')'. Your task is to determine if there exists a valid path from the top-left corner (0, 0) to the bottom-right corner (m-1, n-1) that forms a valid parentheses string.

A valid parentheses string must follow these rules:

  • It can be ()
  • It can be formed by concatenating two valid parentheses strings (like AB where both A and B are valid)
  • It can be formed by wrapping a valid parentheses string with parentheses (like (A) where A is valid)

The path must satisfy these constraints:

  • Start at cell (0, 0)
  • End at cell (m-1, n-1)
  • Only move down or right at each step
  • The sequence of parentheses collected along the path forms a valid parentheses string

For example, if you traverse a path and collect "(())", this would be valid. But if you collect "(()" or ")(", these would be invalid.

Return true if such a valid path exists, otherwise return false.

The solution uses depth-first search with memoization. It tracks the balance of parentheses (count of '(' minus count of ')') as it explores paths. The key insight is that at any point, if the balance becomes negative or exceeds the remaining cells needed to reach the destination, the path cannot be valid. The path is valid only if it reaches the bottom-right corner with a balance of exactly 0.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

To form a valid parentheses string, we need equal numbers of '(' and ')', with '(' never being outnumbered by ')' at any point when reading left to right. This gives us our first insight: the total path length m + n - 1 must be even (since we need pairs of parentheses).

The key observation is tracking the "balance" - the difference between open and close parentheses seen so far. As we traverse the path:

  • When we see '(', the balance increases by 1
  • When we see ')', the balance decreases by 1
  • The balance must never go negative (that would mean more ')' than '(' at some point)
  • The balance must equal 0 when we reach the destination

We can use DFS to explore all possible paths from (0, 0) to (m-1, n-1). At each cell (i, j), we have at most two choices: go right or go down. We keep track of the current balance k as we explore.

An important optimization comes from realizing that if our current balance k exceeds the number of remaining cells to the destination (m - i + n - j), we can't possibly close all open parentheses even if all remaining cells were ')'. This allows us to prune invalid paths early.

Similarly, we can immediately reject paths where:

  • The starting cell (0, 0) contains ')' (would start with negative balance)
  • The ending cell (m-1, n-1) contains '(' (can't end with balance 0)

The memoization with @cache is crucial because many different paths might reach the same cell with the same balance, and we don't want to recompute the result for each one.

Learn more about Dynamic Programming patterns.

Solution Approach

The implementation uses Depth-First Search (DFS) with memoization to explore all valid paths while avoiding redundant computations.

Initial Validation: First, we check three conditions that would make a valid path impossible:

  • If (m + n - 1) % 2 == 1: The path length is odd, so we can't have matching parentheses
  • If grid[0][0] == ")": Starting with a close parenthesis gives negative balance
  • If grid[m-1][n-1] == "(": Ending with an open parenthesis prevents reaching balance 0

DFS Function Design: The core function dfs(i, j, k) determines if there's a valid path from position (i, j) with current balance k:

  1. Update Balance: First, we update the balance based on the current cell:

    d = 1 if grid[i][j] == "(" else -1
    k += d
  2. Pruning Conditions: We check if the current state is invalid:

    • If k < 0: More close parentheses than open ones (invalid string)
    • If k > m - i + n - j: Balance exceeds remaining cells, impossible to close all parentheses
  3. Base Case: When we reach (m-1, n-1), return True only if k == 0 (balanced parentheses)

  4. Recursive Exploration: We try moving right and down:

    for a, b in pairwise((0, 1, 0)):
        x, y = i + a, j + b

    This iterates through (0, 1) for moving right and (1, 0) for moving down.

  5. Boundary Check and Recursion: For each valid next position within bounds, we recursively check if it leads to a valid path.

Memoization with @cache: The @cache decorator stores results for each unique (i, j, k) combination. This is crucial because multiple paths might reach the same cell with the same balance, and we avoid recomputing the result.

The solution starts by calling dfs(0, 0, 0) with initial position (0, 0) and balance 0 (before processing the first cell). The algorithm efficiently explores all possible paths while pruning invalid branches early and reusing computed results through memoization.

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 a small example with this grid:

[['(', '(', ')'],
 [')', '(', ')']]

Initial Checks:

  • Grid dimensions: m=2, n=3
  • Path length: m + n - 1 = 4 (even ✓)
  • grid[0][0] = '(' (valid start ✓)
  • grid[1][2] = ')' (valid end ✓)

DFS Exploration starting from dfs(0, 0, 0):

  1. At (0,0): Cell contains '(', balance becomes 0+1=1

    • Balance 1 is valid (not negative)
    • Balance 1 ≤ remaining cells (4), so continue
    • Try two paths: right to (0,1) or down to (1,0)
  2. Path 1: (0,0) → (0,1) with balance 1

    • At (0,1): Cell contains '(', balance becomes 1+1=2
    • Balance 2 ≤ remaining cells (3), valid
    • Try: right to (0,2) or down to (1,1)
  3. Path 1a: (0,0) → (0,1) → (0,2) with balance 2

    • At (0,2): Cell contains ')', balance becomes 2-1=1
    • Balance 1 ≤ remaining cells (2), valid
    • Only option: down to (1,2)
    • At (1,2): Cell contains ')', balance becomes 1-1=0
    • Reached destination with balance 0! ✓
    • Path "(())" is valid!
  4. Path 1b: (0,0) → (0,1) → (1,1) with balance 2

    • At (1,1): Cell contains '(', balance becomes 2+1=3
    • Balance 3 > remaining cells (2)
    • Cannot close all parentheses, prune this path ✗
  5. Path 2: (0,0) → (1,0) with balance 1

    • At (1,0): Cell contains ')', balance becomes 1-1=0
    • Balance 0 ≤ remaining cells (3), valid
    • Only option: right to (1,1)
    • At (1,1): Cell contains '(', balance becomes 0+1=1
    • Only option: right to (1,2)
    • At (1,2): Cell contains ')', balance becomes 1-1=0
    • Reached destination with balance 0! ✓
    • Path "()()" is valid!

Result: The function returns True because we found valid paths. The memoization would cache results like dfs(1,1,2) = False and dfs(1,2,1) = True to avoid recomputation if these states were reached again through different paths.

Solution Implementation

1class Solution:
2    def hasValidPath(self, grid: List[List[str]]) -> bool:
3        from functools import cache
4        from typing import List
5      
6        @cache
7        def dfs(row: int, col: int, open_count: int) -> bool:
8            """
9            Depth-first search to find valid parentheses path.
10          
11            Args:
12                row: Current row position
13                col: Current column position
14                open_count: Number of unmatched opening parentheses
15          
16            Returns:
17                True if a valid path exists from current position to end
18            """
19            # Update open parentheses count based on current cell
20            delta = 1 if grid[row][col] == "(" else -1
21            open_count += delta
22          
23            # Pruning: invalid if negative count or impossible to balance
24            if open_count < 0:
25                return False
26          
27            # Maximum possible closing parentheses from current position to end
28            remaining_steps = (rows - row - 1) + (cols - col - 1)
29            if open_count > remaining_steps:
30                return False
31          
32            # Check if we reached the destination
33            if row == rows - 1 and col == cols - 1:
34                return open_count == 0
35          
36            # Try moving right and down
37            directions = [(0, 1), (1, 0)]  # right, down
38            for dx, dy in directions:
39                next_row, next_col = row + dx, col + dy
40                # Check bounds and recursively explore
41                if 0 <= next_row < rows and 0 <= next_col < cols:
42                    if dfs(next_row, next_col, open_count):
43                        return True
44          
45            return False
46      
47        rows, cols = len(grid), len(grid[0])
48      
49        # Early termination checks
50        # Path length must be even for balanced parentheses
51        if (rows + cols - 1) % 2 == 1:
52            return False
53      
54        # Start must be '(' and end must be ')'
55        if grid[0][0] == ")" or grid[rows - 1][cols - 1] == "(":
56            return False
57      
58        # Start DFS from top-left corner with 0 open parentheses
59        return dfs(0, 0, 0)
60
1class Solution {
2    // Grid dimensions
3    private int rows, cols;
4    // The input grid containing parentheses
5    private char[][] grid;
6    // 3D visited array: [row][col][balance]
7    // Tracks if we've visited position (row, col) with a specific balance value
8    private boolean[][][] visited;
9
10    /**
11     * Determines if there's a valid path from top-left to bottom-right
12     * where parentheses are balanced.
13     * 
14     * @param grid 2D array containing '(' and ')' characters
15     * @return true if a valid balanced path exists, false otherwise
16     */
17    public boolean hasValidPath(char[][] grid) {
18        rows = grid.length;
19        cols = grid[0].length;
20      
21        // Early termination checks:
22        // 1. Path length must be even for balanced parentheses
23        // 2. Must start with '(' and end with ')'
24        if ((rows + cols - 1) % 2 == 1 || 
25            grid[0][0] == ')' || 
26            grid[rows - 1][cols - 1] == '(') {
27            return false;
28        }
29      
30        this.grid = grid;
31        // Initialize visited array with maximum possible balance value
32        visited = new boolean[rows][cols][rows + cols];
33      
34        // Start DFS from top-left corner with initial balance 0
35        return dfs(0, 0, 0);
36    }
37
38    /**
39     * Performs depth-first search to find a valid balanced path.
40     * 
41     * @param row Current row position
42     * @param col Current column position
43     * @param balance Current balance of parentheses (incremented for '(', decremented for ')')
44     * @return true if a valid path exists from current position, false otherwise
45     */
46    private boolean dfs(int row, int col, int balance) {
47        // Check if this state has been visited before
48        if (visited[row][col][balance]) {
49            return false;
50        }
51      
52        // Mark current state as visited
53        visited[row][col][balance] = true;
54      
55        // Update balance based on current cell's parenthesis
56        balance += grid[row][col] == '(' ? 1 : -1;
57      
58        // Pruning conditions:
59        // 1. Balance cannot be negative (more closing than opening parentheses)
60        // 2. Balance cannot exceed remaining cells (impossible to balance)
61        if (balance < 0 || balance > rows - row + cols - col) {
62            return false;
63        }
64      
65        // Check if we reached the destination
66        if (row == rows - 1 && col == cols - 1) {
67            // Valid path only if parentheses are balanced
68            return balance == 0;
69        }
70      
71        // Direction vectors for moving right and down
72        // dirs[0]=1, dirs[1]=0 represents moving down (row+1, col+0)
73        // dirs[1]=0, dirs[2]=1 represents moving right (row+0, col+1)
74        final int[] dirs = {1, 0, 1};
75      
76        // Try both possible moves: down and right
77        for (int d = 0; d < 2; d++) {
78            int nextRow = row + dirs[d];
79            int nextCol = col + dirs[d + 1];
80          
81            // Check if next position is within bounds and recursively explore
82            if (nextRow >= 0 && nextRow < rows && 
83                nextCol >= 0 && nextCol < cols && 
84                dfs(nextRow, nextCol, balance)) {
85                return true;
86            }
87        }
88      
89        // No valid path found from current position
90        return false;
91    }
92}
93
1class Solution {
2public:
3    bool hasValidPath(vector<vector<char>>& grid) {
4        int rows = grid.size();
5        int cols = grid[0].size();
6      
7        // Early termination: path length must be even for balanced parentheses
8        // Also check if start is ')' or end is '(' which would make balancing impossible
9        if ((rows + cols - 1) % 2 != 0 || 
10            grid[0][0] == ')' || 
11            grid[rows - 1][cols - 1] == '(') {
12            return false;
13        }
14      
15        // DP memoization: visited[row][col][balance]
16        // balance represents the current count of unmatched '(' parentheses
17        bool visited[rows][cols][rows + cols];
18        memset(visited, false, sizeof(visited));
19      
20        // Direction vectors for moving right and down
21        int directions[3] = {1, 0, 1}; // {row_delta, col_delta} pairs
22      
23        // DFS with memoization to find valid path
24        auto dfs = [&](this auto&& dfs, int row, int col, int balance) -> bool {
25            // Check if this state has been visited
26            if (visited[row][col][balance]) {
27                return false;
28            }
29            visited[row][col][balance] = true;
30          
31            // Update balance based on current cell
32            balance += (grid[row][col] == '(') ? 1 : -1;
33          
34            // Pruning: balance should never be negative (more ')' than '(')
35            // Also, balance shouldn't exceed remaining cells (impossible to balance)
36            int remaining_cells = (rows - row - 1) + (cols - col - 1);
37            if (balance < 0 || balance > remaining_cells + 1) {
38                return false;
39            }
40          
41            // Check if we reached the destination with balanced parentheses
42            if (row == rows - 1 && col == cols - 1) {
43                return balance == 0;
44            }
45          
46            // Try moving in two directions: right and down
47            for (int dir = 0; dir < 2; ++dir) {
48                int next_row = row + directions[dir];
49                int next_col = col + directions[dir + 1];
50              
51                // Check boundaries and recursively search
52                if (next_row >= 0 && next_row < rows && 
53                    next_col >= 0 && next_col < cols && 
54                    dfs(next_row, next_col, balance)) {
55                    return true;
56                }
57            }
58          
59            return false;
60        };
61      
62        // Start DFS from top-left corner with initial balance of 0
63        return dfs(0, 0, 0);
64    }
65};
66
1/**
2 * Determines if there exists a valid path from top-left to bottom-right
3 * in a grid containing only '(' and ')' characters, where the path forms
4 * a valid parentheses string.
5 * 
6 * @param grid - 2D array of strings containing only '(' or ')'
7 * @returns true if a valid parentheses path exists, false otherwise
8 */
9function hasValidPath(grid: string[][]): boolean {
10    const rows: number = grid.length;
11    const cols: number = grid[0].length;
12
13    // Early termination checks:
14    // 1. Path length must be even for balanced parentheses
15    // 2. Must start with '(' and end with ')'
16    if ((rows + cols - 1) % 2 !== 0 || 
17        grid[0][0] === ')' || 
18        grid[rows - 1][cols - 1] === '(') {
19        return false;
20    }
21
22    // 3D visited array: [row][col][balance]
23    // Tracks if we've visited position (row, col) with a specific balance count
24    const visited: boolean[][][] = Array.from({ length: rows }, () =>
25        Array.from({ length: cols }, () => 
26            Array(rows + cols).fill(false)
27        )
28    );
29
30    // Direction vectors for moving right and down
31    const directions: number[] = [1, 0, 1];
32
33    /**
34     * Depth-first search to find a valid parentheses path
35     * 
36     * @param row - Current row position
37     * @param col - Current column position
38     * @param balance - Current balance of parentheses (count of unmatched '(')
39     * @returns true if a valid path exists from current position
40     */
41    const dfs = (row: number, col: number, balance: number): boolean => {
42        // Check if this state has been visited
43        if (visited[row][col][balance]) {
44            return false;
45        }
46
47        // Mark current state as visited
48        visited[row][col][balance] = true;
49
50        // Update balance based on current cell
51        // '(' increases balance, ')' decreases it
52        balance += grid[row][col] === '(' ? 1 : -1;
53
54        // Pruning conditions:
55        // 1. Balance cannot be negative (more ')' than '(')
56        // 2. Balance cannot exceed remaining cells (impossible to balance)
57        const remainingCells: number = (rows - row - 1) + (cols - col - 1);
58        if (balance < 0 || balance > remainingCells) {
59            return false;
60        }
61
62        // Check if we reached the destination
63        if (row === rows - 1 && col === cols - 1) {
64            return balance === 0; // Valid only if perfectly balanced
65        }
66
67        // Try moving in both possible directions (right and down)
68        for (let dirIndex = 0; dirIndex < 2; dirIndex++) {
69            const nextRow: number = row + directions[dirIndex];
70            const nextCol: number = col + directions[dirIndex + 1];
71          
72            // Check bounds and recursively explore
73            if (nextRow >= 0 && nextRow < rows && 
74                nextCol >= 0 && nextCol < cols && 
75                dfs(nextRow, nextCol, balance)) {
76                return true;
77            }
78        }
79
80        return false;
81    };
82
83    // Start DFS from top-left corner with initial balance of 0
84    return dfs(0, 0, 0);
85}
86

Time and Space Complexity

Time Complexity: O(m × n × (m + n))

The time complexity is determined by the memoized DFS function. The dfs function has three parameters: i (row position from 0 to m-1), j (column position from 0 to n-1), and k (balance of parentheses). The key insight is that k represents the current balance of opening and closing parentheses, which can range from 0 to at most m + n - 1 (the maximum path length from top-left to bottom-right). Due to the @cache decorator, each unique combination of (i, j, k) is computed only once. There are m possible values for i, n possible values for j, and at most m + n possible values for k. Therefore, the total number of unique states is O(m × n × (m + n)).

Space Complexity: O(m × n × (m + n))

The space complexity comes from two sources:

  1. The memoization cache stores results for all possible states (i, j, k), which requires O(m × n × (m + n)) space
  2. The recursion stack depth in the worst case can go up to O(m + n) (the length of the longest path)

Since the cache dominates the space usage, the overall space complexity is O(m × n × (m + n)).

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Incorrect Balance Calculation Timing

A common mistake is updating the balance after checking pruning conditions or at the wrong point in the recursion. The balance must be updated immediately upon entering a cell, before any validity checks.

Incorrect approach:

def dfs(row, col, open_count):
    # Checking conditions before updating balance - WRONG!
    if open_count < 0:
        return False
  
    # Update balance after checks
    delta = 1 if grid[row][col] == "(" else -1
    open_count += delta

Correct approach:

def dfs(row, col, open_count):
    # Update balance first
    delta = 1 if grid[row][col] == "(" else -1
    open_count += delta
  
    # Then check validity
    if open_count < 0:
        return False

2. Off-by-One Error in Remaining Steps Calculation

Many implementations incorrectly calculate the remaining steps to reach the destination, forgetting that we're already at position (row, col).

Incorrect calculation:

# This counts total remaining cells including current position
remaining_steps = (rows - row) + (cols - col)

Correct calculation:

# Excludes current position, counts only moves needed
remaining_steps = (rows - row - 1) + (cols - col - 1)

3. Missing Early Termination Checks

Failing to check if a valid path is even possible before starting DFS leads to unnecessary computation.

Incomplete validation:

# Only checking path length
if (rows + cols - 1) % 2 == 1:
    return False
# Missing checks for start and end cells

Complete validation:

# Check all three conditions
if (rows + cols - 1) % 2 == 1:
    return False
if grid[0][0] == ")" or grid[rows - 1][cols - 1] == "(":
    return False

4. Incorrect Memoization Key

Using the wrong combination of parameters as the memoization key can lead to incorrect results or missed optimization opportunities.

Wrong memoization:

@cache
def dfs(row, col):  # Missing balance parameter!
    balance = calculate_balance_somehow()  # Can't track properly

Correct memoization:

@cache
def dfs(row, col, open_count):  # All state variables included
    # Process with complete state information

5. Balance vs. Open Count Confusion

Some implementations confuse "balance" (which can be negative) with "open count" (unmatched opening parentheses). While both approaches work, mixing them leads to errors.

Consistent open count approach:

# open_count represents unmatched '(' parentheses
delta = 1 if grid[row][col] == "(" else -1
open_count += delta
if open_count < 0:  # More ')' than '(' encountered
    return False

Solution to Avoid These Pitfalls:

  1. Always update the balance/count immediately upon entering a cell
  2. Carefully calculate remaining steps excluding the current position
  3. Implement all necessary pre-validation checks before starting DFS
  4. Include all state variables (row, col, balance) in the memoization decorator
  5. Be consistent with your chosen representation (balance or open count) throughout the implementation
  6. Test edge cases: single cell grid, all same parenthesis type, alternating patterns
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which algorithm is best for finding the shortest distance between two points in an unweighted graph?


Recommended Readings

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

Load More