题目:最长有效括号匹配子序列的变种:统计所有不同的、连续的有效括号子串数量
字数 10069 2025-12-18 04:16:33

好的,在仔细检查了你提供的庞大历史记录列表,并确保不重复之后,我将为你讲解一个经典的、但你可能还未听过的线性动态规划问题。

题目:最长有效括号匹配子序列的变种:统计所有不同的、连续的有效括号子串数量

题目描述:
给你一个只包含字符 '('')' 的字符串 s。一个有效括号子串定义为满足以下条件的连续子串:

  1. 所有括号都能正确匹配。
  2. 子串是连续的(即原始字符串中一段连续的字符序列)。

例如,对于字符串 "()(())"

  • 有效的连续括号子串有:"()", "(())", "()(())"
  • "())" 不是有效的,因为括号没有完全匹配。
  • "()()" 也是有效的,但在这个例子中不是连续的子串(在 "()(())" 中,它是两个独立的子串 "()""(())" 拼接而成的一个更大有效串)。

我们的目标是计算字符串 s 中所有不同的、有效的连续括号子串的数量。注意,即使两个子串内容相同,只要它们在字符串中的起始和结束位置不同,就被视为不同的子串。

例如:

  • s = "()", 输出:1 (子串:[0, 1] -> "()")
  • s = "(())",输出:2 (子串:[1, 2] -> "()", [0, 3] -> "(())")
  • s = "()(())",输出:3 (子串:[0, 1] -> "()", [3, 4] -> "()", [2, 5] -> "(())")
  • s = "(()())",输出:3 (子串:[1, 2] -> "()", [3, 4] -> "()", [0, 5] -> "(()())")

解题过程循序渐进讲解

这个问题是 “最长有效括号子串” 问题的延伸。最长有效括号子串问题通常使用栈或动态规划来找到最长的有效子串。而这里我们需要统计所有有效子串的数量。

核心思路:
我们可以利用动态规划。定义 dp[i] 表示 以字符 s[i] 为结尾的最长有效括号子串的长度。如果能求出 dp[i],那么我们就能判断以 i 结尾的有效子串是什么,并据此进行计数。

为什么能计数?
因为对于以 i 结尾的最长有效括号子串,它的所有后缀(只要本身是有效的)也都是有效的括号子串。例如,"(()())" 以最后一个 ) 结尾的最长子串是 "(()())",它的有效后缀子串有:"()" (对应子串 [4, 5]), "()" (对应子串 [1,2][3,4],但注意,以 i=5 结尾时,我们只关心从某个位置开始到 i 结束的子串),"(())" (对应 [2,5]),以及整个 "(()())"。我们需要一种系统的方法来统计它们。


步骤 1:定义动态规划状态

dp[i] 表示以字符 s[i] 结尾的最长有效括号子串的长度。如果以 s[i] 结尾无法形成有效括号子串,则 dp[i] = 0

例如,对于 s = “()(())”

  • i=1 (‘)’): 以它结尾的最长有效子串是 “()”,长度 2,所以 dp[1] = 2
  • i=5 (‘)’): 以它结尾的最长有效子串是 “(())”,长度 4,所以 dp[5] = 4

步骤 2:推导状态转移方程

我们如何计算 dp[i]
只有当前字符 s[i]‘)’ 时,才有可能形成以它结尾的有效子串(因为有效子串必须以 ‘)’ 结尾)。

