视频加载失败

安吉D2-T4

2226 字
11 分钟
安吉D2-T4
原题呈现

P9356 Bracket#

题目描述#

Mirika 有一个长度为 nn 的括号序列 ss

对于一个括号序列 SS,Mirika 可以执行两种操作:

  • 变换:选择一个位置 ii 满足 1iS1 \leq i \leq \lvert S \rvert,使得 SS 变为 SiSi+1SSS1S2Si2Si1S_iS_{i+1}\cdots S_{\lvert S\rvert}S_1S_2\cdots S_{i-2}S_{i-1}
  • 插入:在这个序列的 任意位置 插入一个括号(左右括号均可).

Mirika 定义括号序列 SS 的权值 f(S)f(S) 为能将这个括号序列变成一个合法括号序列所需的最小操作数.

其中,合法括号序列的定义为:

  • 空串为 合法括号序列.
  • A\texttt A 为 合法括号序列,则 (A)\texttt{(A)} 为 合法括号序列.
  • A,B\texttt A, \texttt B 均为 合法括号序列,则 AB\texttt{AB} 也为 合法括号序列.

现在 Mirika 想要求出:

l=1nr=lnf(s[l,r])\sum_{l=1}^n \sum_{r=l}^n f(s[l,r])

其中 s[l,r]s[l,r] 表示由 sl,sl+1,,srs_l,s_{l+1},\cdots,s_r 形成的连续子序列.

但是 Mirika 太菜了不会算,于是只好求助于你.

输入格式#

本题每个测试点内有多组数据.

第一行一个正整数 TT 表示测试数据组数.

对于每组数据,第一行一个正整数 nn

第二行一个长度为 nn 的括号序列 ss

输出格式#

输出共 TT 行,第 ii 行一个整数表示第 ii 组测试数据的答案.

输入输出样例 #1#

输入 #1#

5
2
((
4
())(
5
()(()
5
()()(
15
()())(())))()()

输出 #1#

4
11
16
12
241

说明/提示#

样例解释#

对于 s=())(s = \texttt{())(}

  • 考虑 s[1,4]=())(s[1,4]=\texttt{())(}.执行变换操作 i=4i=4,有 ())((())\texttt{())(} \Rightarrow \texttt{(())},其中 (())\texttt{(())} 是合法括号序列,故 f(s[1,4])=1f(s[1, 4]) = 1.可以证明不存在更优的策略.
  • 考虑 s[2,4]=))(s[2,4]=\texttt{))(}.执行变换操作 i=2i=2,再在序列开头插入一个左括号,有 ))()()()()\texttt{))(} \Rightarrow \texttt{)()} \Rightarrow \texttt{()()},其中 ()()\texttt{()()} 是合法括号序列,故 f(s[2,4])=2f(s[2, 4]) = 2.可以证明不存在更优的策略.

数据规模与约定#

本题采用捆绑测试.

  • Subtask 0(15 pts):n400n \leq 400n800\sum n \leq 800
  • Subtask 1(20 pts):n2×103n \leq 2\times 10^3n4×103\sum n \leq 4\times 10^3
  • Subtask 2(5 pts):ss 内不含有右括号.
  • Subtask 3(10 pts):对于所有整数 1i<n1\le i < n,有 sisi+1s_i \neq s_{i+1}
  • Subtask 4(30 pts):n2×105n \leq 2\times 10^5n5×105\sum n \leq 5\times 10^5
  • Subtask 5(20 pts):无特殊限制.

对于所有数据,1T100001 \leq T \leq 100001n2×1061 \leq n \leq 2 \times 10^61n2×1071 \leq \sum n \leq 2 \times 10^7

小技巧:快速判断括号序列的合法性

对于一个括号序列 TT,长度为 mm,若将其左括号视作 1,右括号视作 -1,将这个序列做一次前缀和,得到 ss 数组:

  • s0=0s_0 = 0,是一个空串
  • sis_i 表示括号序列遍历到第 ii 个括号时,栈中的左括号个数

因此,对于括号序列 TTTT 是合法的当且仅当:

  • sm=0s_m = 0
  • i,si0\forall i, s_i \geq 0

对于括号序列 TT,一个 TT 的子串 [l,r][l,r] 是合法的,当且仅当以下条件全部满足:

  • sr=sl1s_r=s_{l-1}
  • srs_rsl..rs_{l..r} 中的最小值

