当前位置:首页 > 技术 > 正文内容

蓝桥杯2021国赛AB组:括号序列翻转与最长合法子串查询(线段树+二分)

访客 技术 2026年6月18日 1

题目描述

给定长度为 n 的括号序列(仅包含 '(' 和 ')'),支持两种操作:

  1. 翻转操作:将区间 [Li, Ri] 内所有括号翻转('(' 变 ')',')' 变 '(')。
  2. 查询操作:给定左端点 Li,找到最大的 Ri,使得子串 [Li, Ri] 是一个合法括号序列;若不存在,输出 0。

数据范围

  • 1 ≤ n ≤ 106
  • 1 ≤ m ≤ 2 × 105

分析思路

将序列映射为数值:'(' = 1,')' = -1。则合法括号序列等价于:区间和为 0,且任意前缀和非负。利用前缀和 pre[i],合法括号序列 [l, r] 满足:

pre[r] == pre[l-1]
min{pre[k] | k ∈ [l, r]} ≥ pre[l-1]

因此问题转化为:找到最大的 r,使得 pre[r] = pre[l-1] 且区间 [l, r] 内所有 pre 值不小于 pre[l-1]。

操作一:区间翻转

翻转区间 [l, r] 后,前缀和数组的变化规律:

  • 区间 [l, r] 内 pre 值变为相反数,并加上 2×pre[l-1](因为翻转后,区间起点的影响需要修正)。
  • 区间 [r+1, n] 内 pre 值减少 2×(翻转后区间最后一个元素的新值)。

为简化实现,可将翻转分解为两次操作:先翻转前缀 [1, r],再翻转前缀 [1, l-1]。这样只需维护两种懒惰标记:

  • 翻转标记 (rev):交换节点最大值和最小值,并取负。
  • 加法标记 (add):区间整体添加一个常数。

两种标记需要正确处理相互影响:当节点被翻转时,加法标记也应取负。

操作二:查询

朴素二分法:对每个查询,二分右端点位置,用线段树查询区间最小值。复杂度 O(m log² n),数据较大时需优化。

优化方法:在线段树上直接二分,利用前缀和的连续性——若区间内最小值小于目标值,则第一个小于目标值的点必然在更左侧;若区间内所有值都不小于目标值,则需找到最后一个等于目标值的点。

算法步骤

  1. 查找第一个小于目标值的位置:若存在,则合法右端点可能在该位置左侧。
  2. 若不存在小于目标值的位置:则区间内所有 pre 值 ≥ 目标值,需查找最后一个等于目标值的位置。

实现细节

线段树节点属性

  • mn:区间最小值
  • mx:区间最大值
  • lazy_rev:翻转标记
  • lazy_add:加法标记

关键函数

  • build:构建线段树
  • push_down:下传懒标记,处理 rev 和 add 的相互作用
  • update_rev:区间翻转
  • update_add:区间加常数
  • query_pos:单点查询
  • query_first_less:寻找第一个小于目标值的位置
  • query_last_equal:寻找最后一个等于目标值的位置

时间复杂度

优化后 O(n log n),适用于大数据范围。

代码实现 (C++)

#include <bits/stdc++.h>
using namespace std;
const int INF = 1e9;
const int maxn = 1e6 + 5;

int n, q;
string s;
int pre[maxn];

struct SegTree {
    int mn[maxn << 2], mx[maxn << 2];
    int lazy_add[maxn << 2];
    bool lazy_rev[maxn << 2];

    void build(int p, int l, int r) {
        if (l == r) {
            mn[p] = mx[p] = pre[l];
            return;
        }
        int mid = (l + r) >> 1;
        build(p << 1, l, mid);
        build(p << 1 | 1, mid + 1, r);
        mn[p] = min(mn[p << 1], mn[p << 1 | 1]);
        mx[p] = max(mx[p << 1], mx[p << 1 | 1]);
    }

    void push_down(int p) {
        if (lazy_rev[p]) {
            swap(mn[p << 1], mx[p << 1]);
            mn[p << 1] = -mn[p << 1];
            mx[p << 1] = -mx[p << 1];
            swap(mn[p << 1 | 1], mx[p << 1 | 1]);
            mn[p << 1 | 1] = -mn[p << 1 | 1];
            mx[p << 1 | 1] = -mx[p << 1 | 1];
            lazy_rev[p << 1] ^= 1;
            lazy_rev[p << 1 | 1] ^= 1;
            lazy_add[p << 1] *= -1;
            lazy_add[p << 1 | 1] *= -1;
            lazy_rev[p] = 0;
        }
        if (lazy_add[p]) {
            mx[p << 1] += lazy_add[p];
            mn[p << 1] += lazy_add[p];
            mx[p << 1 | 1] += lazy_add[p];
            mn[p << 1 | 1] += lazy_add[p];
            lazy_add[p << 1] += lazy_add[p];
            lazy_add[p << 1 | 1] += lazy_add[p];
            lazy_add[p] = 0;
        }
    }

