最长有效括号是字符串处理中的经典问题,要求我们找出给定字符串中最长的有效括号子串的长度。有效括号字符串需要满足每个左括号都能找到对应的右括号,并且括号嵌套关系正确。
我最初解决这个问题时,尝试过暴力解法——枚举所有可能的子串并验证其有效性。这种方法虽然直观,但时间复杂度高达O(n³),显然不适合处理较长的输入。经过思考,我发现可以利用栈这种数据结构来优化解法。
栈的特性完美匹配了括号匹配的需求:后进先出的特性可以确保最近遇到的左括号能够优先匹配右括号。具体实现时,我们不仅需要记录括号本身,还需要记录它们在字符串中的位置信息,这样才能准确计算有效子串的长度。
在实现过程中,我设置了一个初始值为-1的栈底元素作为哨兵节点。这个技巧非常重要,它帮助我们处理边界情况,特别是当有效子串从字符串开头开始时。
python复制class Solution:
def longestValidParentheses(self, s: str) -> int:
stack = [-1] # 虚拟哨兵,以这个点为界
result = 0
for i, ch in enumerate(s):
if ch == '(':
stack.append(i)
elif len(stack) > 1: # 除哨兵外元素非空
stack.pop()
result = max(result, i - stack[-1])
else:
stack[0] = i
return result
这段代码的核心逻辑是:
这个算法的时间复杂度是O(n),因为我们只需要遍历字符串一次。空间复杂度也是O(n),最坏情况下需要存储所有左括号的索引。
在实际测试中,我发现这个解法已经相当高效。不过,对于特别长的字符串,可以考虑使用双指针法进一步优化空间复杂度到O(1),但实现起来会更加复杂,且需要考虑更多边界情况。
不同路径问题要求计算从网格左上角到右下角的所有可能路径数,每次只能向右或向下移动。这是一个典型的动态规划问题,因为当前格子的路径数取决于其上方和左侧格子的路径数。
我最初尝试用递归解决这个问题,但很快就遇到了性能问题。对于较大的网格(比如19x13),递归解法会因为重复计算而变得极其缓慢。这促使我转向动态规划解法。
python复制class Solution:
def uniquePaths(self, m: int, n: int) -> int:
dp = [[0] * n for i in range(m)]
for i in range(m):
dp[i][0] = 1
for j in range(n):
dp[0][j] = 1
for i in range(1, m):
for j in range(1, n):
dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
return dp[m - 1][n - 1]
实现中的几个关键点:
这个问题还可以用组合数学的方法解决。从起点到终点需要移动(m-1)+(n-1)步,其中(m-1)步向下,(n-1)步向右。因此路径总数等于从(m+n-2)步中选择(m-1)步向下的组合数。
虽然数学解法更简洁,但在实际编程中计算大数组合数可能会遇到数值溢出问题,需要特别注意。动态规划解法虽然空间复杂度较高,但实现起来更直观可靠。
最小路径和问题要求找到从网格左上角到右下角的路径,使得路径上的数字总和最小。这看起来与不同路径问题相似,但需要计算的是最小值而非总数。
我尝试将不同路径问题的解法进行调整,发现只需要将递推公式中的加法改为取最小值操作即可。这说明很多动态规划问题虽然具体要求不同,但基本思路是相通的。
python复制class Solution:
def minPathSum(self, grid: List[List[int]]) -> int:
m = len(grid)
n = len(grid[0])
dp = [[0] * n for i in range(m)]
dp[0][0] = grid[0][0]
for i in range(1, m):
dp[i][0] = dp[i - 1][0] + grid[i][0]
for j in range(1, n):
dp[0][j] = dp[0][j - 1] + grid[0][j]
for i in range(1, m):
for j in range(1, n):
dp[i][j] = min(dp[i - 1][j] + grid[i][j],
dp[i][j - 1] + grid[i][j])
return dp[m - 1][n - 1]
实现要点:
这个问题的标准解法使用了O(mn)的空间。在实际应用中,如果网格非常大,可以考虑优化空间复杂度。因为每次计算只需要上一行的数据,所以可以只维护两行或一行的数据,将空间复杂度降低到O(n)甚至O(min(m,n))。
通过这三个问题的练习,我总结出识别动态规划问题的几个特征:
在实现动态规划解法时,我经常遇到以下问题:
调试时,我通常会:
在实际面试中,可能会遇到最长有效括号的变种问题,例如:
对于这些问题,基本的栈思路仍然适用,但需要根据具体要求进行调整。
路径问题也有很多有趣的变种:
这些变种通常需要在标准解法的基础上增加额外的状态信息或约束条件。
在处理大规模数据时,我总结了以下优化经验:
虽然这些问题看起来抽象,但它们的思想在实际开发中很有价值:
根据我的经验,有效的刷题策略包括:
对于动态规划入门,我推荐以下题目序列:
在算法面试中,我建议:
记住,面试官不仅考察你能否解决问题,更关注你解决问题的过程和方法。即使不能立即给出最优解,展示出系统的思考过程也很重要。