2267. Check if There Is a Valid Parentheses String Path
Problem Description
The given problem presents a matrix grid
filled with parentheses characters, either '(' or ')'. The task is to determine whether there exists at least one path from the top-left corner of the grid to the bottom-right corner, such that:
- The path only moves downwards or to the right, one cell at a time.
- The sequence of parentheses encountered along the path forms a valid parentheses string.
A parentheses string is considered valid if it meets any of the following conditions:
- It is an empty string or consists solely of one pair of parentheses:
()
. - It can be divided into two consecutive parts,
A
andB
, both of which are valid parentheses strings themselves. - It can be enclosed in a single pair of parentheses, with the inside being a valid parentheses string.
In simpler terms, we must find a path where the order and number of opening '(' and closing ')' parentheses balance each other correctly as specified by the valid parentheses string definition.
Intuition
To find a solution, we use a Depth-First Search (DFS) algorithm combined with memoization to avoid redundant computations for previously visited cell states. The idea is to traverse the grid by moving right or down and tracking the balance of parentheses as follows:
- Increment the balance
t
whenever an opening parenthesis '(' is encountered. - Decrement the balance
t
whenever a closing parenthesis ')' is encountered.
Since we cannot have more closing brackets before opening brackets in a valid parentheses sequence, any path where the balance becomes negative at any point is invalid and can be disregarded. While traversing, if we reach the bottom-right cell and the balance t
is zero, we have found a valid path, and we can return true
.
Through memoization with @cache
, we store the results of subproblems so that we don't recompute the validity of paths from the same cell and with the same current balance t
. By trying each possible path and checking the balance of parentheses, we can determine if a valid path exists without exploring every possible permutation exhaustively, thus saving time.
The recursive DFS function dfs(i, j, t)
tries to find a path starting from cell (i, j)
with a current parentheses balance t
. If a valid path is found at any point, the function returns true
and concludes early. Otherwise, it exhaustively searches and eventually may return false
if no valid paths exist.
Learn more about Dynamic Programming patterns.
Solution Approach
The solution to the problem utilizes a Deep-First Search (DFS) approach that is defined in the function dfs(i, j, t)
, where i
and j
represent the current cell's row and column indices in the grid, and t
is the current balance of parentheses.
Here's a step-by-step breakdown of how the solution is implemented:
-
Initialization: The function initializes by reading the dimensions of the grid into variables
m
andn
. The DFS function is then called starting from the top-left cell(0, 0)
with an initial balancet
of0
. -
DFS Function (
dfs(i, j, t)
): The core recursive function checks whether the current path is valid as it traverses the grid:-
Update Balance: We update the balance
t
by incrementing for an opening parenthesis'('
and decrementing for a closing parenthesis')'
. -
Invalid Condition: If the balance
t
is negative, we stop exploration down this path because more closing parentheses than opening ones have been encountered, making the path invalid. -
Base Case - Target Cell: If the current cell is the bottom-right cell
(m - 1, n - 1)
, we check ift
is0
. A balance of zero means we have an equal number of opening and closing parentheses, indicating a valid path. -
Recursive Exploration: For a non-terminating cell, the function branches by calling itself for the next cell to the right
(i, j + 1)
or the next cell down(i + 1, j)
, if those cells are within the bounds of the grid. This continues until the base case or invalid condition is met.
-
-
Memoization: The function is decorated with the
@cache
decorator from Python'sfunctools
library, which memoizes the results. This means that for a given state(i, j, t)
, subsequent DFS calls with the same state will return the memoized result instead of recomputing. This optimizes the DFS algorithm’s efficiency and reduces the overall time complexity. -
Returning Result: The initial call to
dfs(0, 0, 0)
will eventually returntrue
orfalse
based on whether a valid path is found. This value is then returned by thehasValidPath
method.
The solution wisely combines a DFS traversal to explore paths through the grid, a balance tracker to ensure valid parentheses structure, and memoization to avoid redundant calculations for paths already explored with a given balance. By employing these algorithms and patterns, the solution effectively returns true
if there exists at least one valid parenthesis path in the grid, or false
otherwise.
Ready to land your dream job?
Unlock your dream job with a 2-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's illustrate the solution approach with an example. Consider the following grid
:
grid = [ "(()", "())", "())" ]
We want to find at least one path from the top-left corner to the bottom-right corner, where the sequence of parentheses forms a valid string. Here is how we might apply the solution approach outlined previously:
-
Initialization: We start at the top-left corner of the
grid
at cell(0, 0)
and an initial balancet
of0
. -
Recursive DFS Exploration:
- Start at
(0, 0)
with character'('
which increments the balancet
to1
. - Move right to
(0, 1)
encountering another'('
, now balance is2
. - Next, at
(0, 2)
, we encounter')'
, and balance decrements to1
. - Now we can only go down to
(1, 2)
. Here we find')'
again, so balance decrements to0
. - From
(1, 2)
, we can only move down to(2, 2)
. The character here is')'
, which would decrement the balance to-1
.
At this point, the path goes invalid because the balance
t
becomes negative, so this path is abandoned. - Start at
-
Backtracking: We backtrack to the last valid cell
(1, 2)
with balance0
and attempt another path:- Move up one cell to
(0, 2)
and then left to(0, 1)
, looping back to a previously visited state, but this time with an invalid balance due to the path we've taken. This path is abandoned.
- Move up one cell to
-
Exploring Alternative Paths:
- Start over at
(0, 0)
this time going downwards. - Move down to
(1, 0)
encountering')'
, which decrements the balance to0
. - Next, we move right to
(1, 1)
and get'('
, now balance is1
. - Moving right again to
(1, 2)
, we find')'
, so balance goes to0
. - Now, go down to
(2, 2)
. The balance is valid at0
, but we have found that this cell will lead to an invalid path from our previous exploration.
- Start over at
The algorithm continues exploring alternative paths by stepping through the grid, updating balances, backtracking upon invalid paths, and attempting new pathways until it eventually finds a valid path or exhausts all possibilities.
-
Memoization: Throughout this process, the algorithm has been storing the resulting state of each cell and current balance in memory, ensuring that any repeat visits to the same state do not require re-computation. This significantly reduces the amount of work needed.
-
Returning Result: It turns out there's no valid path from the top-left to the bottom-right cell in our example, so the
hasValidPath
call would returnfalse
.
This example illustrates the recursive and backtrack nature of the DFS approach with memoization by trying different paths and checking the balance of parentheses until a valid or no path is found.
Solution Implementation
1from typing import List
2from functools import lru_cache
3
4class Solution:
5 def has_valid_path(self, grid: List[List[str]]) -> bool:
6 # Cache the results of the recursive function to avoid recomputation.
7 @lru_cache(maxsize=None)
8 def dfs(row: int, col: int, balance: int) -> bool:
9 # Increment the balance if an opening parenthesis is encountered
10 if grid[row][col] == '(':
11 balance += 1
12 else: # Decrement the balance for a closing parenthesis
13 balance -= 1
14
15 # A negative balance means there are more closing than opening parens - invalid
16 if balance < 0:
17 return False
18
19 # Reached the bottom-right corner. Check if the path is valid.
20 if row == height - 1 and col == width - 1:
21 return balance == 0
22
23 # Attempt to move down or right
24 for next_row, next_col in [(row + 1, col), (row, col + 1)]:
25 # Check if the next move is within the grid and whether it leads to a solution
26 if next_row < height and next_col < width and dfs(next_row, next_col, balance):
27 return True
28
29 # None of the paths led to a solution
30 return False
31
32 # Get grid dimensions
33 height, width = len(grid), len(grid[0])
34
35 # Start the depth-first search from the top-left corner with a balance of zero
36 return dfs(0, 0, 0)
37
38# Example usage:
39# sol = Solution()
40# result = sol.has_valid_path([['(', ')'], ['(', ')']])
41# print(result) # Output should be True if the path is valid
42
1class Solution {
2 private boolean[][][] visited; // A 3D array to keep track of visited positions with a particular balance t
3 private char[][] grid; // The input grid containing parentheses
4 private int rows; // Number of rows in the grid
5 private int columns; // Number of columns in the grid
6
7 // Method to determine if there's a valid path from top-left to bottom-right
8 public boolean hasValidPath(char[][] grid) {
9 rows = grid.length;
10 columns = grid[0].length;
11 this.grid = grid;
12 visited = new boolean[rows][columns][rows + columns]; // Initialize the visited array
13 return dfs(0, 0, 0); // Start depth-first search from the top-left corner with balance 0
14 }
15
16 // Depth-first search method to find a valid path
17 private boolean dfs(int row, int col, int balance) {
18 // If this position with the current balance is already visited, return false to avoid loops
19 if (visited[row][col][balance]) {
20 return false;
21 }
22 // Mark the current position and balance as visited
23 visited[row][col][balance] = true;
24
25 // Update the balance: increment for '(', decrement for ')'
26 balance += grid[row][col] == '(' ? 1 : -1;
27
28 // If balance is negative, we have more ')' than '(', so return false
29 if (balance < 0) {
30 return false;
31 }
32
33 // If we've reached the bottom-right corner, check for correct balance
34 if (row == rows - 1 && col == columns - 1) {
35 return balance == 0;
36 }
37
38 // Directions to move right or down
39 int[] directionRow = {0, 1};
40 int[] directionCol = {1, 0};
41
42 // Loop through possible directions
43 for (int k = 0; k < 2; ++k) {
44 int newRow = row + directionRow[k];
45 int newCol = col + directionCol[k];
46
47 // Check if we can move to the new position and if dfs returns true, then path is valid
48 if (newRow < rows && newCol < columns && dfs(newRow, newCol, balance)) {
49 return true;
50 }
51 }
52
53 // If no path is found, return false
54 return false;
55 }
56}
57
1// Visited array to keep track of the states that have been visited
2bool visited[100][100][200];
3
4// Directions array represents the movement: right (0,1) and down (1,0)
5int directions[2][2] = {{0, 1}, {1, 0}};
6
7class Solution {
8public:
9 // Function to determine if there exists a valid path in the grid
10 bool hasValidPath(vector<vector<char>>& grid) {
11 // Initialize visited array to track visited states
12 memset(visited, 0, sizeof(visited));
13 // Start the Depth-First Search from the top-left corner with 0 opened parentheses
14 return dfs(0, 0, 0, grid);
15 }
16
17 // Helper function to perform DFS
18 bool dfs(int row, int col, int balance, vector<vector<char>>& grid) {
19 // If current state has already been visited, we can return false as it would be a cycle
20 if (visited[row][col][balance]) return false;
21 // Mark current state as visited
22 visited[row][col][balance] = true;
23
24 // Adjust balance based on whether current char is an opening or closing parenthesis
25 balance += grid[row][col] == '(' ? 1 : -1;
26
27 // If balance is negative, the path is invalid
28 if (balance < 0) return false;
29
30 // Get grid dimensions
31 int rows = grid.size(), cols = grid[0].size();
32
33 // If we have reached the bottom-right corner, check if balance is 0 (valid path)
34 if (row == rows - 1 && col == cols - 1) {
35 return balance == 0;
36 }
37
38 // Explore the neighboring right and down cells
39 for (int k = 0; k < 2; ++k) {
40 int newRow = row + directions[k][0];
41 int newCol = col + directions[k][1];
42
43 // Check bounds and continue DFS if the next cell is within the grid
44 if (newRow < rows && newCol < cols && dfs(newRow, newCol, balance, grid)) {
45 return true;
46 }
47 }
48
49 // If no path from this cell leads to a valid path, return false
50 return false;
51 }
52};
53
1interface Coordinate {
2 x: number;
3 y: number;
4}
5
6// The visited record keeps track of which states have been visited: [row][column][balance]
7const visited: boolean[][][] = Array(100).fill(null).map(() =>
8 Array(100).fill(null).map(() => Array(200).fill(false)));
9
10// Directions array represents the movement: right (0,1) and down (1,0)
11const directions: Coordinate[] = [{x: 0, y: 1}, {x: 1, y: 0}];
12
13// Function to determine if there exists a valid path in the grid
14function hasValidPath(grid: char[][]): boolean {
15 // Initialize visited array to track visited states
16 resetVisited();
17 // Start the Depth-First Search from the top-left corner with 0 opened parentheses
18 return dfs(0, 0, 0, grid);
19}
20
21// Helper function to perform DFS
22function dfs(row: number, col: number, balance: number, grid: char[][]): boolean {
23 // If current state has already been visited, return false to avoid cycles
24 if (visited[row][col][balance]) return false;
25
26 // Mark current state as visited
27 visited[row][col][balance] = true;
28
29 // Adjust balance based on whether current char is an opening or closing parenthesis
30 balance += grid[row][col] === '(' ? 1 : -1;
31
32 // If balance is negative, the path is invalid
33 if (balance < 0) return false;
34
35 // Get grid dimensions
36 const rows = grid.length;
37 const cols = grid[0].length;
38
39 // If we have reached the bottom-right corner, check if balance is 0 (valid path)
40 if (row === rows - 1 && col === cols - 1) {
41 return balance === 0;
42 }
43
44 // Explore the neighboring right and down cells
45 for (let k = 0; k < directions.length; ++k) {
46 const newRow = row + directions[k].x;
47 const newCol = col + directions[k].y;
48
49 // Check bounds and continue DFS if the next cell is within the grid
50 if (newRow < rows && newCol < cols && dfs(newRow, newCol, balance, grid)) {
51 return true;
52 }
53 }
54
55 // If no path from this cell leads to a valid path, return false
56 return false;
57}
58
59// Resets the visited array to track new states for a fresh call to hasValidPath
60function resetVisited(): void {
61 for (let i = 0; i < visited.length; i++) {
62 for (let j = 0; j < visited[i].length; j++) {
63 visited[i][j].fill(false);
64 }
65 }
66}
67
68// Assuming the global type char[][] is equivalent to the character grid structure being used
69type char = '(' | ')';
70
Time and Space Complexity
The given Python code defines a method hasValidPath
which determines if there is a valid path from the upper left corner to the lower right corner in a grid, following certain rules related to parentheses balance. The algorithm uses Depth First Search (DFS) with memoization to explore possible paths.
Time Complexity:
The time complexity of the DFS is potentially O(2^(m+n))
without memoization, since in the worst case, each cell has two choices (to go right or down) and could be visited multiple times. However, with memoization, each unique state is computed only once. A state is defined by the current position (i, j)
and the current balance t
of parentheses. Since i
can take m
values, j
can take n
values, and t
can range from -n
to m
(since the path is invalid if the balance is outside this range), the number of unique states is O(m * n * (m+n))
.
Therefore, t
can have maximum m + n + 1
valid different values (from 0
to m + n
), so the time complexity is O(m * n * (m+n))
.
Space Complexity:
For space complexity, we have:
- The recursive stack space, which in the worst-case scenario can grow as deep as
m + n
(the length of the longest path from(0, 0)
to(m-1, n-1)
). This contributesO(m + n)
to the space complexity. - The cache that memoizes the states to avoid re-computing the same state. This cache could be at most
m * n * (m+n)
in size, as analyzed before, which makes itO(m * n * (m+n))
.
Thus, the space complexity is primarily determined by the size of the memoization cache and the recursive call stack. The resulting total space complexity of the algorithm is O(m * n * (m+n))
.
Learn more about how to find time and space complexity quickly using problem constraints.
Which of the two traversal algorithms (BFS and DFS) can be used to find whether two nodes are connected?
Recommended Readings
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
LeetCode Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way we
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https algomonster s3 us east 2 amazonaws com recursion jpg You first
Want a Structured Path to Master System Design Too? Don’t Miss This!