1. 题目数据 (Problem Metadata)

2. 题意简述 (Problem Summary)

给定长度为 nn1n1061 \le n \le 10^6)的字符串 ss,仅由 () 组成。设 LLss最长合法括号子串的长度,CC 为长度恰为 LL 的合法括号子串个数。输出 LLCC;若不存在任何合法括号子串,输出 0 1

其中"合法括号序列"指可以通过在其中插入 +1 得到正确数学表达式的序列,等价于经典定义:空串合法;若 AA 合法则 (A)(A) 合法;若 A,BA,B 合法则 ABAB 合法。

3. 朴素解法 (Brute-Force)

枚举所有子串区间 [l,r][l,r]O(n2)O(n^2) 个,对每个子串用栈判定是否合法(O(n)O(n)),总复杂度 O(n3)O(n^3);或预处理括号前缀和后用"区间左右括号数相等且任意前缀左括号数 \ge 右括号数"判定,降至 O(n2)O(n^2)。当 n=106n = 10^6 时均远超时限,必须利用子串的嵌套与拼接结构做线性 DP。

4. 核心解法 (Main Solution)

  • 特殊性质:合法括号子串具有"嵌套 + 拼接"两种组合方式:(A)(A) 仍合法,ABAB 仍合法。这意味着"以 ii 结尾的最长合法子串"可以由更短的合法段在常数步内拼出。
  • 关键突破:从 O(n2)O(n^2) 降到的关键在于只跟踪"以每个位置结尾"的最长合法段,而非所有区间。定义 dp[i]dp[i] 表示以 sis_i 结尾的最长合法括号子串长度。利用已算出的 dp[j] (j<i)dp[j]\ (j<i) 进行状态转移计算出 dp[i]dp[i],可以把总复杂度降低到 O(n)O(n)
  • 推导过程
    • si=s_i = (:无法作为合法子串的结尾,dp[i]=0dp[i] = 0
    • si=s_i = ),分两种情况(设 s0s_0 为非括号占位符,避免越界,同时规定 dp[0]=0dp[0]=0):
      1. 直接配对si1=s_{i-1} = (
        此情况下,si1,sis_{i-1},s_i 自身构成一段 ()。它前面还可能紧贴一个以 i2i-2 结尾的合法段,两段通过拼接的方式组合,有 $$dp[i] = dp[i-2] + 2.$$
      2. 跨段配对si1=s_{i-1} = )dp[i1]>0dp[i-1] > 0
        此情况下,设以 i1i-1 结尾的合法段为 s[idp[i1]..i1]s[i-dp[i-1]\,..\,i-1],则与 sis_i 配对的那个 ( 必须落在位置 j=idp[i1]1j = i - dp[i-1] - 1。若 sj=s_j = (,则 sjs_jsis_i 把中间的合法段包住,整体合法,长度为 dp[i1]+2dp[i-1] + 2;此外 jj 之前还可能紧贴一个以 j1j-1 结尾的合法段,于是 $$dp[i] = dp[i-1] + 2 + dp[i - dp[i-1] - 2].$$
        sjs_j \neq (,则 sis_i 找不到配对的 (dp[i]=0dp[i] = 0

统计答案:扫一遍 dpdp,维护最大值 max\max 与计数 cntcnt。注意长度为 00不计入计数,保证无合法子串时输出 0 1

5. 正确性证明 (Proof of Correctness)

只需证明问题具有最优子结构性质,且 dp[i]dp[i] 的分类讨论完备且无后效性

  • 无后效性dp[i]dp[i] 的转移只依赖 j<ij < idp[j]dp[j]sjs_j 本身,不依赖 ii 之后的信息,因此从左到右一轮扫描即可确定。
  • 最优子结构(对结尾为 ) 的两种情况)
    • 情况 1 中,si1si=s_{i-1}s_i = () 是以 ii 结尾的最短合法后缀。任何以 ii 结尾的合法子串若长度 >2> 2,其倒数第二个字符只能是 )(;前者归入情况 2,后者意味着紧邻的 () 前面还有合法段,长度恰为 dp[i2]+2dp[i-2]+2。由于 dp[i2]dp[i-2] 已是以 i2i-2 结尾的最长值,dp[i2]+2dp[i-2]+2 即此情形下的最优。
    • 情况 2 中,以 i1i-1 结尾的合法段 T=s[idp[i1]..i1]T = s[i-dp[i-1]\,..\,i-1]最长的,故与 sis_i 配对的 ( 只可能在 j=idp[i1]1j = i - dp[i-1] - 1(更靠左则 TT 不再是以 i1i-1 结尾的最长段)。若 sj=s_j = (,则 sjTsis_j\,T\,s_i 整体合法;其左侧以 j1j-1 结尾的最长合法段长度为 dp[j1]dp[j-1],拼接后得到 dp[i1]+2+dp[j1]dp[i-1]+2+dp[j-1],由 dp[j1]dp[j-1] 的最优性知此即以 ii 结尾的最长值。若 sjs_j \neq (,则不存在以 sis_i 配对的 (dp[i]=0dp[i]=0
  • 两种情况穷尽了") 能否成为某合法子串结尾"的所有可能,综上所述 DP 算法正确。

6. 复杂度分析 (Complexity)

  • 时间复杂度O(n)O(n)。状态复杂度 O(n)O(n),转移复杂度常数,每个位置 ii 只做常数次数组访问与比较,n=106n = 10^6 时约几百万次运算,2 秒时限充裕。
  • 空间复杂度O(n)O(n),即 dp 数组与占位后的字符串,远低于 256 MB。

7. 实现细节与避坑指南 (Implementation Details)

  • 下标占位:代码用 string s = " " + str; 使 s0=s_0 = ' '(非括号)。这样 dp[i-2]s[i - dp[i-1] - 1]i=2i=2dp[i1]=i2dp[i-1]=i-2 等极端位置也不会越界,且 s0s_0 \neq ( 保证了"找不到配对"分支的正确性。若不占位,必须额外处理 i=1i=1i - dp[i-1] - 1 = 0 的边界。
  • 计数的 0 1 特例update_maxelse if (v == maxv && v) cnt++;&& v 保证了长度为 00 的"段"不被计数,初始 cnt = 1 则覆盖了"无合法子串输出 0 1"的要求,无需单独特判。
  • 初始情况dp 为全局数组默认全 00;本题单组数据无需清空,若改为多组数据需手动重置相关区间。
  • 读入:用 cin >> s 跳过前导空白并读到第一个空白符为止,恰好对应单行括号串。

8. 参考代码 (Reference Code)

下面给出 DP 写法,为推导略复杂但代码最简洁的 O(n)O(n) 解法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <bits/stdc++.h>
using namespace std;

const int N = 1e6 + 5;
int dp[N];

// 更新最大值与计数:长度为 0 时不计入计数
void update_max(int v, int &maxv, int &cnt) {
if (v > maxv) maxv = v, cnt = 1;
else if (v == maxv && v) cnt++;
}

int main() {
ios::sync_with_stdio(false);
string str;
cin >> str;
int n = str.length();
string s = " " + str; // s[0]=' ' 占位,避免下标越界与误配对

for (int i = 2; i <= n; i++) {
if (s[i] == ')') {
if (s[i - 1] == '(')
// 情况1:直接与前一字符配对,再接上更前的合法段
dp[i] = dp[i - 2] + 2;
else if (s[i - dp[i - 1] - 1] == '(')
// 情况2:跨过以 i-1 结尾的合法段,与更早的 '(' 配对
dp[i] = dp[i - 1] + 2 + dp[i - dp[i - 1] - 2];
}
}

int maxdp = 0, cnt = 1;
for (int i = 1; i <= n; i++)
update_max(dp[i], maxdp, cnt);
cout << maxdp << ' ' << cnt << endl;
return 0;
}

9. 补充说明 (Additional Notes)

  • 经典模型:本题是"最长合法括号子串"系列的最基础版本,与 LeetCode 32. Longest Valid Parentheses 同构,区别仅在输出要求多了"个数统计"。DP、栈、双向贪心三种主流解法在此题上等价可达 O(n)O(n)
  • 计数语义的陷阱:题面"个数"指不同位置的子串个数(同一合法子串若可由不同 [l,r][l,r] 表示则分别计数),所以即便两段合法子串内容相同,只要位置不同就各计一次。dp + update_max 的方式按位置计数,可以做到不重不漏。

其他版本

本题我学习了三种解法,下面对比列出。

方法2(栈 + 最长连续段,O(n)O(n)

思路最直观——用栈完成括号匹配,匹配成功的位置在 flag 中标记,最后统计 flag 连续段的长度与个数。是使用栈进行括号匹配的应用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bool flag[N];

void calc_RBS_2(const string &str) {
int n = str.length();
const string s = " " + str;
stack<int> stk;
for (int i = 1; i <= n; i++)
if (s[i] == '(') stk.push(i);
else if (!stk.empty()) {
flag[i] = flag[stk.top()] = 1;
stk.pop();
}
int maxl = 0, cnt = 1, l = 0;
for (int i = 1; i <= n; i++) {
if (flag[i]) l++;
else update_max(l, maxl, cnt), l = 0;
}
update_max(l, maxl, cnt);
cout << maxl << ' ' << cnt << endl;
}

方法3(双向贪心 + set 去重)

从左到右、从右到左各扫一遍,用 left/right 计数在归零时记录合法段,并用 set<pair<int,int>> 去重。两次扫描分别覆盖"左括号多余"和"右括号多余"两种断点情况。

  • 理论复杂度set::insert 单次 O(logn)O(\log n),最坏情况下整体 O(nlogn)O(n \log n)
  • 实际开销set::insert 只在扫描中 left/right 归零且当前长度 \ge 已知最大值时才触发,而最长合法子串的数量通常远小于 nn,使得实际插入次数很少。经实测在 n=106n = 10^6 的数据下仅比 DP/栈写法慢约 30ms,2 秒时限下毫无压力。
  • 去重设计的合理性:两次扫描(从左、从右)会捕获同一合法段各一次,但表示区间的端点定义不同——左扫描用 [i-lth+1, i],右扫描用 [i, i+lth-1],因此同一区间两次插入的结果在 set 中天然一致,靠 set 去重是正确且简洁的做法,并非多余。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
void calc_RBS_3(const string &str) {
int n = str.length();
const string s = " " + str;
int left = 0, right = 0, lth = 0, maxl = 0;
set<pair<int, int>> st_cnt;

// 从左到右:处理"右括号不足"导致的断点
for (int i = 1; i <= n; i++) {
if (s[i] == '(') left++;
else if (!left) lth = 0;
else {
left--, lth += 2;
if (!left) { // left 归零 = 一段完整合法
if (lth > maxl) {
maxl = lth;
st_cnt.clear();
st_cnt.insert({i - lth + 1, i});
} else if (lth == maxl)
st_cnt.insert({i - lth + 1, i});
}
}
}

// 从右到左:处理"左括号不足"导致的断点
lth = 0;
for (int i = n; i; i--) {
if (s[i] == ')') right++;
else if (!right) lth = 0;
else {
right--, lth += 2;
if (!right) {
if (lth > maxl) {
maxl = lth;
st_cnt.clear();
st_cnt.insert({i, i + lth - 1});
} else if (lth == maxl)
st_cnt.insert({i, i + lth - 1});
}
}
}

int cnt = (st_cnt.size() ? st_cnt.size() : 1);
cout << maxl << ' ' << cnt << endl;
}

三种解法对比小结:DP 写法状态最紧凑、转移直接;栈写法思路最直观、适合初学者;双向贪心 + set 写法思路最巧妙——通过两次反向扫描覆盖两类断点,用 set 统一去重,避免了去重逻辑的显式编码。实际性能差距在 30ms 量级,三者都是可提交的优质解法,方法经典,都有学习的价值。


本站由 zaochen 使用 Stellar 1.33.1 主题创建。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
全站访问量 - 次 · 访客数 - 人 · 本页面浏览 -