定义
一般地,定义一个括号序列是仅由 ( 与 ) 组成的字符串。合法括号序列的定义为:
空串为一个合法括号序列;
若 s 为合法括号序列,则 (s) 也是合法括号序列;
若 s、t 均为合法括号序列,则 st 也是合法括号序列。
括号序列其实是一种模型,描述一类二元对应平衡关系。有时还会有 []、{}、<> 等不同的括号。
经典问题
合法性判断
栈的经典应用。令 n = ∣ s ∣ n=|s| n = ∣ s ∣ ,维护一个栈,枚举 i ∈ [ 1 , n ] i \in [1,n] i ∈ [ 1 , n ] :
若 s i s_i s i 为右括号,且栈顶为对应的左括号,弹出栈顶;
否则,将 s i s_i s i 入栈。
复杂度为 O ( n ) O(n) O ( n ) 。代码:
1 2 3 4 5 6 7 8 9 bool check (const string& s) { stack<char > st; int n = s.size (); for (int i = 0 ; i < n; i++) { if (s[i] == ')' && !st.empty () && st.top () == '(' ) st.pop (); else st.emplace (s[i]); } return st.empty (); }
合法括号序列计数
设 H n H_n H n 为 n n n 对括号能形成的合法括号序列数。设合法序列的形式为 (a)b,其中 a 的括号对数为 m m m ,则 b b b 的括号对数为 n − m − 1 n-m-1 n − m − 1 ,则有
H n = ∑ m = 0 n − 1 H i H n − m − 1 H_n=\sum_{m=0}^{n-1}H_i H_{n-m-1}
H n = m = 0 ∑ n − 1 H i H n − m − 1
这是 Catalan 数的递推式。计算公式可见 组合数学复习笔记 中 Catalan 数相关部分。
字典序后继
在这里我们认为左括号的字典序小于右括号。
找到一个最大的 i i i ,满足:
s i s_i s i 为左括号;
[ 1 , i − 1 ] [1,i-1] [ 1 , i − 1 ] 中左括号的数量严格大于右括号数量
于是将 s i s_i s i 变为右括号。设此时 [ 1 , i ] [1,i] [ 1 , i ] 中左括号比右括号多 k k k 个,贪心地,令最后 k k k 个为右括号,剩余部分用 ((((())))) 这样的形式填充。复杂度 O ( n ) O(n) O ( n ) 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 bool next_sequence (string& s) { int n = s.size (); for (int i = n - 1 , cnt = 0 ; i >= 0 ; i--) { if (s[i] == '(' ) cnt--; else cnt++; if (s[i] == '(' && cnt > 0 ) { cnt--; int x = (n - i - 1 - cnt) / 2 , y = n - i - 1 - x; string t = s.substr (0 , i) + ')' + string (x, '(' ) + string (y, ')' ); s.swap (t); return true ; } } return false ; }
字典序计算
令 n = ∣ s ∣ n=|s| n = ∣ s ∣ ,求比 s s s 字典序小的括号序列 t t t 的数目。枚举一个 i i i ,表示 ∀ 1 ≤ j < i t j = s j \forall 1\le j <i t_j=s_j ∀1 ≤ j < i t j = s j ,而 t i < s i t_i<s_i t i < s i ,即 s i s_i s i 为右括号而 t i t_i t i 为左括号。
设在 [ 1 , i ] [1,i] [ 1 , i ] 中左括号比右括号多 k k k 个,则 [ i + 1 , n ] [i+1,n] [ i + 1 , n ] 中就少 k k k 个。设 d p i , j dp_{i,j} d p i , j 为长度为 i i i 的括号序列,上述讨论中 k = j k=j k = j 的个数。有:
d p i , j = d p i − 1 , j − 1 + d p i − 1 , j + 1 dp_{i,j}=dp_{i-1,j-1}+dp_{i-1,j+1}
d p i , j = d p i − 1 , j − 1 + d p i − 1 , j + 1
预处理 O ( n 2 ) O(n^2) O ( n 2 ) ,查询 O ( n ) O(n) O ( n ) 。Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 int dp[N][N];void solve (int n) { dp[0 ][0 ] = 1 ; for (int i = 1 ; i <= n; i++) { dp[i][0 ] = dp[i - 1 ][1 ]; for (int j = 1 ; j <= n; j++) dp[i][j] = dp[i - 1 ][j - 1 ] + dp[i - 1 ][j + 1 ]; } }long long rnk (const string& s) { int n = s.size (), cnt = 0 ; long long res = 1 ; for (int i = 1 ; i <= n; i++) { if (s[i - 1 ] == ')' ) res += dp[n - i][cnt + 1 ], cnt--; else cnt++; } return res; }
求字典序为 k k k 的括号序列
上面的 d p dp d p 仍然管用。我们从前往后思考,考虑第 i i i 个位置是否能为 )。如果 s i s_i s i 是 ),则有 k > d p n − i , c n t + 1 k > dp_{n-i,cnt+1} k > d p n − i , c n t + 1 ,其中 c n t cnt c n t 是当前左括号的数量与右括号数量之差。
若不能填右括号,且填一个左括号不会让后面怎么填都不合法了,也就是 c n t < 1 2 n cnt < \dfrac{1}{2} n c n t < 2 1 n ,则填一个左括号。反之,填右括号并令 k ← k − d p n − i , c n t + 1 k \gets k-dp_{n-i,cnt+1} k ← k − d p n − i , c n t + 1 。预处理 O ( n 2 ) O(n^2) O ( n 2 ) ,查询 O ( n ) O(n) O ( n ) 。
1 2 3 4 5 6 7 8 9 10 11 12 13 string kth (int n, long long k) { string s (n, ' ' ) ; int cnt = 0 ; for (int i = 1 ; i <= n; i++) { if (cnt + 1 <= n / 2 && dp[n - i][cnt + 1 ] >= k) s[i - 1 ] = '(' , cnt++; else { s[i - 1 ] = ')' ; if (cnt + 1 <= n / 2 ) k -= dp[n - i][cnt + 1 ]; cnt--; } } return s; }
例题
括号序列的例题是很活的。
括号匹配基础练手题。我这里介绍一个模板,用一个 01 串的每一位表示该位是否匹配,本题所求即为最长的连续 1 子串的下标。这个 01 串用栈即可方便地求出,复杂度 O ( n ) 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 const int N = 1e6 + 5 ;int n;char s[N];bool ok[N];void solve () { stack<pair<char , int >> st; for (int i = 1 ; i <= n; i++) { if (!st.empty () && ((st.top ().first == '(' && s[i] == ')' ) || (st.top ().first == '[' && s[i] == ']' ))) { ok[i] = ok[st.top ().second] = true ; st.pop (); } else { st.emplace (s[i], i); } } }void _main() { cin >> (s + 1 ); n = strlen (s + 1 ); solve (); int mxlen = 0 , l = 0 , r = 0 ; for (int i = 1 ; i <= n; ) { while (i <= n && !ok[i]) i++; int start = i; while (i <= n && ok[i]) i++; if (i - start > mxlen) mxlen = i - start, l = start, r = i - 1 ; } for (int i = l; i <= r; i++) cout << s[i]; }
树上的括号匹配。考虑一个简单的树形 DP,令 d p u dp_u d p u 为 [ 1 , u ] [1,u] [ 1 , u ] 的合法子串数目。
在全局维护一个栈,和传统方法一样存左括号下标。分类讨论:
若 s u s_u s u 为 (,直接入栈,令 d p u ← 0 dp_u \gets 0 d p u ← 0 ;
若 s u s_u s u 为 ) 但栈为空,令 d p u ← 0 dp_u \gets 0 d p u ← 0 ;
若 s u s_u s u 为 ),设栈顶为 t t t 并出栈,令 d p u ← d p f a t + 1 dp_u \gets dp_{fa_t}+1 d p u ← d p f a t + 1 。这里的贡献为 t t t 父亲那里的合法子串加上 ( t , u ) (t,u) ( t , u ) 这对括号,或者就是单独一对括号。注意这里的出栈操作最后要回溯回来。
复杂度为 O ( n ) O(n) O ( n ) 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 int n, fa[N], st[N];char s[N];long long dp[N], ans;void dfs (int u, long long pre, int top) { int t = 0 ; if (s[u] == '(' ) st[++top] = u; else if (top) t = st[top--], dp[u] = dp[fa[t]] + 1 , pre += dp[u]; ans ^= pre * u; for (int j = head[u]; j != 0 ; j = edge[j].next) dfs (edge[j].to, pre, top); if (t) st[++top] = t; }void _main() { cin >> n >> (s + 1 ); for (int i = 2 ; i <= n; i++) cin >> fa[i], add_edge (fa[i], i); dfs (1 , 0 , 0 ); cout << ans; }
对于一个不合法的括号序列,如 )((())(,如果我们把已经匹配的位置删去,结果为 )((,所以使它合法的添加方式是:在左侧加左括号,在右侧加右括号。
两种情况显然同理,不妨先看左侧。以样例为例,如果我们将序列按右括号断开,则可以写成
1 2 3 4 5 (x (x (x (x ((xx ((xx(x ((x (xx (((xxx
所以“本质不同”的添加结果实际上就是不同的划分方案,这种方案就是在连续的 ))) 之间的位置加入若干左括号,可以想到 DP。
令 d p i , j dp_{i,j} d p i , j 表示第 i i i 个右括号前加上 j j j 个左括号的方案数,同时设 a i a_i a i 为以 i i i 结尾的子串中有多少个右括号未匹配,这样 j ≥ a i j \ge a_i j ≥ a i 。枚举上一个右括号前加了多少个左括号,有转移:
d p i , j = ∑ k = a i − 1 j d p i − 1 , k dp_{i,j}=\sum_{k=a_{i-1}}^{j} dp_{i-1,k}
d p i , j = k = a i − 1 ∑ j d p i − 1 , k
直接做是 O ( n 3 ) O(n^3) O ( n 3 ) 的。不过我们发现 k ∈ [ a i − 1 , j ] k \in [a_{i-1},j] k ∈ [ a i − 1 , j ] ,直接对 d p i − 1 dp_{i-1} d p i − 1 处理前缀和,复杂度为 O ( n 2 ) O(n^2) O ( n 2 ) 。
至于右边的情况,将序列翻转并镜像(也就是左右括号互换)后同理可求得。由乘法原理,两种情况相乘取答案。
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 const int N = 5005 ;int n, a[N];char s[N]; modint dp[N][N], pre[N];modint solve () { memset (dp, 0 , sizeof (dp)), memset (a, 0 , sizeof (a)); int tot = 0 , lcnt = 0 , rcnt = 0 ; for (int i = 1 ; i <= n; i++) { if (s[i] == '(' ) lcnt++; else rcnt++; if (s[i] == ')' ) { if (lcnt > 0 ) lcnt--, rcnt--; a[++tot] = rcnt; } } fill (pre, pre + N, 0 ); for (int j = a[1 ]; j <= tot; j++) dp[1 ][j] = 1 , pre[j] = pre[j - 1 ] + dp[1 ][j]; for (int i = 2 ; i <= tot; i++) { for (int j = a[i]; j <= tot; j++) { if (a[i - 1 ] == 0 ) dp[i][j] = pre[j]; else dp[i][j] = pre[j] - pre[a[i - 1 ] - 1 ]; } fill (pre, pre + a[i], 0 ); for (int j = a[i]; j <= tot; j++) pre[j] = pre[j - 1 ] + dp[i][j]; } return dp[tot][a[tot]]; }void _main() { cin >> (s + 1 ); n = strlen (s + 1 ); modint l = solve (); reverse (s + 1 , s + n + 1 ); for (int i = 1 ; i <= n; i++) s[i] = (s[i] == '(' ) ? ')' : '(' ; modint r = solve (); if (l == 0 ) cout << r; else if (r == 0 ) cout << l; else cout << l * r; }
括号序列中这种加字符使原序列合法的问题,常用区间 DP。令 d p l , r dp_{l,r} d p l , r 表示使子串 [ l , r ] [l,r] [ l , r ] 合法需添加的最少字符数量。
若 s l , s r s_l,s_r s l , s r 为一对匹配括号,则 d p l , r ← min ( d p l , r , d p l + 1 , r − 1 ) dp_{l,r} \gets \min(dp_{l,r},dp_{l+1,r-1}) d p l , r ← min ( d p l , r , d p l + 1 , r − 1 ) 。若 l + 1 > r − 1 l+1>r-1 l + 1 > r − 1 ,令 d p l , r ← 0 dp_{l,r} \gets 0 d p l , r ← 0 。
常规区间 DP 套路,枚举断点 m i d mid mi d 分成两段序列,则 d p l , r ← min ( d p l , r , d p l , m i d + d p m i d + 1 , r ) dp_{l,r} \gets \min(dp_{l,r},dp_{l,mid}+dp_{mid+1,r}) d p l , r ← min ( d p l , r , d p l , mi d + d p mi d + 1 , r ) 。
题目要求方案,所以我们再整一次递归,按照上述转移的方法去输出。复杂度 O ( n 3 ) O(n^3) O ( n 3 ) 。
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 45 46 47 48 const int N = 105 ; string h;int n, dp[N][N];char s[N];void dfs (int l, int r) { if (l == r) { if (s[l] == '(' || s[l] == ')' ) cout << "()" ; else cout << "[]" ; return ; } if ((s[l] == '(' && s[r] == ')' ) || (s[l] == '[' && s[r] == ']' )) { if (l + 1 > r - 1 ) return cout << s[l] << s[r], void (); if (dp[l][r] == dp[l + 1 ][r - 1 ]) { cout << s[l], dfs (l + 1 , r - 1 ), cout << s[r]; return ; } } for (int mid = l; mid < r; mid++) { if (dp[l][r] == dp[l][mid] + dp[mid + 1 ][r]) return dfs (l, mid), dfs (mid + 1 , r), void (); } }void _main(int t) { getline (cin, h), getline (cin, h); n = h.size (); memset (s, 0 , sizeof (s)); for (int i = 0 ; i < n; i++) s[i + 1 ] = h[i]; memset (dp, 0x3f , sizeof (dp)); for (int i = 1 ; i <= n; i++) dp[i][i] = 1 ; for (int len = 1 ; len <= n; len++) { for (int l = 1 ; l + len - 1 <= n; l++) { int r = l + len - 1 ; if ((s[l] == '(' && s[r] == ')' ) || (s[l] == '[' && s[r] == ']' )) { dp[l][r] = min (dp[l][r], l + 1 <= r - 1 ? dp[l + 1 ][r - 1 ] : 0 ); } for (int mid = l; mid < r; mid++) dp[l][r] = min (dp[l][r], dp[l][mid] + dp[mid + 1 ][r]); } } dfs (1 , n); cout << '\n' ; if (t) cout << '\n' ; } signed main () { int t = 1 ; for (scanf ("%d" , &t), getchar (); t--; ) _main(t); return 0 ; }
括号序列 + n ≤ 500 n \le 500 n ≤ 500 ,区间 DP 启动。但是这题还有通配符,记 <> 为一个合法括号序列(空串也认为合法),*+ 表示多个 * 连续(可为 0 个),我们考虑五种情况:
*+;
(<>);
<>*+;
(<>)<>(<>);
*+<>。
然后用 d p o p t , l , r dp_{opt,l,r} d p o pt , l , r 表示方案数。下称 * 与 ? 为通配符,则转移:
若 r − l + 1 ≤ k r-l+1\le k r − l + 1 ≤ k ,且 d p 0 , l , r − 1 = 0 dp_{0,l,r-1}=0 d p 0 , l , r − 1 = 0 ,且 s r s_r s r 为通配符,则 d p 0 , l , r = 1 dp_{0,l,r}= 1 d p 0 , l , r = 1 。
首先区间长度不能超过 k k k ,然后借助 d p 0 , l , r − 1 dp_{0,l,r-1} d p 0 , l , r − 1 判断 [ l , r − 1 ] [l,r-1] [ l , r − 1 ] 内是否都是通配符,这样直接判断一下 r r r 就行。
若 s l , s r s_l,s_r s l , s r 可以成为一对匹配括号(即为匹配括号或 ?),则 d p 1 , l , r = d p 0 , l + 1 , r − 1 + d p 2 , l + 1 , r − 1 + d p 3 , l + 1 , r − 1 + d p 4 , l + 1 , r − 1 dp_{1,l,r} = dp_{0,l+1,r-1}+dp_{2,l+1,r-1}+dp_{3,l+1,r-1}+dp_{4,l+1,r-1} d p 1 , l , r = d p 0 , l + 1 , r − 1 + d p 2 , l + 1 , r − 1 + d p 3 , l + 1 , r − 1 + d p 4 , l + 1 , r − 1 。
这个转移我们在上一题是见过的。就是抛掉左右两端的括号看区间里头的方案。
d p 2 , l , r = ∑ i = l r − 1 d p 3 , l , i × d p 0 , i + 1 , r dp_{2,l,r}=\sum_{i=l}^{r-1} dp_{3,l,i}\times dp_{0,i+1,r} d p 2 , l , r = ∑ i = l r − 1 d p 3 , l , i × d p 0 , i + 1 , r 。
枚举断点 i i i ,2 类序列的前面是 <>,也就是一个任意合法的序列,由 3 类序列提供贡献,而后面是一个 0 类序列。
d p 4 , l , r = ∑ i = l r − 1 d p 0 , l , i × d p 3 , i + 1 , r dp_{4,l,r}=\sum_{i=l}^{r-1} dp_{0,l,i}\times dp_{3,i+1,r} d p 4 , l , r = ∑ i = l r − 1 d p 0 , l , i × d p 3 , i + 1 , r 。
这个与 2 类序列的转移同理。
d p 3 , l , r = d p 1 , l , r + ∑ i = l r − 1 ( d p 2 , l , i + d p 3 , l , i ) × d p 1 , i + 1 , r dp_{3,l,r}=dp_{1,l,r}+\sum_{i=l}^{r-1} (dp_{2,l,i}+dp_{3,l,i}) \times dp_{1,i+1,r} d p 3 , l , r = d p 1 , l , r + ∑ i = l r − 1 ( d p 2 , l , i + d p 3 , l , i ) × d p 1 , i + 1 , r 。
首先 1 类序列是特殊的 3 类序列。然后对于 [ l , i ] [l,i] [ l , i ] ,3 类序列左边那个 (<>)<> 可以由 2/3 两类序列提供,而右边那个 (<>) 则由 1 类序列提供。
这样 dp,复杂度 O ( n 3 ) O(n^3) O ( n 3 ) ,答案为 d p 3 , 1 , n dp_{3,1,n} d p 3 , 1 , 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 const int N = 505 ;int n, k;char s[N]; modint dp[5 ][N][N];void _main() { cin >> n >> k >> (s + 1 ); for (int i = 1 ; i <= n; i++) dp[0 ][i][i - 1 ] = 1 ; for (int len = 1 ; len <= n; len++) { for (int l = 1 ; l + len - 1 <= n; l++) { int r = l + len - 1 ; if (len <= k && dp[0 ][l][r - 1 ] == 1 && (s[r] == '*' || s[r] == '?' )) dp[0 ][l][r] = 1 ; if (len < 2 ) continue ; if ((s[l] == '(' || s[l] == '?' ) && (s[r] == ')' || s[r] == '?' )) dp[1 ][l][r] = dp[0 ][l + 1 ][r - 1 ] + dp[2 ][l + 1 ][r - 1 ] + dp[3 ][l + 1 ][r - 1 ] + dp[4 ][l + 1 ][r - 1 ]; dp[3 ][l][r] = dp[1 ][l][r]; for (int i = l; i < r; i++) { dp[2 ][l][r] += dp[3 ][l][i] * dp[0 ][i + 1 ][r]; dp[4 ][l][r] += dp[0 ][l][i] * dp[3 ][i + 1 ][r]; dp[3 ][l][r] += (dp[2 ][l][i] + dp[3 ][l][i]) * dp[1 ][i + 1 ][r]; } } } cout << dp[3 ][1 ][n]; }
可消除子串就是一个合法括号序列,本题有 26 26 26 种括号。
我们维护一个栈去做括号匹配,若当前字符串 s i s_i s i 与栈顶形成一对匹配括号则弹出栈顶否则入栈。动态维护栈内这个字符串的哈希值 h h h 。比如说对于 abbaa 这个串:
1 2 3 4 5 i =1 stack: [a] h =hash(a)i =2 stack: [ab] h =hash(ab)i =3 stack: [a] h =hash(a)i =4 stack: [] h =hash()i =5 stack: [a] h =hash(a)
当 h h h 又一次出现时,意味着在两个括号之间的序列是合法的,所以带来贡献。我们可以维护一个哈希表 m p mp m p ,用 m p h mp_{h} m p h 表示哈希值为 h h h 的子串所带来的当前贡献量。在循环结束时,令 m p h ← m p h + 1 mp_{h} \gets mp_{h}+1 m p h ← m p h + 1 。
当然在上述样例 i = 4 i=4 i = 4 时,栈为空串意味着所有东西都匹配走了,所以空串的初始贡献为 1 1 1 ,即 m p 0 ← 1 mp_{0} \gets 1 m p 0 ← 1 。
哈希模数取 10 15 + 37 10^{15}+37 1 0 15 + 37 并使用平板电视卡常。理论复杂度 O ( n ) 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 const int N = 2e6 + 5 ;const modint base = 13331 ; modint b[N];int n;char s[N]; stack<char > st; cc_hash_table<long long , int > mp;void _main() { cin >> n >> (s + 1 ); mp[0 ] = 1 ; b[0 ] = 1 ; for (int i = 1 ; i < N; i++) b[i] = b[i - 1 ] * base; modint h = 0 ; long long res = 0 ; for (int i = 1 ; i <= n; i++) { if (!st.empty () && s[i] == st.top ()) h -= b[st.size ()] * st.top (), st.pop (); else st.emplace (s[i]), h += b[st.size ()] * st.top (); res += mp[h.val]++; } cout << res; }
有意思的数据结构题。最长合法括号子序列这个东西是一个可合并信息,具体地:
维护三个值 v a l , l c , r c val,lc,rc v a l , l c , rc ,v a l val v a l 为所求答案,在答案最大化情况下,l c , r c lc,rc l c , rc 分别表示失配的左右括号数目。在合并时,令 p = min ( l c x , r c y ) p=\min(lc_x,rc_y) p = min ( l c x , r c y ) ,用 x x x 中的失配左括号去匹配 y y y 中的失配右括号,累加答案即可。至此静态版本可用线段树解决。
考虑带修,也就是支持区间取反。这里有两种方法:
类似文艺平衡树,维护两棵树,一棵存正串一棵存反串,修改时打标记交换对应结点;
在一棵树的同一个结点维护正反串,修改时打标记交换。
代码用的方法 2。复杂度 O ( ( n + q ) log n ) O((n+q) \log n) O (( n + q ) log 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 const int N = 5e5 + 5 ;int n, q, opt, l, r;char s[N];struct node { int val, lc, rc; node () : val (0 ), lc (0 ), rc (0 ) {} node (int x, int y, int z) : val (x), lc (y), rc (z) {} }; node operator + (const node& a, const node& b) { return node{ a.val + b.val + min (a.lc, b.rc), a.lc + b.lc - min (a.lc, b.rc), a.rc + b.rc - min (a.lc, b.rc) }; }#define ls (rt << 1) #define rs (rt << 1 | 1) bool rev[N << 2 ]; node val[N << 2 ], rval[N << 2 ];void pushup (int rt) {val[rt] = val[ls] + val[rs], rval[rt] = rval[ls] + rval[rs];}void pushdown (int rt) { if (!rev[rt]) return ; rev[ls] ^= 1 , rev[rs] ^= 1 , rev[rt] ^= 1 ; swap (val[ls], rval[ls]), swap (val[rs], rval[rs]); }void build (int l, int r, int rt) { if (l == r) { if (s[l] == '(' ) val[rt].lc = rval[rt].rc = 1 ; else val[rt].rc = rval[rt].lc = 1 ; return ; } int mid = (l + r) >> 1 ; build (l, mid, ls), build (mid + 1 , r, rs), pushup (rt); }void reverse (int tl, int tr, int l, int r, int rt) { if (tl <= l && r <= tr) return rev[rt] ^= 1 , swap (val[rt], rval[rt]), void (); pushdown (rt); int mid = (l + r) >> 1 ; if (tl <= mid) reverse (tl, tr, l, mid, ls); if (tr > mid) reverse (tl, tr, mid + 1 , r, rs); pushup (rt); }node query (int tl, int tr, int l, int r, int rt) { if (tl <= l && r <= tr) return val[rt]; pushdown (rt); int mid = (l + r) >> 1 ; node res; if (tl <= mid) res = res + query (tl, tr, l, mid, ls); if (tr > mid) res = res + query (tl, tr, mid + 1 , r, rs); return res; }void _main() { cin >> n >> (s + 1 ) >> q; build (1 , n, 1 ); while (q--) { cin >> opt >> l >> r; if (opt == 1 ) reverse (l, r, 1 , n, 1 ); if (opt == 2 ) cout << query (l, r, 1 , n, 1 ).val << '\n' ; } }
再介绍一个括号题的套路。令左括号为 − 1 -1 − 1 ,右括号为 1 1 1 ,设前缀和为 p r e i pre_i p r e i 。当 [ l , r ] [l,r] [ l , r ] 为合法括号序列时:
p r e l − 1 = p r e r pre_{l-1}=pre_r p r e l − 1 = p r e r ;
∀ i ∈ [ l , r ] , p r e l − 1 ≤ p r e i \forall i \in [l,r],pre_{l-1} \le pre_i ∀ i ∈ [ l , r ] , p r e l − 1 ≤ p r e i 。
翻译一下:序列的和与最小前缀和均为 0 0 0 。而 l l l 一定时,最小前缀和关于 r r r 单调不增,考虑二分。取反操作与上题同理,维护反串。复杂度 O ( q log 2 n ) O(q \log^2 n) O ( q log 2 n ) 。使用线段树上二分可以做到 O ( q log n ) O(q \log n) O ( q log 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 const int N = 1e6 + 5 ;int n, q, opt, l, r;char s[N];struct node { int sum, premn, premx; node () : sum (0 ), premn (0 ), premx (0 ) {} node (int a, int b, int c) : sum (a), premn (b), premx (c) {} }; node operator + (const node& a, const node& b) { return node{ a.sum + b.sum, min (a.premn, a.sum + b.premn), max (a.premx, a.sum + b.premx) }; }#define ls (rt << 1) #define rs (rt << 1 | 1) bool rev[N << 2 ]; node val[N << 2 ];void pushup (int rt) {val[rt] = val[ls] + val[rs];}void reverses (int rt) { val[rt].sum = -val[rt].sum, val[rt].premx = -val[rt].premx, val[rt].premn = -val[rt].premn; swap (val[rt].premx, val[rt].premn), rev[rt] ^= 1 ; }void pushdown (int rt) { if (!rev[rt]) return ; rev[rt] ^= 1 ; reverses (ls), reverses (rs); }void build (int l, int r, int rt) { if (l == r) { if (s[l] == '(' ) val[rt].sum = val[rt].premx = val[rt].premn = 1 ; else val[rt].sum = val[rt].premx = val[rt].premn = -1 ; return ; } int mid = (l + r) >> 1 ; build (l, mid, ls), build (mid + 1 , r, rs), pushup (rt); }void reverse (int tl, int tr, int l, int r, int rt) { if (tl <= l && r <= tr) return reverses (rt), void (); pushdown (rt); int mid = (l + r) >> 1 ; if (tl <= mid) reverse (tl, tr, l, mid, ls); if (tr > mid) reverse (tl, tr, mid + 1 , r, rs); pushup (rt); }int query_sum (int tl, int tr, int l, int r, int rt) { if (tl <= l && r <= tr) return val[rt].sum; pushdown (rt); int mid = (l + r) >> 1 , res = 0 ; if (tl <= mid) res += query_sum (tl, tr, l, mid, ls); if (tr > mid) res += query_sum (tl, tr, mid + 1 , r, rs); return res; }int query_pre (int tl, int tr, int l, int r, int rt, int tot) { if (tl <= l && r <= tr) return val[rt].premn + tot; pushdown (rt); int mid = (l + r) >> 1 , res = INT_MAX; if (tl <= mid) res = min (res, query_pre (tl, tr, l, mid, ls, tot)), tot += query_sum (tl, tr, l, mid, ls); if (tr > mid) res = min (res, query_pre (tl, tr, mid + 1 , r, rs, tot)); return res; }int query (int l, int r) { int tl = l, tr = r, res = INT_MAX; while (tl <= tr) { int mid = (tl + tr) >> 1 ; if (query_pre (l, mid, 1 , n, 1 , 0 ) >= 0 ) tl = mid + 1 , res = mid; else tr = mid - 1 ; } if (res == INT_MAX) return 0 ; int x = res; while (x >= l) { int h = query_sum (l, x, 1 , n, 1 ); if (h == 0 ) return x; x -= h; } return 0 ; }void _main() { cin >> n >> q >> (s + 1 ); build (1 , n, 1 ); while (q--) { cin >> opt >> l; if (opt == 1 ) cin >> r, reverse (l, r, 1 , n, 1 ); else cout << query (l, n) << '\n' ; } }