括号匹配的最大得分问题(带符号权重版本)
题目描述
给定一个由 '(' 和 ')' 组成的字符串 s,以及两个整数 a 和 b。字符串 s 中每个匹配的括号对 () 的得分为 a(如果 a 为正,则得分增加;如果 a 为负,则得分减少),而每个不匹配的括号(即无法与任何其他括号形成有效对的括号)的得分为 b(同样,b 的正负影响得分增减)。你的任务是计算字符串 s 的所有可能子序列中,括号匹配子序列能获得的最大得分。
一个括号匹配子序列是指一个子序列,其中每个开括号 '(' 都有一个对应的闭括号 ')' 与之正确匹配,并且整个子序列是有效的括号序列。
例如,对于 s = "()())", a = 1, b = -1:
- 子序列 "()" 是匹配的,得分为 a = 1。
- 子序列 "()()" 是匹配的,得分为 2*a = 2。
- 子序列 "(()))" 不是有效的匹配子序列。
- 最大得分子序列是 "()()",得分为 2。
解题过程
-
问题分析
我们需要在字符串 s 中找到一个子序列(不一定连续),使得这个子序列是一个有效的括号序列,并且根据给定的权重 a 和 b,计算其得分(匹配括号对得分 a,不匹配括号得分 b)并最大化该得分。关键在于,一个有效的括号序列中,不应该存在不匹配的括号。因此,在一个有效的括号匹配子序列中,只有匹配的括号对会贡献得分 a,而不会有不匹配的括号(因为不匹配的括号不会出现在有效的子序列中)。然而,题目描述中提到了“每个不匹配的括号得分为b”,这似乎与有效子序列的定义矛盾。这里需要澄清:题目是在所有可能的子序列(包括无效的)中寻找匹配子序列的最大得分。在匹配子序列内部,没有不匹配的括号,所以 b 不直接影响匹配子序列的得分。但是,我们的决策(选择哪些括号进入子序列)会影响最终匹配子序列中匹配对的数量。我们可能为了获得更多的匹配对而包含更多的括号,但也可能因为某些括号难以匹配而选择不包含它们以避免无效序列。实际上,对于匹配子序列,其得分就是 a * (匹配对的数量)。我们的目标是最大化这个匹配对的数量(因为 a 是固定的,尽管可能是负数)。权重 b 在这个问题表述中似乎并不直接用于计算匹配子序列的得分,这可能与常见的“带得分的括号匹配”问题描述有出入。一个更常见的版本是:每个匹配的括号对得分为 a,每个未匹配的括号(在最终选择的子序列中,无论是否整体有效,只要它没配对)得分为 b,然后求所有子序列(不一定有效)的最大得分。但题目明确要求是“括号匹配子序列”。让我们重新审视题目描述:“计算字符串 s 的所有可能子序列中,括号匹配子序列能获得的最大得分。” 并且提到了匹配对得分 a 和不匹配括号得分 b。一个合理的解释是:题目本意可能是求所有子序列(包括无效的)的最大得分,其中得分由匹配对和不匹配的括号共同贡献。但问题描述中又强调了是“括号匹配子序列”。这存在歧义。为了提供一个有意义的、典型的区间DP解法,我们将问题解释为以下更常见且清晰的问题(这很可能也是题目的本意,描述可能存在笔误):
修正后的问题描述:给定一个由 '(' 和 ')' 组成的字符串 s,以及两个整数 a 和 b。对于 s 的任何一个子序列(不一定连续),其得分计算如下:子序列中每一个匹配的括号对 () 贡献得分 a,每一个不匹配的括号(即在这个子序列中,没有与之配对的括号)贡献得分 b。请计算所有可能子序列(不一定需要是有效的括号序列)能获得的最大得分。
例如,s = "()())", a = 1, b = -1:
- 子序列 "()" :有1个匹配对,得分为 a = 1。没有不匹配括号。
- 子序列 "()()" :有2个匹配对,得分为 2*a = 2。
- 子序列 "(( )" :如果取索引0,1,2的字符 "(()",则有1个匹配对(索引1和2的括号),索引0的 '(' 不匹配。得分 = a + b = 1 + (-1) = 0。
- 子序列 "())" :有1个匹配对(索引0和1的括号),索引2的 ')' 不匹配。得分 = a + b = 1 + (-1) = 0。
- 最大得分子序列是 "()()",得分为 2。
这个修正后的问题是一个经典的区间DP问题。
-
动态规划状态定义
我们使用一个二维数组dp,其中dp[i][j]表示在子字符串s[i:j+1](即从索引 i 到索引 j 的闭区间)中,所有可能子序列能获得的最大得分。
注意:这个子序列是从s[i]到s[j]这个区间内选择的,不要求连续。 -
状态转移方程
我们考虑区间[i, j]的最大得分dp[i][j]如何由更小的区间转移而来。-
情况1:不考虑字符
s[i]
最大得分可能来自于完全不选择区间第一个字符s[i]的情况。那么得分就是区间[i+1, j]的最大得分:dp[i][j] = max(dp[i][j], dp[i+1][j]) -
情况2:考虑字符
s[i]
如果我们决定选择字符s[i],那么它有两种可能:-
2a:
s[i]作为不匹配的括号
如果我们让s[i]成为一个不匹配的括号,那么它的贡献是得分b。然后,我们需要加上剩下的区间[i+1, j]中能获得的最大得分。所以这种情况下的得分是:b + dp[i+1][j]。因此:dp[i][j] = max(dp[i][j], b + dp[i+1][j]) -
2b:
s[i]与区间内的另一个括号s[k]匹配
如果s[i]是一个开括号 '(',我们可以在区间[i+1, j]中寻找一个闭括号 ')' 即s[k](其中 i < k <= j 且 s[k] == ')'),与之配对。
当s[i]和s[k]匹配时,它们形成了一个匹配对,贡献得分a。
那么,整个区间[i, j]的得分可以分解为三部分:s[i]和s[k]匹配对的得分:a。- 区间
[i+1, k-1]内部的子序列得分:dp[i+1][k-1]。注意这是区间(i, k)的内部。 - 区间
[k+1, j]内部的子序列得分:dp[k+1][j]。
所以,如果s[i]和s[k]匹配,总得分为:a + dp[i+1][k-1] + dp[k+1][j]。
我们需要遍历所有可能的 k (i < k <= j 且 s[k] == ')'),找到能使总得分最大的那个 k。
因此:dp[i][j] = max(dp[i][j], a + dp[i+1][k-1] + dp[k+1][j])对于所有满足条件的 k。
同理,如果
s[i]是一个闭括号 ‘)’,理论上它也可以与前面的开括号匹配,但我们的状态转移是从左到右考虑区间起点的。通常,我们只考虑开括号作为起点去寻找匹配的闭括号,因为如果s[i]是闭括号,它要么不匹配,要么应该由之前的一个开括号来“匹配”它,而不是它自己作为起点去找匹配。在我们的循环设计中,当 i 是闭括号时,情况2b(匹配)不会发生,它只能走情况1(不选)或情况2a(作为不匹配括号)。 -
综合状态转移方程:
初始化 dp[i][j] = 负无穷 (或一个很小的数,表示无效/极小值) For 区间长度 len from 1 to n: For 起点 i from 0 to n-len: j = i + len - 1 // 情况1:不选s[i] dp[i][j] = max(dp[i][j], dp[i+1][j]) // 情况2:选s[i] // 2a: s[i]作为不匹配括号 dp[i][j] = max(dp[i][j], b + dp[i+1][j]) // 2b: 如果s[i]是开括号'(',尝试与s[k]匹配 if s[i] == '(': for k from i+1 to j: if s[k] == ')': // 确保内部区间有效,如果i+1 > k-1,则dp[i+1][k-1]为0(空区间得分0) inner = 0 if i+1 <= k-1: inner = dp[i+1][k-1] outer = 0 if k+1 <= j: outer = dp[k+1][j] dp[i][j] = max(dp[i][j], a + inner + outer)注意:空区间的得分是0。
-
-
初始化
- 对于任意区间,如果区间长度为0(即 i > j),则得分为0。
dp[i][i-1] = 0(对于所有 i)。 - 对于长度为1的区间
[i, i],只有一个字符。这个字符无法形成匹配对,只能作为不匹配括号(如果被选择)或者不被选择。- 不选:得分为0(空序列得分0)。
- 选:得分是 b(一个不匹配括号)。
所以dp[i][i] = max(0, b)。
- 对于任意区间,如果区间长度为0(即 i > j),则得分为0。
-
计算顺序
我们需要按照区间长度从小到大的顺序来计算dp数组。因为计算长区间的dp[i][j]需要用到更短的区间(如[i+1, j],[i+1, k-1],[k+1, j])的结果。 -
最终结果
整个字符串s对应的最大得分就是dp[0][n-1],其中 n 是字符串 s 的长度。 -
复杂度分析
- 时间复杂度:O(n³)。需要遍历所有 O(n²) 个区间,对于每个区间,最坏情况下需要遍历 O(n) 个可能的 k 来进行匹配。
- 空间复杂度:O(n²),用于存储
dp表。
总结
这个算法通过动态规划系统地计算了所有区间内子序列的最大得分。对于每个区间,它考虑了起点字符的不同命运(不选、作为不匹配括号、与区间内另一个括号匹配),从而逐步构建出整个问题的解。最终结果 dp[0][n-1] 给出了在整个字符串上能获得的最大得分。