情况分析:

  1. 基本情况:如果 s[i] = ‘)’s[i-1] = ‘(’,即形如 “...()”

    • 那么以 i 结尾的最长有效子串就是 s[i-1]s[i] 组成的 “()”
    • 此外,这个有效子串还可能和它前面紧邻的有效子串连接起来。所以:
    • dp[i] = dp[i-2] + 2,前提是 i >= 2。如果 i-2 不存在,则 dp[i] = 2
  2. 嵌套情况:如果 s[i] = ‘)’s[i-1] = ‘)’,即形如 “...))”

    • 这意味着当前 ‘)’ 可能是一个外层括号的右括号。例如 “(()())”,最后一个 ‘)’ 匹配的是索引 0‘(’
    • 我们需要找到与当前 s[i] 匹配的左括号的位置。这个位置是 j = i - dp[i-1] - 1
      • dp[i-1] 是以 s[i-1] 结尾的最长有效子串长度。i - dp[i-1] - 1 就是跳过这个有效子串后,前面的一个字符。
    • 如果 j >= 0s[j] == ‘(’,说明我们找到了匹配的左括号。
      • 那么,以 s[i] 结尾的有效子串长度至少是 dp[i-1] + 2(内部的 dp[i-1] 长的有效子串加上外面一对括号)。
      • 此外,这个新形成的有效子串还可能和它前面 (j-1) 结尾的有效子串拼接起来。所以:
      • dp[i] = dp[i-1] + 2 + (dp[j-1] if j-1 >= 0 else 0)

状态转移方程总结:

  • 如果 s[i] == ‘)’
    • 如果 i > 0s[i-1] == ‘(’
      dp[i] = (dp[i-2] if i >= 2 else 0) + 2
    • 如果 i > 0s[i-1] == ‘)’
      j = i - dp[i-1] - 1
      如果 j >= 0s[j] == ‘(’
      dp[i] = dp[i-1] + 2 + (dp[j-1] if j >= 1 else 0)
  • 如果 s[i] == ‘(’dp[i] = 0

步骤 3:统计所有有效子串

我们有了 dp 数组。现在关键在于:如何利用 dp[i] 统计所有以 i 结尾的有效子串?

观察到一个重要性质:对于一个以索引 i 结尾的、长度为 L (L = dp[i]) 的最长有效括号子串,所有以 i 结尾的更短的有效子串,必然是这个最长子串的某个后缀,并且这些后缀的有效性可以通过检查它们的长度是否是“有效单位”的偶数倍,并满足括号匹配规则来间接确认吗?

更简单直接的方法:当我们计算出 dp[i] 后,i 结尾的有效子串数量,就等于以 i 结尾的最长有效子串中,所有合法的“前缀-后缀”对的数量吗? 不完全是。

更巧妙的计数方法:
我们引入第二个状态 count[i],它表示 以字符 s[i] 结尾的有效括号子串的数量

count[i] 的推导依赖于 dp[i]

  1. 如果 dp[i] > 0(即以 s[i] 结尾存在有效子串):

    • 那么,以 s[i] 结尾的最短有效子串,就是和 s[i] 匹配的那个左括号 s[j]s[i] 形成的子串 s[j...i],其长度为 len1 = i - j + 1。这个 j 就是我们计算 dp[i] 时找到的 j
    • 这个子串 s[j...i] 本身就是一个有效子串,所以 count 至少为 1。
    • 更重要的是,以 i 结尾的其他有效子串,都可以看作是:在 s[j...i] 这个基础有效单元前面,拼接上若干个以 s[j-1] 结尾的有效子串(如果存在的话)。因为 s[j...i] 自身是有效的,前面再拼一个有效的子串 s[k...j-1],结果 s[k...i] 仍然是有效的。
    • 所以,i 结尾的有效子串总数 = 1 (基础单元 s[j...i]) + 以 j-1 结尾的有效子串总数 (count[j-1])
    • 即:count[i] = 1 + (count[j-1] if j-1 >= 0 else 0)
  2. 如果 dp[i] == 0,则 count[i] = 0

为什么这样是对的?
因为任何以 i 结尾的有效子串,其最后一个字符 ‘)’ 必然匹配某个 ‘(’ 在位置 j。这个子串必然可以分解为 A + B,其中 B = s[j...i] 是一个最小的、不可再分割的、以 i 结尾的有效单元(即 B 内部是有效匹配的,且 s[j]s[i] 匹配)。As[0...j-1] 的某个以 j-1 结尾的有效后缀(可以为空)。A 的有效后缀数量正好就是 count[j-1]。所以总数量是 1 + count[j-1]

步骤 4:计算最终答案

字符串 s 中所有不同的有效括号子串总数 ans,就是所有 count[i] 的和。
ans = sum(count[i]) for i in range(n)

因为 count[i] 精确地统计了以每个位置 i 结尾的所有有效子串,且每个有效子串都有唯一的结束位置 i,所以求和即可得到总数。

步骤 5:算法步骤与示例

让我们用 s = “()(())” 来演示。

  1. 初始化 dp = [0, 0, 0, 0, 0, 0], count = [0, 0, 0, 0, 0, 0]ans = 0
  2. i=0s[0]=‘(’dp[0]=0, count[0]=0
  3. i=1s[1]=‘)’,且 s[0]=‘(’ (基本情况)。
    • j = i - dp[i-1] - 1 不适用,我们走 s[i-1]==‘(’ 分支。
    • dp[1] = (dp[-1]? 视为0) + 2 = 2
    • 找到与 s[1] 匹配的左括号位置 j = 1 - dp[0] - 1? 不,对于基本情况,匹配的左括号就是 i-1=0
    • j = 0
    • count[1] = 1 + (count[-1]? 视为0) = 1
    • ans += 1 (现在 ans=1,对应子串 [0,1] -> “()”)。
  4. i=2s[2]=‘(’dp[2]=0, count[2]=0
  5. i=3s[3]=‘(’dp[3]=0, count[3]=0
  6. i=4s[4]=‘)’,且 s[3]=‘(’ (基本情况)。
    • dp[4] = dp[2] + 2 = 0 + 2 = 2
    • 匹配的左括号 j = 3
    • count[4] = 1 + count[2] = 1 + 0 = 1
    • ans += 1 (现在 ans=2,对应新增子串 [3,4] -> “()”)。
  7. i=5s[5]=‘)’,且 s[4]=‘)’ (嵌套情况)。
    • j = i - dp[i-1] - 1 = 5 - dp[4] - 1 = 5 - 2 - 1 = 2
    • s[2] = ‘(’,匹配成功。
    • dp[5] = dp[4] + 2 + dp[1] = 2 + 2 + 2 = 6? 等等,dp[1]dp[j-1] = dp[2-1]=dp[1]=2。所以 dp[5]=2+2+2=6。这表示以 i=5 结尾的最长有效子串是 s[0...5]“()(())”,长度 6。
    • 匹配的左括号位置 j = 2
    • count[5] = 1 + count[1] = 1 + 1 = 2
      • 这两个以 i=5 结尾的有效子串分别是:
        a. 基础单元 s[2...5] -> “(())” (对应 count=1 中的 1)。
        b. 基础单元前面拼接上以 j-1=1 结尾的有效子串 s[0...1] -> “()”,得到 s[0...5] -> “()(())” (对应 count=1 中的 count[1]=1)。
    • ans += 2 (现在 ans=4)。等等,我们的例子预期是 3。哪里出错了?

问题分析:
我们的推导似乎多了一个。检查 s = “()(())”

  • i=1 结尾的有效子串:[0,1] -> “()”
  • i=4 结尾的有效子串:[3,4] -> “()”
  • i=5 结尾的有效子串:[2,5] -> “(())”[0,5] -> “()(())”
    总数为 4。但题目描述的例子说输出是 3。矛盾在于题目描述!让我们重新审视题目描述中的例子。

题目描述说:s = “()(())”,输出 3,子串为 [0,1], [3,4], [2,5]。它没有[0,5] 算进去。为什么?
因为 [0,5] 对应的子串是 “()(())”,它虽然整体是有效的,但它是由两个独立的有效子串 “()”“(())” 拼接而成的。在许多这类计数问题的变种中,“连续的有效括号子串”有时被隐式定义为“不可再被分割为两个连续的有效括号子串”,或者说**“不能被更小的连续有效子串覆盖”**。这更像是在统计“有效括号子串”的“原子”单位或“极大”单位。

修正题目理解与状态定义:
因此,我们需要明确:我们想统计的是所有满足“该子串本身有效,且其任何真前缀都不是有效括号串”的有效括号子串吗?不,那样会漏掉 “()(())” 这样的串。

更常见的统计目标是:统计所有“不能被扩展”的有效括号子串,即对于子串 s[l...r],如果它是有效的,并且 s[l-1...r]s[l...r+1] 都不是有效的(如果存在的话),那么它就是一个“极大”有效子串。但我们的例子中 [0,5] 是极大的,[0,1][2,5] 不是极大的(因为它们被包含在 [0,5] 里)。这与题目例子不符。

根据例子反推:题目例子似乎是在统计所有形如 “(……)” 的、最外层括号匹配的、且内部也是有效的子串,但它把 “()(())” 这种由两个独立单元 ()(()) 并列而成的串,只算作两个单元 ([0,1][2,5][3,4][0,5]? 不对)……例子中的三个子串是 [0,1], [3,4], [2,5]。注意 [2,5](()),它是一个完整的、内部嵌套的有效单元。[0,1][3,4] 都是 ()[0,5] 没有被计入。

所以,题目的真实意图可能是:统计所有“原始的”、“不可分割的”有效括号子串,即该有效子串不能被划分为两个或多个连续的非空有效括号子串。在 “()(())” 中,[0,5] 可以被划分为 [0,1][2,5][0,3][4,5] 等,所以它不计入。而 [0,1][3,4][2,5] 都不能被这样划分。

这引导我们修改计数逻辑。

步骤 6:修正计数逻辑(统计“不可分割”的有效子串)

我们需要修改 count[i] 的定义或引入新的判断。

新思路
我们依然计算 dp[i]。当我们找到以 i 结尾的最长有效子串的匹配左括号 j 后,形成的有效子串 s[j...i] 是一个候选。
这个子串 s[j...i] 是“不可分割的”当且仅当 dp[i] == i - j + 1 并且 dp[i] 就是原始计算出的长度,没有和前面的 dp[j-1] 连接?不完全是。

实际上,一个有效括号子串 s[l...r] 是“不可分割的”(或称“本原有效的”)的充要条件是:对于任何满足 l < k < rk,子串 s[l...k] 都不是有效的括号串。换句话说,除了 r 之外,任何真前缀都不是有效的。

如何在动态规划中识别?
当我们按照之前的方法计算 dp[i] 时:

  • 在“基本情况” () 下,s[j...i] (其中 j=i-1) 一定是本原有效的,因为它的真前缀只有一个字符 ‘(’,不是有效的。
  • 在“嵌套情况” “(...)” 下,s[j...i] 也是本原有效的,因为它的第一个字符是 ‘(’,最后一个字符是 ‘)’,并且内部 s[j+1...i-1] 是一个有效的子串(由 dp[i-1] 定义)。它的任何真前缀 s[j...k] (对于 k < i) 都会多一个 ‘(’ 而缺少对应的 ‘)’,因此无效。

但是,问题出在当 dp[i] 的计算包含了 dp[j-1] 的部分时(即 dp[i] = dp[i-1] + 2 + dp[j-1]),我们实际上将两个本原有效子串 s[?..j-1]s[j...i] 连接成了一个更大的有效子串 s[?..i]。这个更大的子串 s[?..i] 不是本原有效的,因为它可以被分割。

因此,要统计本原有效的子串,我们只应该计数那些在计算 dp[i] 时,没有与前面子串连接的情况。即,我们只计数“基础单元” s[j...i] 本身,而不是 s[?..i]

修正后的计数方法:
我们定义 primitive_count[i] 表示以 i 结尾的本原有效括号子串的数量。

  1. 如果 s[i] == ‘)’ 且我们找到了匹配的 j (使得 s[j] == ‘(’):
    • 那么 s[j...i] 一定是一个本原有效子串。
    • 所以 primitive_count[i] = 1
    • 注意:即使 dp[i] 因为连接了前面的有效子串而变得更长,我们也不为那个更长的、非本原的子串计数。我们只计 s[j...i] 这一个。
  2. 否则,primitive_count[i] = 0

那么,整个字符串 s 中所有本原有效子串的总数就是 sum(primitive_count[i])

用修正后的方法验证例子 s = “()(())”

  1. i=1: 匹配 j=0, primitive_count[1] = 1。子串 [0,1]
  2. i=4: 匹配 j=3, primitive_count[4] = 1。子串 [3,4]
  3. i=5: 匹配 j=2, primitive_count[5] = 1。子串 [2,5]
    总和为 3,符合例子。

再验证 s = “(()())”

  1. i=1: s[1]=‘)’, s[0]=‘(’, primitive_count[1]=1 (子串 [0,1]? 不对,应该是 [0,1] -> “()”?等等,s[0]=‘(’, s[1]=‘)’,确实是 “()”。)
  2. i=2: s[2]=‘(’, primitive_count[2]=0
  3. i=3: s[3]=‘(’, primitive_count[3]=0
  4. i=4: s[4]=‘)’, s[3]=‘(’, primitive_count[4]=1 (子串 [3,4] -> “()”)。
  5. i=5: s[5]=‘)’, s[4]=‘)’, 计算 j = 5 - dp[4] - 1 = 5 - 2 - 1 = 2s[2]=‘(’,匹配。primitive_count[5] = 1 (子串 [2,5] -> “()()”? 等等,s[2]=‘(’, s[3]=‘(’, s[4]=‘)’, s[5]=‘)’,即 “(()())” 中的内层 “()” 和外层括号?不对,j=2, i=5,子串是 s[2...5] = “(())”?我们检查字符串:s = “(()())”, 索引: 0:(, 1:(, 2:), 3:(, 4:), 5:)。s[2...5] = “)(()”,这显然不对。我犯了一个错误。

重新计算 s = “(()())” 的 dp 数组

  • dp = [0,0,0,0,0,0]
  • i=0: ‘(’, dp=0
  • i=1: ‘(’, dp=0
  • i=2: ‘)’, 且 s[1]=‘(’ (基本情况),dp[2] = dp[0] + 2 = 0+2=2。匹配 j=1
  • i=3: ‘(’, dp=0
  • i=4: ‘)’, 且 s[3]=‘(’ (基本情况),dp[4] = dp[2] + 2 = 2+2=4。匹配 j=3? 等等,基本情况匹配的是 i-1=3dp[4]=4 意味着以 i=4 结尾的最长子串是 s[1...4] = “(()())” 中的 “()()”? s[1...4]“(()”,不对。dp[4]=4 表示子串长度为4,即 s[1...4] (“()()”) 是有效的吗?“()()” 是有效的,长度4。但 s[1]‘(’, s[4]‘)’,中间 s[2]‘)’, s[3]‘(’,即 “()()”,确实有效。匹配的 j = i - dp[i-1] - 1? 对于基本情况,匹配的 j 就是 i-1=3。所以基础单元是 s[3...4] = “()”dp[4]=4 是因为它连接了前面的 dp[2]=2(即 s[1...2]=“()”),所以 dp[4] = dp[2] + 2 = 4
  • i=5: ‘)’, 且 s[4]=‘)’ (嵌套情况)。j = 5 - dp[4] - 1 = 5-4-1=0s[0]=‘(’,匹配。dp[5] = dp[4] + 2 + dp[-1]? (j-1=-1, 视为0) = 4+2+0=6。匹配 j=0

现在计算 primitive_count

  • i=2: 匹配 j=1, primitive_count[2]=1 (子串 [1,2] -> “()”)
  • i=4: 匹配 j=3, primitive_count[4]=1 (子串 [3,4] -> “()”)
  • i=5: 匹配 j=0, primitive_count[5]=1 (子串 [0,5] -> “(()())”)

总和为 3。符合例子 “(()())” 输出 3 的预期(子串:[1,2], [3,4], [0,5])。

步骤 7:最终算法总结

  1. 初始化:创建长度为 n 的数组 dpprimitive_count,初始化为 0。total = 0
  2. 遍历 i 从 0 到 n-1:
    • 如果 s[i] == ‘)’
      • 如果 s[i-1] == ‘(’ (且 i > 0):
        • dp[i] = (dp[i-2] if i >= 2 else 0) + 2
        • j = i - 1 // 匹配的左括号位置
        • primitive_count[i] = 1
      • 否则如果 s[i-1] == ‘)’ (且 i > 0):
        • j = i - dp[i-1] - 1
        • 如果 j >= 0s[j] == ‘(’
          • dp[i] = dp[i-1] + 2 + (dp[j-1] if j >= 1 else 0)
          • primitive_count[i] = 1
    • total += primitive_count[i]
  3. 返回 total

复杂度分析

  • 时间复杂度:O(n),我们只遍历字符串一次。
  • 空间复杂度:O(n),用于存储 dpprimitive_count 数组(实际上 primitive_count 可以只用一个变量累加,不需要数组)。

这个算法巧妙地利用动态规划找出了每一个“本原”有效括号子串的结束位置,并进行了计数,最终得到了题目要求的结果。

好的,在仔细检查了你提供的庞大历史记录列表,并确保不重复之后,我将为你讲解一个经典的、但你可能还未听过的线性动态规划问题。 题目:最长有效括号匹配子序列的变种:统计所有不同的、连续的有效括号子串数量 题目描述: 给你一个只包含字符 '(' 和 ')' 的字符串 s 。一个 有效括号子串 定义为满足以下条件的 连续 子串: 所有括号都能正确匹配。 子串是连续的(即原始字符串中一段连续的字符序列)。 例如,对于字符串 "()(())" : 有效的连续括号子串有: "()" , "(())" , "()(())" 。 "())" 不是有效的,因为括号没有完全匹配。 "()()" 也是有效的,但在这个例子中不是连续的子串(在 "()(())" 中,它是两个独立的子串 "()" 和 "(())" 拼接而成的一个更大有效串)。 我们的目标是计算字符串 s 中所有 不同的、有效的连续括号子串 的数量。注意,即使两个子串内容相同,只要它们在字符串中的 起始和结束位置 不同,就被视为不同的子串。 例如: s = "()" , 输出:1 (子串: [0, 1] -> "()" ) s = "(())" ,输出:2 (子串: [1, 2] -> "()" , [0, 3] -> "(())" ) s = "()(())" ,输出:3 (子串: [0, 1] -> "()" , [3, 4] -> "()" , [2, 5] -> "(())" ) s = "(()())" ,输出:3 (子串: [1, 2] -> "()" , [3, 4] -> "()" , [0, 5] -> "(()())" ) 解题过程循序渐进讲解 这个问题是 “最长有效括号子串” 问题的延伸。最长有效括号子串问题通常使用栈或动态规划来找到 最长 的有效子串。而这里我们需要 统计所有 有效子串的数量。 核心思路: 我们可以利用动态规划。定义 dp[i] 表示 以字符 s[i] 为结尾的最长有效括号子串的长度 。如果能求出 dp[i] ,那么我们就能判断以 i 结尾的有效子串是什么,并据此进行计数。 为什么能计数? 因为对于以 i 结尾的最长有效括号子串,它的所有后缀(只要本身是有效的)也都是有效的括号子串。例如, "(()())" 以最后一个 ) 结尾的最长子串是 "(()())" ,它的有效后缀子串有: "()" (对应子串 [4, 5] ), "()" (对应子串 [1,2] 和 [3,4] ,但注意,以 i=5 结尾时,我们只关心从某个位置开始到 i 结束的子串), "(())" (对应 [2,5] ),以及整个 "(()())" 。我们需要一种系统的方法来统计它们。 步骤 1:定义动态规划状态 设 dp[i] 表示以字符 s[i] 结尾的 最长有效括号子串的长度 。如果以 s[i] 结尾无法形成有效括号子串,则 dp[i] = 0 。 例如,对于 s = “()(())” : i=1 ( ‘)’ ): 以它结尾的最长有效子串是 “()” ,长度 2,所以 dp[1] = 2 。 i=5 ( ‘)’ ): 以它结尾的最长有效子串是 “(())” ,长度 4,所以 dp[5] = 4 。 步骤 2:推导状态转移方程 我们如何计算 dp[i] ? 只有当前字符 s[i] 是 ‘)’ 时,才有可能形成以它结尾的有效子串(因为有效子串必须以 ‘)’ 结尾)。 情况分析: 基本情况 :如果 s[i] = ‘)’ 且 s[i-1] = ‘(’ ,即形如 “...()” 。 那么以 i 结尾的最长有效子串就是 s[i-1] 和 s[i] 组成的 “()” 。 此外,这个有效子串还可能和它前面紧邻的有效子串连接起来。所以: dp[i] = dp[i-2] + 2 ,前提是 i >= 2 。如果 i-2 不存在,则 dp[i] = 2 。 嵌套情况 :如果 s[i] = ‘)’ 且 s[i-1] = ‘)’ ,即形如 “...))” 。 这意味着当前 ‘)’ 可能是一个外层括号的右括号。例如 “(()())” ,最后一个 ‘)’ 匹配的是索引 0 的 ‘(’ 。 我们需要找到与当前 s[i] 匹配的左括号的位置。这个位置是 j = i - dp[i-1] - 1 。 dp[i-1] 是以 s[i-1] 结尾的最长有效子串长度。 i - dp[i-1] - 1 就是跳过这个有效子串后,前面的一个字符。 如果 j >= 0 且 s[j] == ‘(’ ,说明我们找到了匹配的左括号。 那么,以 s[i] 结尾的有效子串长度至少是 dp[i-1] + 2 (内部的 dp[i-1] 长的有效子串加上外面一对括号)。 此外,这个新形成的有效子串还可能和它前面 (j-1) 结尾的有效子串拼接起来。所以: dp[i] = dp[i-1] + 2 + (dp[j-1] if j-1 >= 0 else 0) 状态转移方程总结: 如果 s[i] == ‘)’ : 如果 i > 0 且 s[i-1] == ‘(’ : dp[i] = (dp[i-2] if i >= 2 else 0) + 2 如果 i > 0 且 s[i-1] == ‘)’ : j = i - dp[i-1] - 1 如果 j >= 0 且 s[j] == ‘(’ : dp[i] = dp[i-1] + 2 + (dp[j-1] if j >= 1 else 0) 如果 s[i] == ‘(’ : dp[i] = 0 步骤 3:统计所有有效子串 我们有了 dp 数组。现在关键在于: 如何利用 dp[i] 统计所有以 i 结尾的有效子串? 观察到一个重要性质: 对于一个以索引 i 结尾的、长度为 L ( L = dp[i] ) 的最长有效括号子串,所有以 i 结尾的更短的有效子串,必然是这个最长子串的某个后缀,并且这些后缀的有效性可以通过检查它们的长度是否是“有效单位”的偶数倍,并满足括号匹配规则来间接确认吗? 更简单直接的方法:当我们计算出 dp[i] 后, 以 i 结尾的有效子串数量,就等于以 i 结尾的最长有效子串中,所有合法的“前缀-后缀”对的数量吗? 不完全是。 更巧妙的计数方法: 我们引入第二个状态 count[i] ,它表示 以字符 s[i] 结尾的有效括号子串的数量 。 count[i] 的推导依赖于 dp[i] : 如果 dp[i] > 0 (即以 s[i] 结尾存在有效子串): 那么,以 s[i] 结尾的最短有效子串,就是和 s[i] 匹配的那个左括号 s[j] 到 s[i] 形成的子串 s[j...i] ,其长度为 len1 = i - j + 1 。这个 j 就是我们计算 dp[i] 时找到的 j 。 这个子串 s[j...i] 本身就是一个有效子串,所以 count 至少为 1。 更重要的是,以 i 结尾的其他有效子串,都可以看作是:在 s[j...i] 这个 基础有效单元 的 前面 ,拼接上若干个以 s[j-1] 结尾的有效子串(如果存在的话)。因为 s[j...i] 自身是有效的,前面再拼一个有效的子串 s[k...j-1] ,结果 s[k...i] 仍然是有效的。 所以, 以 i 结尾的有效子串总数 = 1 (基础单元 s[j...i] ) + 以 j-1 结尾的有效子串总数 ( count[j-1] ) 。 即: count[i] = 1 + (count[j-1] if j-1 >= 0 else 0) 如果 dp[i] == 0 ,则 count[i] = 0 。 为什么这样是对的? 因为任何以 i 结尾的有效子串,其最后一个字符 ‘)’ 必然匹配某个 ‘(’ 在位置 j 。这个子串必然可以分解为 A + B ,其中 B = s[j...i] 是一个最小的、不可再分割的、以 i 结尾的有效单元(即 B 内部是有效匹配的,且 s[j] 和 s[i] 匹配)。 A 是 s[0...j-1] 的某个以 j-1 结尾的有效后缀(可以为空)。 A 的有效后缀数量正好就是 count[j-1] 。所以总数量是 1 + count[j-1] 。 步骤 4:计算最终答案 字符串 s 中所有不同的有效括号子串总数 ans ,就是所有 count[i] 的和。 ans = sum(count[i]) for i in range(n) 因为 count[i] 精确地统计了以每个位置 i 结尾的所有有效子串,且每个有效子串都有唯一的结束位置 i ,所以求和即可得到总数。 步骤 5:算法步骤与示例 让我们用 s = “()(())” 来演示。 初始化 dp = [0, 0, 0, 0, 0, 0] , count = [0, 0, 0, 0, 0, 0] 。 ans = 0 。 i=0 , s[0]=‘(’ , dp[0]=0 , count[0]=0 。 i=1 , s[1]=‘)’ ,且 s[0]=‘(’ (基本情况)。 j = i - dp[i-1] - 1 不适用,我们走 s[i-1]==‘(’ 分支。 dp[1] = (dp[-1]? 视为0) + 2 = 2 。 找到与 s[1] 匹配的左括号位置 j = 1 - dp[0] - 1? 不,对于基本情况,匹配的左括号就是 i-1=0 。 j = 0 。 count[1] = 1 + (count[-1]? 视为0) = 1 。 ans += 1 (现在 ans=1 ,对应子串 [0,1] -> “()” )。 i=2 , s[2]=‘(’ , dp[2]=0 , count[2]=0 。 i=3 , s[3]=‘(’ , dp[3]=0 , count[3]=0 。 i=4 , s[4]=‘)’ ,且 s[3]=‘(’ (基本情况)。 dp[4] = dp[2] + 2 = 0 + 2 = 2 。 匹配的左括号 j = 3 。 count[4] = 1 + count[2] = 1 + 0 = 1 。 ans += 1 (现在 ans=2 ,对应新增子串 [3,4] -> “()” )。 i=5 , s[5]=‘)’ ,且 s[4]=‘)’ (嵌套情况)。 j = i - dp[i-1] - 1 = 5 - dp[4] - 1 = 5 - 2 - 1 = 2 。 s[2] = ‘(’ ,匹配成功。 dp[5] = dp[4] + 2 + dp[1] = 2 + 2 + 2 = 6 ? 等等, dp[1] 是 dp[j-1] = dp[2-1]=dp[1]=2 。所以 dp[5]=2+2+2=6 。这表示以 i=5 结尾的最长有效子串是 s[0...5] 即 “()(())” ,长度 6。 匹配的左括号位置 j = 2 。 count[5] = 1 + count[1] = 1 + 1 = 2 。 这两个以 i=5 结尾的有效子串分别是: a. 基础单元 s[2...5] -> “(())” (对应 count=1 中的 1 )。 b. 基础单元前面拼接上以 j-1=1 结尾的有效子串 s[0...1] -> “()” ,得到 s[0...5] -> “()(())” (对应 count=1 中的 count[1]=1 )。 ans += 2 (现在 ans=4 )。等等,我们的例子预期是 3。哪里出错了? 问题分析: 我们的推导似乎多了一个。检查 s = “()(())” : 以 i=1 结尾的有效子串: [0,1] -> “()” 。 以 i=4 结尾的有效子串: [3,4] -> “()” 。 以 i=5 结尾的有效子串: [2,5] -> “(())” 和 [0,5] -> “()(())” 。 总数为 4。但题目描述的例子说输出是 3。矛盾在于题目描述!让我们重新审视题目描述中的例子。 题目描述说: s = “()(())” ,输出 3,子串为 [0,1] , [3,4] , [2,5] 。它 没有 把 [0,5] 算进去。为什么? 因为 [0,5] 对应的子串是 “()(())” ,它虽然整体是有效的,但它是由两个独立的有效子串 “()” 和 “(())” 拼接 而成的。在许多这类计数问题的变种中, “连续的有效括号子串”有时被隐式定义为“不可再被分割为两个连续的有效括号子串” ,或者说** “不能被更小的连续有效子串覆盖”** 。这更像是在统计“有效括号子串”的“原子”单位或“极大”单位。 修正题目理解与状态定义: 因此,我们需要明确:我们想统计的是 所有满足“该子串本身有效,且其任何真前缀都不是有效括号串”的有效括号子串 吗?不,那样会漏掉 “()(())” 这样的串。 更常见的统计目标是: 统计所有“不能被扩展”的有效括号子串 ,即对于子串 s[l...r] ,如果它是有效的,并且 s[l-1...r] 和 s[l...r+1] 都不是有效的(如果存在的话),那么它就是一个“极大”有效子串。但我们的例子中 [0,5] 是极大的, [0,1] 和 [2,5] 不是极大的(因为它们被包含在 [0,5] 里)。这与题目例子不符。 根据例子反推 :题目例子似乎是在统计 所有形如 “(……)” 的、最外层括号匹配的、且内部也是有效的子串 ,但它把 “()(())” 这种由两个独立单元 () 和 (()) 并列而成的串,只算作两个单元 ( [0,1] 和 [2,5] 或 [3,4] 和 [0,5] ? 不对)……例子中的三个子串是 [0,1] , [3,4] , [2,5] 。注意 [2,5] 是 (()) ,它是一个完整的、内部嵌套的有效单元。 [0,1] 和 [3,4] 都是 () 。 [0,5] 没有被计入。 所以,题目的真实意图可能是: 统计所有“原始的”、“不可分割的”有效括号子串 ,即该有效子串不能被划分为两个或多个连续的非空有效括号子串。在 “()(())” 中, [0,5] 可以被划分为 [0,1] 和 [2,5] 或 [0,3] 和 [4,5] 等,所以它不计入。而 [0,1] 、 [3,4] 、 [2,5] 都不能被这样划分。 这引导我们修改计数逻辑。 步骤 6:修正计数逻辑(统计“不可分割”的有效子串) 我们需要修改 count[i] 的定义或引入新的判断。 新思路 : 我们依然计算 dp[i] 。当我们找到以 i 结尾的最长有效子串的匹配左括号 j 后,形成的有效子串 s[j...i] 是一个候选。 这个子串 s[j...i] 是“不可分割的”当且仅当 dp[i] == i - j + 1 并且 dp[i] 就是原始计算出的长度,没有和前面的 dp[j-1] 连接 ?不完全是。 实际上, 一个有效括号子串 s[l...r] 是“不可分割的”(或称“本原有效的”)的充要条件是:对于任何满足 l < k < r 的 k ,子串 s[l...k] 都不是有效的括号串 。换句话说,除了 r 之外,任何真前缀都不是有效的。 如何在动态规划中识别? 当我们按照之前的方法计算 dp[i] 时: 在“基本情况” () 下, s[j...i] (其中 j=i-1 ) 一定是本原有效的,因为它的真前缀只有一个字符 ‘(’ ,不是有效的。 在“嵌套情况” “(...)” 下, s[j...i] 也是本原有效的,因为它的第一个字符是 ‘(’ ,最后一个字符是 ‘)’ ,并且内部 s[j+1...i-1] 是一个有效的子串(由 dp[i-1] 定义)。它的任何真前缀 s[j...k] (对于 k < i ) 都会多一个 ‘(’ 而缺少对应的 ‘)’ ,因此无效。 但是 ,问题出在当 dp[i] 的计算包含了 dp[j-1] 的部分时(即 dp[i] = dp[i-1] + 2 + dp[j-1] ),我们实际上将两个本原有效子串 s[?..j-1] 和 s[j...i] 连接 成了一个更大的有效子串 s[?..i] 。这个更大的子串 s[?..i] 不是 本原有效的,因为它可以被分割。 因此, 要统计本原有效的子串,我们只应该计数那些在计算 dp[i] 时,没有与前面子串连接的情况 。即,我们只计数“基础单元” s[j...i] 本身,而不是 s[?..i] 。 修正后的计数方法: 我们定义 primitive_count[i] 表示以 i 结尾的 本原有效括号子串 的数量。 如果 s[i] == ‘)’ 且我们找到了匹配的 j (使得 s[j] == ‘(’ ): 那么 s[j...i] 一定是一个本原有效子串。 所以 primitive_count[i] = 1 。 注意 :即使 dp[i] 因为连接了前面的有效子串而变得更长,我们也不为那个更长的、非本原的子串计数。我们只计 s[j...i] 这一个。 否则, primitive_count[i] = 0 。 那么,整个字符串 s 中所有本原有效子串的总数就是 sum(primitive_count[i]) 。 用修正后的方法验证例子 s = “()(())” : i=1 : 匹配 j=0 , primitive_count[1] = 1 。子串 [0,1] 。 i=4 : 匹配 j=3 , primitive_count[4] = 1 。子串 [3,4] 。 i=5 : 匹配 j=2 , primitive_count[5] = 1 。子串 [2,5] 。 总和为 3,符合例子。 再验证 s = “(()())” : i=1 : s[1]=‘)’ , s[0]=‘(’ , primitive_count[1]=1 (子串 [0,1] ? 不对,应该是 [0,1] -> “()” ?等等, s[0]=‘(’, s[1]=‘)’ ,确实是 “()” 。) i=2 : s[2]=‘(’ , primitive_count[2]=0 。 i=3 : s[3]=‘(’ , primitive_count[3]=0 。 i=4 : s[4]=‘)’ , s[3]=‘(’ , primitive_count[4]=1 (子串 [3,4] -> “()” )。 i=5 : s[5]=‘)’ , s[4]=‘)’ , 计算 j = 5 - dp[4] - 1 = 5 - 2 - 1 = 2 。 s[2]=‘(’ ,匹配。 primitive_count[5] = 1 (子串 [2,5] -> “()()” ? 等等, s[2]=‘(’, s[3]=‘(’, s[4]=‘)’, s[5]=‘)’ ,即 “(()())” 中的内层 “()” 和外层括号?不对, j=2 , i=5 ,子串是 s[2...5] = “(())” ?我们检查字符串: s = “(()())” , 索引: 0:(, 1:(, 2:), 3:(, 4:), 5:)。 s[2...5] = “)(()” ,这显然不对。我犯了一个错误。 重新计算 s = “(()())” 的 dp 数组 : dp = [0,0,0,0,0,0] i=0 : ‘(’ , dp=0 i=1 : ‘(’ , dp=0 i=2 : ‘)’ , 且 s[1]=‘(’ (基本情况), dp[2] = dp[0] + 2 = 0+2=2 。匹配 j=1 。 i=3 : ‘(’ , dp=0 i=4 : ‘)’ , 且 s[3]=‘(’ (基本情况), dp[4] = dp[2] + 2 = 2+2=4 。匹配 j=3 ? 等等,基本情况匹配的是 i-1=3 。 dp[4]=4 意味着以 i=4 结尾的最长子串是 s[1...4] = “(()())” 中的 “()()” ? s[1...4] 是 “(()” ,不对。 dp[4]=4 表示子串长度为4,即 s[1...4] ( “()()” ) 是有效的吗? “()()” 是有效的,长度4。但 s[1] 是 ‘(’ , s[4] 是 ‘)’ ,中间 s[2] 是 ‘)’ , s[3] 是 ‘(’ ,即 “()()” ,确实有效。匹配的 j = i - dp[i-1] - 1 ? 对于基本情况,匹配的 j 就是 i-1=3 。所以基础单元是 s[3...4] = “()” 。 dp[4]=4 是因为它连接了前面的 dp[2]=2 (即 s[1...2]=“()” ),所以 dp[4] = dp[2] + 2 = 4 。 i=5 : ‘)’ , 且 s[4]=‘)’ (嵌套情况)。 j = 5 - dp[4] - 1 = 5-4-1=0 。 s[0]=‘(’ ,匹配。 dp[5] = dp[4] + 2 + dp[-1]? (j-1=-1, 视为0) = 4+2+0=6 。匹配 j=0 。 现在计算 primitive_count : i=2 : 匹配 j=1 , primitive_count[2]=1 (子串 [1,2] -> “()” ) i=4 : 匹配 j=3 , primitive_count[4]=1 (子串 [3,4] -> “()” ) i=5 : 匹配 j=0 , primitive_count[5]=1 (子串 [0,5] -> “(()())” ) 总和为 3。符合例子 “(()())” 输出 3 的预期(子串: [1,2] , [3,4] , [0,5] )。 步骤 7:最终算法总结 初始化 :创建长度为 n 的数组 dp 和 primitive_count ,初始化为 0。 total = 0 。 遍历 i 从 0 到 n-1: 如果 s[i] == ‘)’ : 如果 s[i-1] == ‘(’ (且 i > 0 ): dp[i] = (dp[i-2] if i >= 2 else 0) + 2 j = i - 1 // 匹配的左括号位置 primitive_count[i] = 1 否则如果 s[i-1] == ‘)’ (且 i > 0 ): j = i - dp[i-1] - 1 如果 j >= 0 且 s[j] == ‘(’ : dp[i] = dp[i-1] + 2 + (dp[j-1] if j >= 1 else 0) primitive_count[i] = 1 total += primitive_count[i] 返回 total 。 复杂度分析 : 时间复杂度:O(n),我们只遍历字符串一次。 空间复杂度:O(n),用于存储 dp 和 primitive_count 数组(实际上 primitive_count 可以只用一个变量累加,不需要数组)。 这个算法巧妙地利用动态规划找出了每一个“本原”有效括号子串的结束位置,并进行了计数,最终得到了题目要求的结果。