mirror of
https://gitcode.com/Zengtudor/alg2025.git
synced 2025-08-21 18:52:07 +00:00
8.1 KiB
8.1 KiB
为了解决这个问题,我们需要统计给定字符串中所有合法的子串数量。合法子串需要满足三个条件:长度至少为3,子串内部(不包括边界)恰好有一个'',并且去掉这个''后,剩余字符串是一个括号匹配串。
方法思路
- 预处理每个位置的左右'*'位置:对于每个位置,我们预处理其左边最近和右边最近的''的位置。这有助于在枚举中心点(即'')时,快速确定其左右边界,确保子串内部只有一个'*'。
- 枚举中心点:遍历字符串中的每个'',将其作为子串内部的唯一''。
- 处理左边部分:对于每个中心点,从中心点左侧开始向左扫描到左边界。在扫描过程中,维护当前子串的平衡值(即括号匹配情况)和最小前缀和(确保过程中没有不匹配的括号)。如果遇到左括号'('且最小前缀和非负,则记录当前平衡值。
- 处理右边部分:从中心点右侧开始向右扫描到右边界。同样维护平衡值和最小前缀和。如果遇到右括号')'且最小前缀和等于当前平衡值(即最小前缀和出现在序列末尾),则记录当前平衡值。
- 统计匹配:对于左边部分记录的每个平衡值x,在右边部分查找平衡值为-x的记录,并累加匹配数量。
解决代码
#include <iostream>
#include <vector>
#include <map>
#include <algorithm>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t;
cin >> t;
while (t--) {
string s;
cin >> s;
int n = s.size();
vector<int> left_star(n, -1);
vector<int> right_star(n, n);
int last = -1;
for (int i = 0; i < n; i++) {
left_star[i] = last;
if (s[i] == '*') {
last = i;
}
}
last = n;
for (int i = n-1; i >= 0; i--) {
right_star[i] = last;
if (s[i] == '*') {
last = i;
}
}
long long ans = 0;
for (int pos = 0; pos < n; pos++) {
if (s[pos] != '*') continue;
int left_bound = (left_star[pos] == -1) ? 0 : left_star[pos] + 1;
int right_bound = (right_star[pos] == n) ? n-1 : right_star[pos] - 1;
map<int, int> left_map;
int cur_left = 0, min_cur_left = 0;
for (int i = pos-1; i >= left_bound; i--) {
int val = (s[i] == '(') ? 1 : -1;
int new_cur = val + cur_left;
int new_min = min(val, val + min_cur_left);
cur_left = new_cur;
min_cur_left = new_min;
if (s[i] == '(' && min_cur_left >= 0) {
left_map[cur_left]++;
}
}
map<int, int> cnt_r;
int cur_r = 0;
int min_cur_r = 0;
for (int j = pos+1; j <= right_bound; j++) {
int val = (s[j] == '(') ? 1 : -1;
cur_r += val;
min_cur_r = min(min_cur_r, cur_r);
if (s[j] == ')' && min_cur_r == cur_r) {
cnt_r[cur_r]++;
}
}
for (const auto& p : left_map) {
int x = p.first;
auto it = cnt_r.find(-x);
if (it != cnt_r.end()) {
ans += p.second * it->second;
}
}
}
cout << ans << '\n';
}
return 0;
}
代码解释
- 预处理左右'*'位置:使用两个数组
left_star
和right_star
,分别存储每个位置左侧和右侧最近'*'的位置。这有助于在枚举中心点时快速确定左右边界。 - 枚举中心点:遍历字符串中的每个'',将其作为可能的子串内部唯一''。
- 处理左边部分:从中心点左侧开始向左扫描,维护当前子串的平衡值和最小前缀和。如果遇到左括号'('且最小前缀和非负,则记录当前平衡值。
- 处理右边部分:从中心点右侧开始向右扫描,同样维护平衡值和最小前缀和。如果遇到右括号')'且最小前缀和等于当前平衡值,则记录当前平衡值。
- 统计匹配:对于左边部分记录的每个平衡值
x
,在右边部分查找平衡值为-x
的记录。每匹配一次,就累加一次组合数量,最终得到合法子串的总数。
这种方法高效地利用了预处理和动态维护平衡值,确保在O(n log n)时间内解决问题,满足题目要求。
平衡值和最小前缀和详解
在解决括号匹配问题时,平衡值和最小前缀和是两个关键概念,用于高效地判断括号序列的合法性。下面我将详细解释这两个概念及其在算法中的应用:
1. 平衡值 (Balance Value)
- 定义:平衡值表示当前括号序列中左括号与右括号的数量差。具体计算规则:
- 遇到左括号
(
:平衡值 +1 - 遇到右括号
)
:平衡值 -1
- 遇到左括号
- 意义:
- 平衡值为正:左括号多于右括号
- 平衡值为零:左右括号完全匹配
- 平衡值为负:右括号多于左括号(非法状态)
- 示例:
- 序列
(()
:平衡值变化0 → 1 → 2 → 1
- 序列
())
:平衡值变化0 → 1 → 0 → -1
(非法)
- 序列
2. 最小前缀和 (Minimum Prefix Sum)
- 定义:在遍历过程中,记录从起始位置到当前位置的所有平衡值的最小值。
- 意义:
- 检测序列是否在任何时刻出现非法状态(平衡值<0)
- 保证整个子串的括号匹配有效性
- 关键性质:
- 最小前缀和 ≥ 0:序列始终合法(无右括号多余的情况)
- 最小前缀和 < 0:序列存在非法位置
在算法中的应用
左边部分扫描(向左遍历)
for (int i = pos-1; i >= left_bound; i--) {
int val = (s[i] == '(') ? 1 : -1;
cur_left = val + cur_left; // 更新平衡值
min_cur_left = min(val, val + min_cur_left); // 更新最小前缀和
if (s[i] == '(' && min_cur_left >= 0) {
left_map[cur_left]++; // 记录有效左边界
}
}
- 操作逻辑:
- 从星号左侧开始向左扫描
- 遇到左括号才记录(因为合法子串必须以
(
开头) - 仅当
最小前缀和 ≥ 0
时记录平衡值(保证子串有效性)
右边部分扫描(向右遍历)
for (int j = pos+1; j <= right_bound; j++) {
int val = (s[j] == '(') ? 1 : -1;
cur_r += val;
min_cur_r = min(min_cur_r, cur_r);
if (s[j] == ')' && min_cur_r == cur_r) {
cnt_r[cur_r]++; // 记录有效右边界
}
}
- 操作逻辑:
- 从星号右侧开始向右扫描
- 遇到右括号才记录(因为合法子串必须以
)
结尾) - 仅当
最小前缀和 == 当前平衡值
时记录:- 表明从开始到当前位置未出现非法状态
- 且当前平衡值是最小值(无未匹配的右括号)
匹配原理
- 核心条件:整个子串(去掉星号)需满足:
左边平衡值 + 右边平衡值 = 0
- 匹配操作:
for (const auto& p : left_map) { int x = p.first; if (cnt_r.count(-x)) { ans += p.second * cnt_r[-x]; } }
- 示例:
- 左边记录平衡值
x = 1
(左括号多1个) - 右边需匹配平衡值
y = -1
(右括号多1个) x + y = 0
→ 构成完整匹配
- 左边记录平衡值
算法特点
-
高效性:
- 每个字符最多被扫描两次(左/右各一次)
- 哈希表操作均摊时间复杂度 O(1)
- 总时间复杂度 O(n),满足 ∑|s| ≤ 10⁶ 的要求
-
正确性保障:
- 预处理星号边界确保唯一性
- 平衡值检测保证括号匹配
- 最小前缀和检测排除非法子串
-
边界处理:
- 星号不在子串边界(预处理保证)
- 子串长度 ≥3(扫描范围控制)
通过平衡值和最小前缀和的协同作用,算法高效地筛选出所有满足条件的合法子串,完美解决了括号匹配与星号位置约束的双重挑战。