为了解决这个问题,我们需要统计给定字符串中所有合法的子串数量。合法子串需要满足三个条件:长度至少为3,子串内部(不包括边界)恰好有一个'*',并且去掉这个'*'后,剩余字符串是一个括号匹配串。 ### 方法思路 1. **预处理每个位置的左右'*'位置**:对于每个位置,我们预处理其左边最近和右边最近的'*'的位置。这有助于在枚举中心点(即'*')时,快速确定其左右边界,确保子串内部只有一个'*'。 2. **枚举中心点**:遍历字符串中的每个'*',将其作为子串内部的唯一'*'。 3. **处理左边部分**:对于每个中心点,从中心点左侧开始向左扫描到左边界。在扫描过程中,维护当前子串的平衡值(即括号匹配情况)和最小前缀和(确保过程中没有不匹配的括号)。如果遇到左括号'('且最小前缀和非负,则记录当前平衡值。 4. **处理右边部分**:从中心点右侧开始向右扫描到右边界。同样维护平衡值和最小前缀和。如果遇到右括号')'且最小前缀和等于当前平衡值(即最小前缀和出现在序列末尾),则记录当前平衡值。 5. **统计匹配**:对于左边部分记录的每个平衡值x,在右边部分查找平衡值为-x的记录,并累加匹配数量。 ### 解决代码 ```cpp #include #include #include #include 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 left_star(n, -1); vector 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 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 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; } ``` ### 代码解释 1. **预处理左右'*'位置**:使用两个数组`left_star`和`right_star`,分别存储每个位置左侧和右侧最近'*'的位置。这有助于在枚举中心点时快速确定左右边界。 2. **枚举中心点**:遍历字符串中的每个'*',将其作为可能的子串内部唯一'*'。 3. **处理左边部分**:从中心点左侧开始向左扫描,维护当前子串的平衡值和最小前缀和。如果遇到左括号'('且最小前缀和非负,则记录当前平衡值。 4. **处理右边部分**:从中心点右侧开始向右扫描,同样维护平衡值和最小前缀和。如果遇到右括号')'且最小前缀和等于当前平衡值,则记录当前平衡值。 5. **统计匹配**:对于左边部分记录的每个平衡值`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:序列存在非法位置 ### 在算法中的应用 #### 左边部分扫描(向左遍历) ```cpp 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]++; // 记录有效左边界 } } ``` - **操作逻辑**: 1. 从星号左侧开始向左扫描 2. 遇到左括号才记录(因为合法子串必须以 `(` 开头) 3. 仅当 `最小前缀和 ≥ 0` 时记录平衡值(保证子串有效性) #### 右边部分扫描(向右遍历) ```cpp 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]++; // 记录有效右边界 } } ``` - **操作逻辑**: 1. 从星号右侧开始向右扫描 2. 遇到右括号才记录(因为合法子串必须以 `)` 结尾) 3. 仅当 `最小前缀和 == 当前平衡值` 时记录: - 表明从开始到当前位置未出现非法状态 - 且当前平衡值是最小值(无未匹配的右括号) ### 匹配原理 - **核心条件**:整个子串(去掉星号)需满足: ``` 左边平衡值 + 右边平衡值 = 0 ``` - **匹配操作**: ```cpp 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` → 构成完整匹配 ### 算法特点 1. **高效性**: - 每个字符最多被扫描两次(左/右各一次) - 哈希表操作均摊时间复杂度 O(1) - 总时间复杂度 O(n),满足 ∑|s| ≤ 10⁶ 的要求 2. **正确性保障**: - 预处理星号边界确保唯一性 - 平衡值检测保证括号匹配 - 最小前缀和检测排除非法子串 3. **边界处理**: - 星号不在子串边界(预处理保证) - 子串长度 ≥3(扫描范围控制) 通过平衡值和最小前缀和的协同作用,算法高效地筛选出所有满足条件的合法子串,完美解决了括号匹配与星号位置约束的双重挑战。