Codeforces 1340F: Nastya and CBS

题目传送门:1340F

题意简述

你需要动态维护一个多种括号组成的括号序列。需要支持两种操作:

  1. 修改单一位置的括号。
  2. 查询一段区间是否是一个合法的括号序列。

序列长度为 \(n\),不同的括号种类数为 \(k\),操作次数为 \(q\)

一个多种括号组成的括号序列 \(S\) 是合法的当且仅当:

  1. \(S\) 为空。
  2. \(S\) 可以表示为 \(A + B\) 的形式,其中 \(A, B\) 都是合法的。
  3. \(S\) 可以表示为 \(\texttt{(}^\ast + A + \texttt{)}^\ast\) 的形式,其中 \(A\) 是合法的并且 \(\texttt{(}^\ast\)\(\texttt{)}^\ast\) 是一对相同种类的左右括号

例如 ()[{}()]{([{}])} 是合法的,而 ][][([{)]} 不是。

题解

部分思路来自于 EntropyIncreaser。

我们考虑使用线段树维护,遇到的问题有二:

  1. 线段树中的每个节点应该存储什么信息?
  2. 如何合并信息?

考虑一段括号序列,在不断删去相邻的匹配括号之后,得到的结果。如果其中仍然包含了相向的括号(此时必然不匹配,因为如果匹配就会被删去),例如 (],则在它两边再拼接上任何括号序列,最终都一定不会变成合法的括号序列。

如果已经可以确保区间内不可能变成合法的括号序列(也就是上述情况发生了),就可以打一个不可行的标记。

否则得到的结果一定是若干个右括号,加上若干个左括号(都可以为 \(0\) 个),例如 )]}[((

可以发现只要在两边加上合适的括号,就可以把括号全部消除,变成合法的括号序列。

所以要记录的信息就是它,就是消到不能消之后的括号序列,分为「在左侧的右括号」和「在右侧的左括号」两部分。

先不考虑如何存储的问题,着眼于区间信息的合并上,如何合并两个子区间的信息呢?

我们令左子区间的左侧部分(右括号)为 \(ll\),左子区间的右侧部分(左括号)为 \(lr\)
 再令右子区间的左侧部分(右括号)为 \(rl\),右子区间的右侧部分(左括号)为 \(rr\),接下来分两种情况:

  1. 如果 \(lr\)\(rl\) 短。
    例如 )[( + )]}{( 这种情况,合并后变成 )}{(
    可以发现必须有:整个 \(lr\) 要匹配得上 \(rl\) 的长度为 \(|lr|\) 的前缀。
    匹配上之后,再把 \(rl\) 的去掉这个前缀的部分连接在 \(ll\) 的后面,作为新的左侧部分,而右侧部分就是 \(rr\)
    如果匹配不上,那就打上无解的标记。
  2. 如果 \(lr\)\(rl\) 长。
    例如 )[( + ){( 这种情况,合并后变成 )[{(
    可以发现必须有:整个 \(rl\) 要匹配得上 \(lr\) 的长度为 \(|rl|\) 的后缀。
    匹配上之后,再把 \(lr\) 的去掉这个后缀的部分连接在 \(rr\) 的前面,作为新的右侧部分,而左侧部分就是 \(ll\)
    如果匹配不上,那就打上无解的标记。

官方题解中使用了可持久化平衡树 + 哈希来维护信息,显然是可行的,而且时空复杂度都是 \(\mathcal O (n \log^2 n)\) 的。

实际上存在一种不那么麻烦,而且空间复杂度为线性的做法:

直接仅存储两部分的长度和字符串哈希值,不需要使用平衡树。

当合并的时候,需要查询某个节点左/右侧部分的某个长度的前/后缀的哈希值,和将要匹配的串进行比对。

但是已知整个串的哈希值是无法求出前/后缀的哈希值的,我们考虑递归!

实现两个函数来求某个节点左/右侧部分的前/后缀哈希值(详见代码中的 gValLgValR 函数)。

以求左侧部分的前缀哈希值为例,如果要求长度为 \(0\) 或总长度就可以直接返回已知结果。
否则考虑要求的长度是否全部落在左子树的左侧部分内,如果是则直接递归左子树。
否则就是有一部分在左子树的左侧部分内,另一部分在右子树的左侧部分内,但是合并的时候会被左子树的右侧部分抵消掉一个前缀的数值,所以递归查询的时候,长度要减去左子树的左侧部分长度,但又要加上左子树的右侧部分长度,求出后再抵消掉左子树的右侧部分的一个前缀,再在前面拼上左子树的左侧部分。

右侧部分的后缀哈希值是对称的。虽然描述起来有些困难,但是如果封装了字符串哈希的一些操作,写起来会十分的简便。

可以发现若要查询此信息,每次只会递归到一个子树内,所以每次合并信息的复杂度是 \(\mathcal O (\log n)\) 的。

每次修改总共要合并 \(\mathcal O (\log n)\) 次信息,总复杂度为 \(\mathcal O (\log^2 n)\)这与李超线段树或类楼房重建线段树的结构有些相似

这部分就是修改时线段树内的维护了。还需要考虑询问时。

可以发现询问时,信息合并之后不再是一个原线段树上的节点了,而可以看作是「询问时建立的虚节点」。

因为信息合并时同样需要进行子树递归,在传统的递归结构中这是比较难以维护的。

我实现时先把在线段树上定位到的区间抽离出来,从左到右塞进一个栈中(详见代码中的 Extract 函数)。

然后利用询问时的特性(在任意时刻前缀不能含有左侧的右括号),维护一个 seq 数组进行更简易的模拟。

注意类似 gValR 的函数还需要针对 seq 实现一个,就是代码中的 gVal 函数。

具体细节详见代码。

评测记录,下面是代码,时间复杂度为 \(\mathcal O (n \log^2 n)\),空间复杂度为 \(\mathcal O (n)\)

#include <cstdio>

const int MN = 100005, MS = 1 << 18 | 7;

namespace Hashing {
	typedef long long LL;
	const int Mod = 998244353, B = 114514, iB = 137043501;
	int _w[MN * 2], *w = _w + MN;
	void Init(int N) {
		w[0] = 1;
		for (int i = 1; i <= N; ++i)
			w[i] = (LL)w[i - 1] * B % Mod,
			w[-i] = (LL)w[-i + 1] * iB % Mod;
	}
	struct str {
		int x, m;
		str() { x = m = 0; }
		str(int v) { x = v, m = 1; }
		str(int _x, int _m) : x(_x), m(_m) {}
		friend bool operator == (str a, str b) { return a.m == b.m && a.x == b.x; }
		friend str operator + (str a, str b) { return str((a.x + (LL)b.x * w[a.m]) % Mod, a.m + b.m); }
		friend str operator - (str a, str b) { return str((LL)(a.x - b.x + Mod) * w[-b.m] % Mod, a.m - b.m); }
	};
}
using Hashing::str;

int N, Q;

#define li (i << 1)
#define ri (li | 1)
#define mid ((l + r) >> 1)
#define ls li, l, mid
#define rs ri, mid + 1, r
struct dat {
	bool err;
	str vl, vr;
	dat() { err = 0; }
	dat(int x) { err = 0; x > 0 ? vr = str(x) : vl = str(-x); }
} tr[MS];
str gValL(int i, int k) {
	if (!k) return str();
	if (k == tr[i].vl.m) return tr[i].vl;
	if (k <= tr[li].vl.m) return gValL(li, k);
	return tr[li].vl + (gValL(ri, k - tr[li].vl.m + tr[li].vr.m) - tr[li].vr);
}
str gValR(int i, int k) {
	if (!k) return str();
	if (k == tr[i].vr.m) return tr[i].vr;
	if (k <= tr[ri].vr.m) return gValR(ri, k);
	return tr[ri].vr + (gValR(li, k - tr[ri].vr.m + tr[ri].vl.m) - tr[ri].vl);
}
void Upd(int i) {
	if (tr[li].err || tr[ri].err) return tr[i].err = 1, void();
	tr[i].err = 0;
	tr[i].vl = tr[li].vl;
	tr[i].vr = tr[ri].vr;
	if (tr[li].vr.m <= tr[ri].vl.m) {
		if (tr[li].vr == gValL(ri, tr[li].vr.m))
			tr[i].vl = tr[i].vl + (tr[ri].vl - tr[li].vr);
		else tr[i].err = 1;
	} else {
		if (tr[ri].vl == gValR(li, tr[ri].vl.m))
			tr[i].vr = tr[i].vr + (tr[li].vr - tr[ri].vl);
		else tr[i].err = 1;
	}
}
void Build(int i, int l, int r) {
	if (l == r) {
		int x;
		scanf("%d", &x);
		tr[i] = x;
		return ;
	}
	Build(ls), Build(rs);
	Upd(i);
}
void Mdf(int i, int l, int r, int p, int x) {
	if (l == r) return tr[i] = x, void();
	p <= mid ? Mdf(ls, p, x) : Mdf(rs, p, x);
	Upd(i);
}

int stk[45], tp;
void Extract(int i, int l, int r, int a, int b) {
	if (r < a || b < l) return ;
	if (a <= l && r <= b) return stk[++tp] = i, void();
	Extract(ls, a, b), Extract(rs, a, b);
}
str seq[45];
str gVal(int i, int k) {
	if (!k) return str();
	if (k == seq[i].m) return seq[i];
	if (k <= tr[stk[i]].vr.m) return gValR(stk[i], k);
	return tr[stk[i]].vr + (gVal(i - 1, k - tr[stk[i]].vr.m + tr[stk[i]].vl.m) - tr[stk[i]].vl);
}
bool Qur(int l, int r) {
	tp = 0, Extract(1, 1, N, l, r);
	for (int i = 1; i <= tp; ++i) {
		if (tr[stk[i]].err) return 0;
		if (seq[i - 1].m < tr[stk[i]].vl.m) return 0;
		if (tr[stk[i]].vl == gVal(i - 1, tr[stk[i]].vl.m))
			seq[i] = tr[stk[i]].vr + (seq[i - 1] - tr[stk[i]].vl);
		else return 0;
	}
	return !seq[tp].m;
}

int main() {
	scanf("%d%*d", &N);
	Hashing::Init(N);
	Build(1, 1, N);
	scanf("%d", &Q);
	while (Q--) {
		int t, x, y;
		scanf("%d%d%d", &t, &x, &y);
		if (t == 1) Mdf(1, 1, N, x, y);
		else puts(Qur(x, y) ? "Yes" : "No");
	}
	return 0;
}
posted @ 2020-04-30 06:55  粉兔  阅读(679)  评论(0编辑  收藏  举报