【UR #25】设计草图(扫描线+并查集 )【离线求区间最小值/前缀最小值数量】

题面

点我
题意:

  • 给定一个长度为 \(n\) 的字符串序列,由 \((\)\(?\)\()\) 构成,其中 \(?\) 可任意替换成 \((\)\()\),你需要求出有多少个子区间可以变成一个合法的括号序列。
  • \(1 \leq n \leq 10^6\)

分析

先来考虑怎样判断一个包含 \(?\) 的字符串序列是否可以成为合法的括号序列。
想到一般的合法括号序列判断方法:左括号数等于右括号数,且任意一个前缀都满足左括号数量不少于右括号
我们沿用这个判断方法,经过手玩能得到一个字符串序列合法的充要条件

  • 长度是偶数。
  • 将全部 \(?\) 替换成 \((\) 后,任意一个前缀都满足左括号的数量不小于右括号。
  • 将全部 \(?\) 替换成 \()\) 后,任意一个后缀都满足右括号的数量不小于左括号。

怎么快速统计满足上述三个条件的区间数量呢?

考虑枚举每个位置作为左端点统计合法的右端点。

对于第二个条件,我们将所有 \(?\) 都替换成 \((\)。然后将 \((\) 看成 \(+1\),将 \()\) 看成 \(-1\) ,求出序列的前缀和 \(A\),那么对于一个左端点 \(i\),满足条件的右端点 \(j\) 一定是\(i\) 开始的一段区间(一个元素看成长度为 \(1\) 的区间),满足这段区间中的所有 \(A_j\)大于等于 \(A_{i - 1}\)。显然可以跑一遍 单调栈 对每个左端点 \(i\) 求出区间右端点 \(R_i\)

接着考虑第三个条件,对于 \([i, R_i]\) 中的一个点 \(j\),判断区间 \([i, j]\) 是否满足第三个条件的方法:将所有 \(?\) 替换成 \()\),然后倒序扫描整个序列,将 \()\) 看成 \(1\)\((\) 看成 \(-1\),求出序列后缀和 \(B\),那么只需要满足 \(B_{j + 1}\)\(B[i, j + 1]\) 的最小值,即是\(B[i, R_i + 1]\) 的一个前缀最小值

也就是说,同时满足第二个条件和第三个条件的位置对应了 \(B[i, R_i + 1]\) 的一个前缀最小值(相同大小认为靠后的更小)。问题转化成了 离线 \(n\) 次查询区间前缀最小值数量

还有第一个限制,不难发现只要满足右端点 \(j\) 和左端点 \(i\) 奇偶性相同即可,因此我们只需要多记录前缀最小值中有几个奇数,几个偶数即可

这个问题怎么做?
回顾 离线求区间 \(rmq\) 的方法:
我们对右端点扫描线,维护一个 单调递增的单调栈,用 并查集 维护每个左端点到目前右端点这段区间的区间最小值。具体而言,当栈顶被弹出时,就将栈顶的并查集连向当前元素,那么每个左端点所在并查集的根就是当前的区间最小值。

将上述做法修改即可统计前缀最小值:一个左端点到根的路径上几个点就有几个前缀最小值,我们在路径压缩的同时合并路径信息(维护有几个奇数几个偶数)即可。时空间复杂度 \(O(n)\)

CODE:

// trick: 离线并查集线性求解区间rmq, 前缀最值数量 
#include<bits/stdc++.h>
#define pb emplace_back
using namespace std;
typedef long long LL;
const int N = 1e6 + 5;
LL ans;
char s[N];
int n, a[N], Top;
int bin[N], cnt[2][N]; // cnt[i][0/1] 表示到目前为止,以 i 为起点, 有几个偶数/奇数位置的最小值 
vector< int > R[N]; 
inline void add(int l, int r) {R[r].pb(l);}
inline int Find(int x) {
	if(bin[x] == x) return x;
	if(bin[bin[x]] == bin[x]) return bin[x];
	int u = Find(bin[x]);
	cnt[0][x] += cnt[0][bin[x]];
	cnt[1][x] += cnt[1][bin[x]];
	return bin[x] = u;
}
inline void Merge(int u, int v) {
	int f1 = Find(u), f2 = Find(v);
	bin[f1] = f2;
}
inline int ask(int L) {
	Find(L);
	return cnt[L & 1][L] + (L == bin[L] ? 0 : cnt[L & 1][bin[L]]);
}
int main() {
	scanf("%s", s + 1); n = strlen(s + 1);
	for(int i = 1; i <= n; i ++ ) a[i] = (s[i] == ')' ? -1 : 1);
	for(int i = 1; i <= n; i ++ ) a[i] += a[i - 1];
	vector< int > vec; vec.pb(0);
	for(int i = 1; i <= n; i ++ ) {
		while(!vec.empty() && a[i] < a[vec.back()]) add(vec.back() + 1, i), vec.pop_back();
		vec.pb(i);
	}
	while(!vec.empty()) add(vec.back() + 1, n + 1), vec.pop_back(); // 求出每个左端点的右端点 
	for(int i = 1; i <= n; i ++ ) a[i] = (s[i] == '(' ? -1 : 1);		
	for(int i = n; i >= 1; i -- ) a[i] += a[i + 1];
	// 后面形如离线查询区间中有多少前缀最小值,相同权值认为靠后的更小 
	for(int i = 1; i <= n + 1; i ++ ) bin[i] = i, cnt[i & 1][i] = 1;
	for(int i = 1; i <= n + 1; i ++ ) {
		while(!vec.empty() && a[i] <= a[vec.back()]) Merge(vec.back(), i), vec.pop_back();
		vec.pb(i);
		for(auto L : R[i]) {
			ans += (ask(L) - 1);
		}
	}
	cout << ans << endl;
	return 0;
} 
posted @ 2026-09-09 18:28  Czl2007  阅读(5)  评论(0)    收藏  举报