安吉D2-T4
- 1安吉D19 T1
- 2安吉D19 T3
- 3安吉D20 模考12总结
- 4安吉D17 T3
- 5安吉D17 T4
- 6安吉D19 模考11总结
- 7安吉D15 A
- 8安吉D16 T4
- 9安吉D17 T1
- 10安吉D17 模考10总结
- 11安吉D16 T3
- 12安吉D16 模考9总结
- 13安吉D13 模考8总结
- 14安吉D13-T2
- 15安吉D13-T3
- 16安吉D12-T3
- 17安吉D11-B
- 18安吉D11-C
- 19安吉D11-D
- 20安吉D12 模考7总结
- 21安吉D12-T2
- 22安吉D11-A
- 23安吉D10 模考6总结
- 24安吉D10-T3
- 25安吉D10-T4
- 26安吉D8-E
- 27安吉D9-T4
- 28安吉D8-A
- 29安吉D8-B
- 30安吉D8-C
- 31安吉D8-D
- 32安吉D9 模考5总结
- 33安吉D9-T2
- 34安吉D9-T3
- 35安吉D6-T4
- 36安吉D4-G
- 37安吉D5-T2
- 38安吉D6 模考4总结
- 39安吉D6-T1
- 40安吉D6-T3
- 41安吉-开营测试 T4
- 42安吉D5 模考3总结
- 43安吉D5-T3
- 44安吉D4-A
- 45安吉D4-B
- 46安吉D4-C
- 47安吉D4:容斥原理
- 48安吉Day4-D
- 49安吉D3 模考2总结
- 50安吉D3-T1
- 51安吉D3-T2
- 52安吉D3-T4
- 53安吉D2 模考1总结
- 54安吉D2-T2
- 55安吉D1-G
- 56安吉D1-L
- 57安吉D2-T4本文
原题呈现
P9356 Bracket
题目描述
Mirika 有一个长度为 的括号序列 .
对于一个括号序列 ,Mirika 可以执行两种操作:
- 变换:选择一个位置 满足 ,使得 变为 .
- 插入:在这个序列的 任意位置 插入一个括号(左右括号均可).
Mirika 定义括号序列 的权值 为能将这个括号序列变成一个合法括号序列所需的最小操作数.
其中,合法括号序列的定义为:
- 空串为 合法括号序列.
- 若 为 合法括号序列,则 为 合法括号序列.
- 若 均为 合法括号序列,则 也为 合法括号序列.
现在 Mirika 想要求出:
其中 表示由 形成的连续子序列.
但是 Mirika 太菜了不会算,于是只好求助于你.
输入格式
本题每个测试点内有多组数据.
第一行一个正整数 表示测试数据组数.
对于每组数据,第一行一个正整数 .
第二行一个长度为 的括号序列 .
输出格式
输出共 行,第 行一个整数表示第 组测试数据的答案.
输入输出样例 #1
输入 #1
52((4())(5()(()5()()(15()())(())))()()输出 #1
4111612241说明/提示
样例解释
对于 :
- 考虑 .执行变换操作 ,有 ,其中 是合法括号序列,故 .可以证明不存在更优的策略.
- 考虑 .执行变换操作 ,再在序列开头插入一个左括号,有 ,其中 是合法括号序列,故 .可以证明不存在更优的策略.
数据规模与约定
本题采用捆绑测试.
- Subtask 0(15 pts):,.
- Subtask 1(20 pts):,.
- Subtask 2(5 pts): 内不含有右括号.
- Subtask 3(10 pts):对于所有整数 ,有 .
- Subtask 4(30 pts):,.
- Subtask 5(20 pts):无特殊限制.
对于所有数据,,,.
对于一个括号序列 ,长度为 ,若将其左括号视作 1,右括号视作 -1,将这个序列做一次前缀和,得到 数组:
- ,是一个空串
- 表示括号序列遍历到第 个括号时,栈中的左括号个数
因此,对于括号序列 , 是合法的当且仅当:
对于括号序列 ,一个 的子串 是合法的,当且仅当以下条件全部满足:
- 是 中的最小值
对于一个括号序列 ,长度为 ,若将其左括号视作 1,右括号视作 -1,将这个序列做一次前缀和,得到 数组:
s[0] = 0,是一个空串s[i]表示括号序列遍历到第 个括号时,栈中的左括号个数 则一个 的子串 是合法的,当且仅当以下条件全部满足:- 是 中的最小值
由于 为通过循环移位和插入括号变成合法括号序列的最少操作数,则有:
- 若 是前缀和数组中最小的,则所需操作数为 ,即在末尾补充 个右括号.
- 若 是前缀和数组中最小的,则所需操作数为 ,即在开头补充 个左括号.
- 否则,需要先循环移位一次,转换为上面两种情况,再补充括号,所需操作数
因此,可以将子串的贡献拆分为所有子串的 的部分和部分子串所需 1 的部分.
的部分
的部分的贡献为:
将两个和式化简,得:
注意,这里 .
将 数组排序,就可以消去绝对值,变成下面的形式(注意:这里需要桶排):
接着,遍历每一个元素,将这一个元素和前面每一个元素进行配对,具体来说,若遍历到的当前元素前面还有 个元素,则当前元素会配成 对,贡献为
可以化简为:
可以维护当前前缀和来 解决.
1 的部分
正难则反,只需要算出不需要 +1 的子串,剩下的就是需要 +1 的子串.
通过上面规律分析我们可以知道,一个子串不需要 +1,当且仅当以下条件满足其一:
- 该子串左端点在该字串范围前缀和数组中取到最小值
- 该子串右端点在该字串范围前缀和数组中取到最小值
观察可知,这两个条件对称,只需要反转序列,并将左右括号对调,就可以将第二个问题条件转化为第一个条件.
定义 为左端点 右边第一个位置 ,满足 小于 (注意,位置和 p 数组下标是 0-based,而 s 数组下标是 1-based).可以使用从小到大的单调栈进行维护.
求出 之后,左端点 的右端点就可以取 ,一共是 种情况,将其作为负贡献计入总和.
容斥掉合法子串
不难发现,合法子串同时满足上面两个条件,因此合法子串会被算两次,要扣除重复的部分,就要知道有多少合法子串.
回顾上面的规律,我们知道,一个子串 是合法序列,当且仅当以下两个条件全部满足:
- 首尾相等:
- 是当前区间最小值
我们将所有的 放到一个桶 中,桶里存放每个元素的下标, 的值域为 ,可以偏移为 .
枚举每一个值 ,遍历下标数组 ,对于每一个下标 (即为左端点 -1),找到所有的 (即为右端点)满足 且 (使得 是当前区间最小值),其中,.
在程序中,我们使用双指针,使用一个指针 指向 中的元素,初始为 0,用于记录当前第一个不满足条件的 .遍历元素 时:
- 移动双指针,直到移动到 边界,或 .
- 此时,所有可能的右边界为:,共有 个,将其作为贡献计入总和.
最终答案为:
其中:
- 为 的部分的贡献.
- 为子串数,.
- 为上述不需要 +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(); }}对于括号,可以将其视为一串只含有 的序列,并通过下标从 0 开始的前缀和解决问题.
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!


