每日一题-力扣-2116. 判断括号字符串是否可以变为有效-20250323
四种解法探究:判断括号字符串是否可以变为有效
引言
在算法编程中,括号匹配问题是一个经典的字符串处理问题。本文将探讨一个扩展版本:给定一个括号字符串和一个锁定状态字符串,判断是否可以通过修改未锁定的字符,使整个字符串变成有效的括号序列。这个问题不仅测试了对数据结构的理解,也考验了算法设计的灵活性和效率。
问题定义
有两个输入字符串:
- 括号字符串
s
,包含 ‘(’ 和 ‘)’ 字符 - 锁定状态字符串
locked
,包含 ‘0’ 和 ‘1’ 字符
如果 locked[i]
为 ‘1’,则 s[i]
不可修改;如果为 ‘0’,则可以将 s[i]
修改为 ‘(’ 或 ‘)’。需要判断是否可以通过修改未锁定的字符,使最终字符串成为有效的括号序列。
有效的括号序列需满足两个条件:
- 左右括号数量相等
- 任意前缀中,左括号数量不少于右括号数量
解法一:双向扫描法
双向扫描法通过两次线性扫描来确保括号序列的有效性,充分利用了括号匹配的特性。
原理
- 从左到右扫描,确保右括号不会过多
- 从右到左扫描,确保左括号不会过多
- 如果两次扫描都通过,则序列可以变为有效
实现代码
class Solution:
def canBeValid(self, s: str, locked: str) -> bool:
if len(s) % 2: # 奇数长度直接返回False
return False
# 简化的双向扫描
def check(s, locked, open_char):
balance = 0
wild = 0 # 未锁定字符数量
# 根据扫描方向调整遍历
indices = range(len(s)) if open_char == '(' else range(len(s)-1, -1, -1)
for i in indices:
if locked[i] == '0':
wild += 1
elif s[i] == open_char:
balance += 1
else:
balance -= 1
# 关键优化:提前退出条件
if wild + balance < 0:
return False
return True
# 一次调用检查左括号平衡,一次检查右括号平衡
return check(s, locked, '(') and check(s, locked, ')')
优势
- 实现简单直观
- 空间复杂度低,只需常数额外空间
- 能有效处理边缘情况,如字符串开头的锁定右括号
解法二:贪心算法
贪心算法尝试优先处理当前位置的括号匹配,灵活调整未锁定的字符。
原理
- 使用栈来跟踪需要匹配的左括号
- 使用一个数组记录未锁定的字符位置
- 先从左到右匹配所有右括号,再从右到左匹配所有左括号
实现代码
class Solution:
def canBeValid(self, s: str, locked: str) -> bool:
if len(s) % 2 != 0:
return False
n = len(s)
available_positions = [] # 存储可修改的位置
stack = [] # 存储需要匹配的左括号位置
# 第一遍:尝试匹配所有右括号
for i in range(n):
if locked[i] == '0':
available_positions.append(i)
elif s[i] == '(':
stack.append(i)
elif s[i] == ')':
if stack: # 有左括号可匹配
stack.pop()
elif available_positions: # 使用未锁定位置作为左括号
available_positions.pop()
else:
return False
# 第二遍:处理多余的左括号
available_positions = [] # 重置
stack = [] # 重置
for i in range(n-1, -1, -1):
if locked[i] == '0':
available_positions.append(i)
elif s[i] == ')':
stack.append(i)
elif s[i] == '(':
if stack: # 有右括号可匹配
stack.pop()
elif available_positions: # 使用未锁定位置作为右括号
available_positions.pop()
else:
return False
return True
优势
- 显式跟踪括号匹配过程
- 适用于需要知道具体如何修改字符的场景
- 提供了括号匹配问题的一种新视角
解法三:平衡范围计算法
平衡范围计算法跟踪每个位置可能的平衡值范围,确保有效的括号序列。
原理
- 维护最小和最大可能的平衡值
- 对于未锁定字符,考虑将其变为左括号或右括号的影响
- 确保最大平衡值不为负,最终最小平衡值为零
实现代码
class Solution:
def canBeValid(self, s: str, locked: str) -> bool:
if len(s) % 2 != 0:
return False
n = len(s)
min_balance = 0 # 最小可能的平衡值
max_balance = 0 # 最大可能的平衡值
for i in range(n):
if locked[i] == '1':
if s[i] == '(':
min_balance += 1
max_balance += 1
else: # s[i] == ')'
min_balance -= 1
max_balance -= 1
else: # 未锁定字符
min_balance -= 1 # 假设是右括号
max_balance += 1 # 假设是左括号
if max_balance < 0: # 即使全部变为左括号也无法平衡
return False
min_balance = max(0, min_balance) # 平衡值不能为负
return min_balance == 0 # 最终必须能达到平衡
优势
- 使用范围思想,考虑所有可能性
- 避免了多次扫描
- 更直观地表示了问题的数学本质
解法四:改进的计数分析法
计数分析法通过精确地分析字符计数和平衡状态,确保有效性判断的准确性。
原理
- 结合双向扫描的思想
- 实时跟踪平衡状态和可用的未锁定字符
- 在每个位置检查平衡状态是否可维持
实现代码
class Solution:
def canBeValid(self, s: str, locked: str) -> bool:
# 基础检查:字符串长度必须为偶数
if len(s) % 2 != 0:
return False
n = len(s)
# 从左到右扫描:确保右括号不会过多
balance = 0
available = 0
for i in range(n):
if locked[i] == '0':
available += 1
elif s[i] == '(':
balance += 1
else: # 锁定的右括号
balance -= 1
# 关键检查点
if balance + available < 0:
return False
# 从右到左扫描:确保左括号不会过多
balance = 0
available = 0
for i in range(n-1, -1, -1):
if locked[i] == '0':
available += 1
elif s[i] == ')':
balance += 1
else: # 锁定的左括号
balance -= 1
if balance + available < 0:
return False
return True
优势
- 结合了双向扫描的优点
- 处理边缘情况的能力强
- 代码简洁高效
方法比较
解法 | 时间复杂度 | 空间复杂度 | 优势 | 劣势 |
---|---|---|---|---|
双向扫描法 | O(n) | O(1) | 简单直观,易于理解 | 需要两次扫描 |
贪心算法 | O(n) | O(n) | 可以知道具体修改方案 | 空间消耗较大 |
平衡范围计算法 | O(n) | O(1) | 单次扫描,概念清晰 | 理解难度较高 |
改进的计数分析法 | O(n) | O(1) | 结合多种优点,健壮性强 | 需要两次扫描 |
实例分析
以下是一个具体的测试案例:
s = "))(()(()(()))(()((()))((()))))(((((((())))))("
locked = "10001111011001101101011110011101110111111000010100110100111"
这个例子中,字符串以锁定的右括号开始,使用以上解法可以迅速判断出它不可能变为有效序列。
总结与思考
这四种解法展示了解决括号匹配问题的不同思路,从简单的双向扫描到更复杂的平衡范围计算。每种方法都有其独特的优势和适用场景。在实际应用中,可以根据具体需求选择合适的算法。
关键的解题思想包括:
- 利用括号匹配的特性
- 巧妙使用计数和平衡概念
- 考虑问题的不同约束条件
- 通过多角度思考提高算法效率
这个问题不仅是算法能力的考验,也是思维灵活性的展示。