题目地址
https://codeforces.com/problemset/problem/1838/D
题目抽象
给一个由小括号 ()
组成长度为
n
n
n 的字符串
要求你从起点走到终点,过程中下标位置的字符按顺序记录下来,形成一个新的字符串
每次行进可以往左或往右移
1
1
1 位(不能移动出字符外)
然后有
q
q
q 次操作,每次操作会给一个下标,将下标对应的括号进行取反(左括号变成右括号,或者右括号变成左括号)
问,每一次操作后,是否有一种进行路线可以让最后构成的新字符串成为一个合法括号序列
数据范围:
1
≤
n
,
q
≤
2
⋅
1
0
5
1\le n,q \le 2\cdot 10^5
1≤n,q≤2⋅105
题目类型
思路
解题思路
提前的额外思考
我们先来看如何判断一个字符串是合法的括号序列:
这是非常简单的题,在 leetcode 上就有这个基础题,也尝尝作为面试题
最直观的做法就是利用栈:
遇到左括号,如栈,遇到右括号,就让栈弹出一个元素,如果没有元素,说明不合法。最后遍历完字符后判断栈是否为空
一次判断需要
O
(
n
)
O(n)
O(n) 的复杂度
而对于字符时时刻刻都在改变,就需要
O
(
q
⋅
n
)
O(q \cdot n)
O(q⋅n) 的复杂度,我们来看看如何优化
我们将上面的栈转化成数学公式(只考虑一种括号),将左括号记为1,右括号记为-1,记
s
u
m
i
=
∑
j
=
1
i
a
i
sum_i=\sum_{j=1}^i a_i
sumi=∑j=1iai
如果一个括号序列是合法的,那么需要满足两个条件
- s u m n = 0 sum_n = 0 sumn=0 (遍历完后判断栈是否为空)
- min 1 ≤ i ≤ n s u m i ≥ 0 \min \limits_{1\le i \le n} sum_i \geq 0 1≤i≤nminsumi≥0 (判断栈是否能弹出)
每次更新需要维护这两个信息,
s
u
m
n
sum_n
sumn 我们可以用树状数组/线段树来维护,
min
1
≤
i
≤
n
s
u
m
i
\min \limits_{1\le i \le n} sum_i
1≤i≤nminsumi 可以用线段树来维护
每次更新和查询都是
O
(
log
n
)
O(\log n)
O(logn),所以整体复杂度是
O
(
(
n
+
q
)
⋅
log
n
)
O((n+q)\cdot \log n)
O((n+q)⋅logn)
思路一
先来看两个很容易判断的非法情况
- 如果 n n n 为奇数,那么一定不合法
- 如果 s[1] 为
)
,或者 s[n] 为(
,也一定不合法
接下来在这两种情况之外分析:
我们没法应用上述提前思考的方式,是因为构造路径时会来回走
但其实也并非所有来回走都有效果
- 如果
(
和)
都是间隔排列,那么从左一直走到右的结果和在中间反复(都会形成 +1 -1 的序列)走是一样的结果 - 只在有连续的
((
或者))
的情况,来回走才有效果,既可以生成无数个(
或者)
我们将出现连续的 ((
的下标用
d
l
dl
dl(double left) 记录下来,既
i
,
s
i
=
s
i
−
1
i,s_i = s_{i-1}
i,si=si−1,连续的 ))
记为
d
r
dr
dr(double r)
然后来分类讨论:
- 如果
d
l
dl
dl 和
d
r
dr
dr 为空,那么就是
(
和)
都是间隔排列的情况,那么就是合法序列 - 如果
d
l
dl
dl 和
d
r
dr
dr 一个不为空,如果
d
l
dl
dl 不为空,
d
r
dr
dr 为空,那么
(
一定比)
多,而在(
内来回走,会导致(
更多,所以一定不合法,反之亦然 - 如果
d
l
dl
dl 和
d
r
dr
dr 都不为空,第一个
d
l
dl
dl 和最后一个
d
r
dr
dr 之间的串
s
d
l
0
s
d
l
0
+
1
…
s
d
r
l
e
n
‾
\overline{s_{dl_0}s_{dl_0+1}\dots s_{dr_{len}}}
sdl0sdl0+1…sdrlen一定可以通过动态调整一定可以达到上述括号序列合法的情况,而第一个
d
l
dl
dl 之前不能出现
d
r
dr
dr,不然会使得
min
1
≤
i
≤
n
s
u
m
i
<
0
\min \limits_{1\le i \le n} sum_i < 0
1≤i≤nminsumi<0,最后一个
d
r
dr
dr 后也不能出现
d
l
dl
dl,不然会使得
s
u
m
n
>
0
sum_n > 0
sumn>0
所以,我们只需要判断第一个 d l dl dl 小于 第一个 d r dr dr 且 最后一个 d l dl dl 小于最后一个 d r dr dr,用set
的begin
和rbegin
就能取到
而 d l dl dl 和 d r dr dr 的维护只需要根据定义,维护他的增加和删除就行,用set
的复杂度为 O ( log n ) O(\log n) O(logn)
代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 2e5+2;
int n, q;
char s[MAXN];
void solve() {
cin >> n >> q;
for (int i = 1; i <= n; i ++) {
cin >> s[i];
}
set<int> dl, dr;
for (int i = 1; i <= n; i ++) {
if (s[i] == s[i - 1]) {
if (s[i] == '(') dl.insert(i);
else dr.insert(i);
}
}
while (q --) {
int index;
cin >> index;
if (n & 1) {
cout << "NO" << endl;
continue;
}
if (s[index] == '(') {
if (s[index-1] == '(') dl.erase(index);
else if (s[index-1] == ')') dr.insert(index);
if (s[index+1] == '(') dl.erase(index+1);
else if (s[index+1] == ')') dr.insert(index+1);
} else {
if (s[index-1] == '(') dl.insert(index);
else if(s[index-1] == ')') dr.erase(index);
if (s[index+1] == '(') dl.insert(index+1);
else if(s[index+1] == ')') dr.erase(index+1);
}
s[index] = (s[index] == '(' ? ')' : '(');
if (s[1] == ')' || s[n] == '(') {
cout << "NO" << endl;
} else if (dl.size() == 0 && dr.size() == 0) {
cout << "YES" << endl;
} else if (dl.size() == 0 || dr.size() == 0) {
cout << "NO" << endl;
} else {
if(*dl.begin() < *dr.begin() && *dl.rbegin() < *dr.rbegin()) {
cout << "YES" << endl;
} else {
cout << "NO" << endl;
}
}
}
}
int main() {
solve();
}
思路二
第二种思路比较难想,我也是看了官方的解题报告才知道
(官方解题报告虽然有严格的证明,但一般没有列如何想到这种思路,所以有时候看了解题报告后虽然会了,但对思维没有任何提升)
在集合
A
A
A 中记录所有满足以下任一条件的
i
i
i:
-
i
i
i 为奇数,且
s
i
s_i
si =
)
-
i
i
i 为偶数,且
s
i
s_i
si =
(
然后分情况讨论
- 如果
A
=
∅
A=\emptyset
A=∅ ,则括号序列一定是
()()...()
这种形式,一定合法 - 如果
m
i
n
(
A
)
min(A)
min(A) 为奇数,那么括号序列一定是
()()())...
这种形式,一定非法 - 如果
m
a
x
(
A
)
max(A)
max(A) 为偶数,那么括号序列一定是
...(()()()
这种形式,一定非法 - 其他情况,括号序列一定是
()()((...))()()
这种形式,必然如果(
比)
少,那么就在((
多反复走几次,一定可以满足 s u m n = 0 sum_n = 0 sumn=0,所以合法,反之亦然
这个思路只需要有一个 set 记录就行,比思路一实现起来要精简很多,不容易写错
代码
#include <bits/stdc++.h>
using namespace std;
int n, q;
string s;
void solve() {
cin >> n >> q;
cin >> s;
set<int> A;
for (int i = 1; i <= n; i ++) {
if (i & 1) {
if (s[i-1] == ')') {
A.insert(i);
}
} else {
if (s[i-1] == '(') {
A.insert(i);
}
}
}
while (q --) {
int index;
cin >> index;
if (n & 1) {
cout << "NO" << endl;
continue;
}
if (A.count(index)) {
A.erase(index);
} else {
A.insert(index);
}
if (!A.empty() && (*A.begin() & 1 || *A.rbegin() & 1 ^ 1)) {
cout << "NO" << endl;
} else {
cout << "YES" << endl;
}
}
}
int main() {
solve();
}