对于一个括号序列 TT,长度为 mm,若将其左括号视作 1,右括号视作 -1,将这个序列做一次前缀和,得到 ss 数组:

  • s[0] = 0,是一个空串
  • s[i] 表示括号序列遍历到第 ii 个括号时,栈中的左括号个数 则一个 TT 的子串 [l,r][l,r] 是合法的,当且仅当以下条件全部满足:
  • sr=sl1s_r=s_{l-1}
  • srs_rsl..rs_{l..r} 中的最小值

由于 f(T)f(T) 为通过循环移位插入括号变成合法括号序列的最少操作数,则有:

  • s1s_1 是前缀和数组中最小的,则所需操作数为 sm|s_m|,即在末尾补充 sm|s_m| 个右括号.
  • sms_m 是前缀和数组中最小的,则所需操作数为 sm|s_m|,即在开头补充 sm|s_m| 个左括号.
  • 否则,需要先循环移位一次,转换为上面两种情况,再补充括号,所需操作数 sm+1|s_m| + 1

因此,可以将子串的贡献拆分为所有子串的 sm|s_m| 的部分和部分子串所需 1 的部分.

sm|s_m| 的部分#

sm|s_m| 的部分的贡献为:

l=1mr=lms[r]s[l1]\sum_{l=1}^{m}\sum_{r=l}^{m} |s[r]-s[l-1]|

将两个和式化简,得:

0i<jms[j]s[i]\sum_{0\leq i<j\leq m} |s[j]-s[i]|

注意,这里 i=l1i=l-1

ss 数组排序,就可以消去绝对值,变成下面的形式(注意:这里需要桶排):

0i<jms[j]s[i]\sum_{0\leq i<j\leq m} s[j]-s[i]

接着,遍历每一个元素,将这一个元素和前面每一个元素进行配对,具体来说,若遍历到的当前元素前面还有 ii 个元素,则当前元素会配成 ii 对,贡献为 (s[i+1]s[i])+(s[i+1]s[i1])+(s[i+1]s[i2])++(s[i+1]s[1])(s[i + 1] - s[i]) + (s[i+1]-s[i-1]) + (s[i+1]-s[i-2]) + \cdots + (s[i+1]-s[1])

可以化简为:s[i1]×i+s[1]+s[2]++s[i]s[i-1] \times i + s[1] + s[2] + \cdots + s[i]

可以维护当前前缀和来 O(n)O(n) 解决.

1 的部分#

正难则反,只需要算出不需要 +1 的子串,剩下的就是需要 +1 的子串.

通过上面规律分析我们可以知道,一个子串不需要 +1,当且仅当以下条件满足其一:

  • 该子串左端点在该字串范围前缀和数组中取到最小值
  • 该子串右端点在该字串范围前缀和数组中取到最小值

观察可知,这两个条件对称,只需要反转序列,并将左右括号对调,就可以将第二个问题条件转化为第一个条件.

定义 p[i]p[i] 为左端点 l=i1l=i-1 右边第一个位置 rr,满足 s[r+1]s[r+1] 小于 s[i]s[i](注意,位置和 p 数组下标是 0-based,而 s 数组下标是 1-based).可以使用从小到大的单调栈进行维护.

求出 p[i]p[i] 之后,左端点 ll 的右端点就可以取 i+1,i+2,,p[i]1i+1,i+2,\cdots,p[i]-1,一共是 p[i]i1p[i]-i-1 种情况,将其作为负贡献计入总和.

容斥掉合法子串#

不难发现,合法子串同时满足上面两个条件,因此合法子串会被算两次,要扣除重复的部分,就要知道有多少合法子串.

回顾上面的规律,我们知道,一个子串 [l,r][l,r] 是合法序列,当且仅当以下两个条件全部满足:

  • 首尾相等:s[l1]=s[r]s[l-1] = s[r]
  • s[l1]s[l-1] 是当前区间最小值

我们将所有的 ss 放到一个桶 bucketbucket 中,桶里存放每个元素的下标ss 的值域为 [n,n][-n, n],可以偏移为 [0,2n][0, 2n]

枚举每一个值 xx,遍历下标数组 pos=bucked[x]pos=bucked[x],对于每一个下标 uu(即为左端点 -1),找到所有的 vv (即为右端点)满足 u<vu<vv<puv < p_u(使得 s[u]s[u] 是当前区间最小值),其中,u,vposu,v\in pos