    void update_rev(int p, int l, int r, int ql, int qr) {
        if (ql > r || qr < l) return;
        if (ql <= l && r <= qr) {
            swap(mn[p], mx[p]);
            mn[p] = -mn[p];
            mx[p] = -mx[p];
            lazy_rev[p] ^= 1;
            lazy_add[p] *= -1;
            return;
        }
        push_down(p);
        int mid = (l + r) >> 1;
        if (ql <= mid) update_rev(p << 1, l, mid, ql, qr);
        if (qr > mid) update_rev(p << 1 | 1, mid + 1, r, ql, qr);
        mn[p] = min(mn[p << 1], mn[p << 1 | 1]);
        mx[p] = max(mx[p << 1], mx[p << 1 | 1]);
    }

    void update_add(int p, int l, int r, int ql, int qr, int val) {
        if (ql > r || qr < l) return;
        if (ql <= l && r <= qr) {
            mn[p] += val;
            mx[p] += val;
            lazy_add[p] += val;
            return;
        }
        push_down(p);
        int mid = (l + r) >> 1;
        if (ql <= mid) update_add(p << 1, l, mid, ql, qr, val);
        if (qr > mid) update_add(p << 1 | 1, mid + 1, r, ql, qr, val);
        mn[p] = min(mn[p << 1], mn[p << 1 | 1]);
        mx[p] = max(mx[p << 1], mx[p << 1 | 1]);
    }

    int query_pos(int p, int l, int r, int pos) {
        if (pos < 1) return 0;
        if (l == r) return mn[p];
        push_down(p);
        int mid = (l + r) >> 1;
        if (pos <= mid) return query_pos(p << 1, l, mid, pos);
        else return query_pos(p << 1 | 1, mid + 1, r, pos);
    }

    int first_less(int p, int l, int r, int pos, int val) {
        if (l == r) return l;
        push_down(p);
        int mid = (l + r) >> 1;
        int ans = 0;
        if (pos <= mid && mn[p << 1] < val)
            ans = first_less(p << 1, l, mid, pos, val);
        if (ans) return ans;
        if (mn[p << 1 | 1] < val)
            ans = first_less(p << 1 | 1, mid + 1, r, pos, val);
        return ans;
    }

    int last_equal(int p, int l, int r, int pos, int val) {
        if (l == r) return l;
        push_down(p);
        int mid = (l + r) >> 1;
        int ans = 0;
        if (mn[p << 1 | 1] <= val)
            ans = last_equal(p << 1 | 1, mid + 1, r, pos, val);
        if (ans) return ans;
        if (pos <= mid && mn[p << 1] <= val)
            ans = last_equal(p << 1, l, mid, pos, val);
        return ans;
    }
} seg;

void solve() {
    cin >> n >> q >> s;
    for (int i = 1; i <= n; ++i) {
        pre[i] = (s[i-1] == '(' ? 1 : -1);
        pre[i] += pre[i-1];
    }
    seg.build(1, 1, n);
    while (q--) {
        int op, l, r;
        cin >> op;
        if (op == 1) {
            cin >> l >> r;
            int v1 = seg.query_pos(1, 1, n, l-1);
            seg.update_rev(1, 1, n, 1, l-1);
            seg.update_add(1, 1, n, l, n, -2*v1);
            int v2 = seg.query_pos(1, 1, n, r);
            seg.update_rev(1, 1, n, 1, r);
            seg.update_add(1, 1, n, r+1, n, -2*v2);
        }
        else {
            cin >> l;
            int val = seg.query_pos(1, 1, n, l-1);
            int pos = seg.first_less(1, 1, n, l, val);
            if (pos) {
                if (pos-1 >= l) cout << pos-1 << "\n";
                else cout << 0 << "\n";
            }
            else {
                pos = seg.last_equal(1, 1, n, l, val);
                cout << (pos ? pos : 0) << "\n";
            }
        }
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    solve();
    return 0;
}

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。