alg2025/src/7/31/U130826.md
2025-07-31 10:46:15 +08:00

8.1 KiB
Raw Blame History

为了解决这个问题我们需要统计给定字符串中所有合法的子串数量。合法子串需要满足三个条件长度至少为3子串内部不包括边界恰好有一个'',并且去掉这个''后,剩余字符串是一个括号匹配串。

方法思路

  1. 预处理每个位置的左右'*'位置:对于每个位置,我们预处理其左边最近和右边最近的''的位置。这有助于在枚举中心点(即'')时,快速确定其左右边界,确保子串内部只有一个'*'。
  2. 枚举中心点:遍历字符串中的每个'',将其作为子串内部的唯一''。
  3. 处理左边部分:对于每个中心点,从中心点左侧开始向左扫描到左边界。在扫描过程中,维护当前子串的平衡值(即括号匹配情况)和最小前缀和(确保过程中没有不匹配的括号)。如果遇到左括号'('且最小前缀和非负,则记录当前平衡值。
  4. 处理右边部分:从中心点右侧开始向右扫描到右边界。同样维护平衡值和最小前缀和。如果遇到右括号')'且最小前缀和等于当前平衡值(即最小前缀和出现在序列末尾),则记录当前平衡值。
  5. 统计匹配对于左边部分记录的每个平衡值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;
}

代码解释

  1. 预处理左右'*'位置:使用两个数组left_starright_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序列存在非法位置

在算法中的应用

左边部分扫描(向左遍历)

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 时记录平衡值(保证子串有效性)

右边部分扫描(向右遍历)

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
    
  • 匹配操作
    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扫描范围控制

通过平衡值和最小前缀和的协同作用,算法高效地筛选出所有满足条件的合法子串,完美解决了括号匹配与星号位置约束的双重挑战。