在程序中,我们使用双指针,使用一个指针 ptrptr 指向 pospos 中的元素,初始为 0,用于记录当前第一个不满足条件的 vv.遍历元素 pos[i]pos[i] 时:

  • 移动双指针,直到移动到 pospos 边界,或 pos[ptr]>ppos[i]pos[ptr] > p_{pos[i]}
  • 此时,所有可能的右边界为:i+1,i+2,,ptri+1,i+2,\cdots,ptr,共有 ptriiptr-i-i 个,将其作为贡献计入总和.

最终答案为:

Ssm+TS1+SgS_{|s_m|} +T-S_{1} +S_g

其中:

  • SsmS_{|s_m|}sm|s_m| 的部分的贡献.
  • TT 为子串数,T=(m+1)m2T=\frac{(m+1)m}{2}
  • S1S_1 为上述不需要 +1 的子串数量.
  • SgS_gS1S_1 计算中被重复计数的合法子串数量.

标程

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 2e6 + 100;
int n, s[N], p[N], rs[N], rp[N];
ll ans;
string str;
// s[i] := sum{ str[0] ~ str[i-1] }
ll solve_1(int *s, int *p) {
ll ans = 0;
stack<pair<int, int>> sta;
for (int i = 0; i <= n; i++)
p[i] = n + 1;
for (int i = 0; i <= n; i++) {
while (!sta.empty() && sta.top().first > s[i]) {
int ele = sta.top().second;
sta.pop();
p[ele] = i;
}
sta.push({s[i], i});
}
for (int i = 0; i < n; i++) { // i 表示左端点的前一个位置
ans += p[i] - 1 - i;
}
return ans;
}
void solve() {
ans = 0;
cin >> n >> str;
vector<int> cnt((n << 1) + 1, 0);
cnt[s[0] + n] = 1;
for (int i = 1; i <= str.size(); i++) {
s[i] = s[i - 1] + (str[i - 1] == '(' ? 1 : -1);
cnt[s[i] + n]++;
}
// 计算 |Sn| 的部分
vector<int> vec;
for (int i = 0; i <= n << 1; i++) {
for (int j = 1; j <= cnt[i]; j++)
vec.push_back(i - n);
}
ll pref = 0;
for (int i = 0; i < vec.size(); i++) {
ans += 1ll * vec[i] * i - pref;
pref += vec[i];
}
// 计算 1 的部分
ll tot = (1ll * n * (n + 1)) >> 1;
// 左边取到最小,p[i] := 下标为i后边第一个小于s[i]的值的下标
tot -= solve_1(s, p);
// 右边取到最小
string rstr = str;
reverse(rstr.begin(), rstr.end());
for (int i = 1; i <= rstr.size(); i++) {
rs[i] = rs[i - 1] + (rstr[i - 1] == '(' ? -1 : 1);
}
tot -= solve_1(rs, rp);
// 去重
vector<vector<int>> bucket(n * 2 + 1);
for (int i = 0; i <= n; i++) {
bucket[s[i] + n].push_back(i);
}
for (int x = 0; x <= n << 1; x++) {
int ptr = 0;
for (int i = 0; i < bucket[x].size(); i++) {
vector<int> &pos = bucket[x];
while (ptr < pos.size() && p[pos[i]] > pos[ptr])
ptr++;
tot += ptr - 1 - i;
}
}
cout << ans + tot << endl;
}
signed main() {
freopen("bracket.in", "r", stdin);
freopen("bracket.out", "w", stdout);
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int T;
cin >> T;
while (T--) {
solve();
}
}

对于括号,可以将其视为一串只含有 ±1\pm 1 的序列,并通过下标从 0 开始的前缀和解决问题.

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

安吉D2-T4
https://blog.jerrylab.top/posts/problem/anji2026/D2/T4/
作者
Jerry
发布于
2026-08-01
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
Jerry
Hello, I'm Jerry.
公告
欢迎来到我的博客!这是一则示例公告。
分类
标签
最新动态
站点统计
文章
80
分类
3
标签
35
总字数
89,198
运行时长
0
最后活动
0 天前
站点信息
构建平台
Cloudflare Pages
博客版本
Firefly v6.16.5
文章许可
CC BY-NC-SA 4.0