mirror of
https://gitcode.com/Zengtudor/alg2025.git
synced 2025-08-22 03:01:43 +00:00
199 lines
8.1 KiB
Markdown
199 lines
8.1 KiB
Markdown
为了解决这个问题,我们需要统计给定字符串中所有合法的子串数量。合法子串需要满足三个条件:长度至少为3,子串内部(不包括边界)恰好有一个'*',并且去掉这个'*'后,剩余字符串是一个括号匹配串。
|
||
|
||
### 方法思路
|
||
1. **预处理每个位置的左右'*'位置**:对于每个位置,我们预处理其左边最近和右边最近的'*'的位置。这有助于在枚举中心点(即'*')时,快速确定其左右边界,确保子串内部只有一个'*'。
|
||
2. **枚举中心点**:遍历字符串中的每个'*',将其作为子串内部的唯一'*'。
|
||
3. **处理左边部分**:对于每个中心点,从中心点左侧开始向左扫描到左边界。在扫描过程中,维护当前子串的平衡值(即括号匹配情况)和最小前缀和(确保过程中没有不匹配的括号)。如果遇到左括号'('且最小前缀和非负,则记录当前平衡值。
|
||
4. **处理右边部分**:从中心点右侧开始向右扫描到右边界。同样维护平衡值和最小前缀和。如果遇到右括号')'且最小前缀和等于当前平衡值(即最小前缀和出现在序列末尾),则记录当前平衡值。
|
||
5. **统计匹配**:对于左边部分记录的每个平衡值x,在右边部分查找平衡值为-x的记录,并累加匹配数量。
|
||
|
||
### 解决代码
|
||
```cpp
|
||
#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;
|
||
}
|
||
```
|
||
|
||
### 代码解释
|
||
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(扫描范围控制)
|
||
|
||
通过平衡值和最小前缀和的协同作用,算法高效地筛选出所有满足条件的合法子串,完美解决了括号匹配与星号位置约束的双重挑战。 |