Hard
题目描述
括号字符串是仅由 ‘(’ 和 ‘)’ 组成的非空字符串。如果满足以下任一条件,则该字符串是有效的:
- 它是 ()
- 它可以写成 AB(A 与 B 连接),其中 A 和 B 都是有效的括号字符串
- 它可以写成 (A),其中 A 是有效的括号字符串
给定一个 m x n 的括号网格矩阵。网格中的有效括号字符串路径是满足以下所有条件的路径:
- 路径从左上角单元格 (0, 0) 开始
- 路径在右下角单元格 (m - 1, n - 1) 结束
- 路径只能向下或向右移动
- 路径形成的括号字符串是有效的
如果网格中存在有效的括号字符串路径,返回 true,否则返回 false。
示例 1:
输入:grid = [["(","(","("],[")","(",")"],["(","(",")"],["(","(",")"]]
输出:true
解释:上图显示了两条形成有效括号字符串的可能路径。
第一条路径形成有效括号字符串 "()(())"。
第二条路径形成有效括号字符串 "((()))"。
注意可能存在其他有效的括号字符串路径。
示例 2:
输入:grid = [[")",")"],["(","("]]
输出:false
解释:两条可能的路径形成括号字符串 "))(" 和 ")(("。由于它们都不是有效的括号字符串,我们返回 false。
约束条件:
- m == grid.length
- n == grid[i].length
- 1 <= m, n <= 100
- grid[i][j] 是 ‘(’ 或 ‘)’
解题思路
这是一道动态规划题目。关键观察是:对于任何有效的括号字符串的前缀,左括号的数量必须大于或等于右括号的数量。
我们可以用三维DP来解决:dp[i][j][k] 表示到达位置 (i,j) 时,当前路径的左括号减去右括号的差值为 k 是否可能。
状态定义:
dp[i][j][k]= true 表示可以到达位置(i,j)且括号平衡值为k- 括号平衡值:遇到 ‘(’ 时 +1,遇到 ‘)’ 时 -1
状态转移:
- 从上方转移:
dp[i][j][k] |= dp[i-1][j][prev_k] - 从左方转移:
dp[i][j][k] |= dp[i][j-1][prev_k] - 其中
prev_k根据当前字符调整:- 如果
grid[i][j] == '(',则prev_k = k - 1 - 如果
grid[i][j] == ')',则prev_k = k + 1
- 如果
边界条件:
- 起点
dp[0][0][1] = true(如果grid[0][0] == '(') - 平衡值不能为负数(保证括号序列的有效性前缀条件)
优化: 可以使用滚动数组或者用集合记录每个位置可能的平衡值,减少空间复杂度。
代码实现
class Solution {
public:
bool hasValidPath(vector<vector<char>>& grid) {
int m = grid.size(), n = grid[0].size();
int maxLen = m + n - 1;
// dp[i][j] 存储在位置(i,j)可能的平衡值集合
vector<vector<set<int>>> dp(m, vector<set<int>>(n));
// 初始化起点
if (grid[0][0] == '(') {
dp[0][0].insert(1);
}
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (i == 0 && j == 0) continue;
char ch = grid[i][j];
int delta = (ch == '(') ? 1 : -1;
// 从上方转移
if (i > 0) {
for (int prev : dp[i-1][j]) {
int curr = prev + delta;
if (curr >= 0 && curr <= maxLen) {
dp[i][j].insert(curr);
}
}
}
// 从左方转移
if (j > 0) {
for (int prev : dp[i][j-1]) {
int curr = prev + delta;
if (curr >= 0 && curr <= maxLen) {
dp[i][j].insert(curr);
}
}
}
}
}
return dp[m-1][n-1].count(0) > 0;
}
};
class Solution:
def hasValidPath(self, grid: List[List[str]]) -> bool:
m, n = len(grid), len(grid[0])
max_len = m + n - 1
# dp[i][j] 存储在位置(i,j)可能的平衡值集合
dp = [[set() for _ in range(n)] for _ in range(m)]
# 初始化起点
if grid[0][0] == '(':
dp[0][0].add(1)
for i in range(m):
for j in range(n):
if i == 0 and j == 0:
continue
ch = grid[i][j]
delta = 1 if ch == '(' else -1
# 从上方转移
if i > 0:
for prev in dp[i-1][j]:
curr = prev + delta
if 0 <= curr <= max_len:
dp[i][j].add(curr)
# 从左方转移
if j > 0:
for prev in dp[i][j-1]:
curr = prev + delta
if 0 <= curr <= max_len:
dp[i][j].add(curr)
return 0 in dp[m-1][n-1]
public class Solution {
public bool HasValidPath(char[][] grid) {
int m = grid.Length, n = grid[0].Length;
int maxLen = m + n - 1;
// dp[i][j] 存储在位置(i,j)可能的平衡值集合
HashSet<int>[,] dp = new HashSet<int>[m, n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
dp[i, j] = new HashSet<int>();
}
}
// 初始化起点
if (grid[0][0] == '(') {
dp[0, 0].Add(1);
}
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (i == 0 && j == 0) continue;
char ch = grid[i][j];
int delta = ch == '(' ? 1 : -1;
// 从上方转移
if (i > 0) {
foreach (int prev in dp[i-1, j]) {
int curr = prev + delta;
if (curr >= 0 && curr <= maxLen) {
dp[i, j].Add(curr);
}
}
}
// 从左方转移
if (j > 0) {
foreach (int prev in dp[i, j-1]) {
int curr = prev + delta;
if (curr >= 0 && curr <= maxLen) {
dp[i, j].Add(curr);
}
}
}
}
}
return dp[m-1, n-1].Contains(0);
}
}
var hasValidPath = function(grid) {
const m = grid.length, n = grid[0].length;
const maxLen = m + n - 1;
// dp[i][j] 存储在位置(i,j)可能的平衡值集合
const dp = Array(m).fill(null).map(() =>
Array(n).fill(null).map(() => new Set())
);
// 初始化起点
if (grid[0][0]
复杂度分析
| 复杂度类型 | 复杂度 |
|---|---|
| 时间复杂度 | O(m × n × (m + n)) |
| 空间复杂度 | O(m × n × (m + n)) |
说明:
- 时间复杂度:对于每个位置 (i,j),最多有 O(m+n) 种可能的平衡值,需要遍历所有可能的状态进行转移
- 空间复杂度:需要存储每个位置所有可能的平衡值,最坏情况下每个位置有 O(m+n) 种状态