括号序列
1.1 合法括号序列的性质相关例题
P7914【0530-T1】
小 w 定义“超级括号序列”是由字符 (
、)
、*
组成的字符串,并且对于某个给定的常数 \(k\),给出了“符合规范的超级括号序列”的定义如下:
()
、(S)
均是符合规范的超级括号序列,其中S
表示任意一个仅由不超过 \(k\) 个字符*
组成的非空字符串(以下两条规则中的S
均为此含义);- 如果字符串
A
和B
均为符合规范的超级括号序列,那么字符串AB
、ASB
均为符合规范的超级括号序列,其中AB
表示把字符串A
和字符串B
拼接在一起形成的字符串; - 如果字符串
A
为符合规范的超级括号序列,那么字符串(A)
、(SA)
、(AS)
均为符合规范的超级括号序列。 - 所有符合规范的超级括号序列均可通过上述 3 条规则得到。
例如,若 \(k = 3\),则字符串 ((**()*(*))*)(***)
是符合规范的超级括号序列,但字符串 *()
、(*()*)
、((**))*)
、(****(*))
均不是。特别地,空字符串也不被视为符合规范的超级括号序列。
现在给出一个长度为 \(n\) 的超级括号序列,其中有一些位置的字符已经确定,另外一些位置的字符尚未确定(用 ?
表示)。小 w 希望能计算出:有多少种将所有尚未确定的字符一一确定的方法,使得得到的字符串是一个符合规范的超级括号序列?
\(1 \le k \le n \le 500\)。
括号序列 dp 的常见方式是令 \(dp_{l, r}\) 表示 \(l \sim r\) 是一个合法括号序列,转移就有两种:往外套一层括号以及两个合法括号序列拼起来。可以发现一种拓扑序是 \(len\) 的升序。这样做的时候可能算重,这时候要考虑“最左和最右两个括号是否对应”。对于一个 \((...)(...)(...)\) 的括号,我们钦定它从 \((...)\) 转移过来,所以令 \(dp_{l,r,0/1}\) 表示 \(l \sim r\) 是一个合法括号序列,左右是/不是对应的。那么 \(dp_{l,r,1}\) 如果用两个拼起来的方式转移,只能从 \(dp_{l, k, 0} +dp_{k+1, r, 0/1}\) 转移过来。
我们考虑这一题的状态设计。会想到分成三类:
- S。也就是若干个
*
组成的字符串。 - 一个合法括号序列,左右匹配。
- 一个合法括号序列,左右不匹配。
考虑转移。首先可以想到的是,为了方便可以认为 S 的长度可以是 \(0\)。这么做之后根据题意有四种转移方式:(S);ASB;(SA);(AS)。为了避免算重,我们令 (SA) 中的 S 长度不能为 \(0\)。会发现直接设计 \(dp_{i,j,0/1/2}\) 然后根据四个规则转移是大于 \(O(n^3)\) 的。
显然可以再优化。考虑同时记录 AS,SA 的方案,并且令 ASB 中的 A 左右括号匹配(否则会算重)。这样我们多了三种:
- 形如 AS,其中 A 的两边匹配。
- 形如 AS,其中 A 的两边不匹配。
- 形如 SA,不管 A 两边是否匹配,但是 S 需要长度不为 \(0\)。
这样我们得出了一个 dp 方式是 \(O(n^3)\) 的,转移有如下几种:
- \(dp_{l,r,0} \rightarrow dp_{l-1,r+1,1}\)
- \(dp_{l,r,1} \times dp_{r+1,r+t,0} \rightarrow dp_{l,r+t,3}(t \in [0,k])\)
- \(dp_{l,r,2} \times dp_{r+1,r+t,0} \rightarrow dp_{l,r+t,4}(t \in [0,k])\)
- \(dp_{l,r,1/2} \times dp_{l-t,l-1,0} \rightarrow dp_{l-t,r,5}(t \in [1,k])\)
- \(dp_{l,r,3/4/5} \rightarrow dp_{l-1,r+1,1}\)
- \(dp_{l,r,3} \times dp_{r+1,q,1/2} \rightarrow dp_{l,q,2}\)
以上是我在草稿纸上第一次写出的转移式。但是需要注意的是存在 \((l, r)\) 转移到 \((l,r)\) 的地方,所以拓扑序需要注意,这里一种方式是可以先做第一、五、六个转移,再做另外三个。
关于拓扑序还需要注意的是,如果写 push 法,是无法保证用到另一个区间的时候算到那个区间的,所以我们需要改成 pull 法。
【要点总结】
- 通过分类的方式处理算重。
- 区间 dp,需要很注意拓扑序,push 法通常无法保证用到另一个区间的时候已经算到了这个区间。
CF1781F【0530-T2】
Vika 喜欢玩括号序列。今天他想执行如下步骤 \(n\) 次来构建一个括号序列:
-
等概率随机选择一个空位(若当前有 \(k\) 个字符,则有 \(k+1\) 个空位)。
-
以 \(p\) 的概率插入字符串
()
或以 \(1-p\) 的概率插入字符串)(
,操作后字符串长度增加 \(2\)。
给定 \(n,p\),求出 Vika 得到一个合法括号序列的概率,对 \(998244353\) 取模。
注意:读入的是 \(q\),而 \(p=q\times 10^{-4}\)
\(1 \le n \le 500; 0 \le q \le 10^4\)。
这题操作的过程是难以维护的,需要从结果/最终操作序列考虑。想到描述这个过程的方式就是对每一个括号的位置赋值为其加入的时间。例如:\(\mathtt{1355312244}\)。进一步地我们发现这个序列对应的区间是无交或包含的。另外,发现添加 \(()\) 或者 \()(\) 和这个序列是独立的。这启发我们对操作序列进行区间 dp。
首先我们发现相同长度的区间都是一样的。其次,虽然过程中的序列(或者叫最终操作序列的某一个完整子序列)不都是合法括号序列,但是如果记录其在二维坐标系中所达到的 \(\max\),那么两个序列拼起来的 \(\max\) 就是两个序列的 \(\max\) 取 \(\max\) 后的结果。这使得我们可以对区间进行合并,对区间 dp 的可行性创造了条件。
那么记 \(dp_{i, j}\) 表示长度为 \(2i\) 的操作序列,向其中插入两两无交或包含的 \(i\) 个区间(通俗地讲是形成一个合法的操作序列),在这个过程之后给每一个区间定一个 \((\) 或 \()\),概率分别为 \(p\) 和 \(1-p\),所对应的括号序列的 \(\max = j\) 的方案数。
这里的“方案数”的意思是:第一步的一个操作序列还是一个方案,但第二步的方案数加起来等于 \(1\)(就是 \(p\) 个方案做 \((\),而 \(1-p\) 个方案做 \()\))。
转移有两种:一种是往外套一个区间,第二种是两个区间拼起来。于是要加一维 \(0/1\) 表示左右是否对应这个不说了,重要的是要弄懂一个小长度的区间外面加一个区间之后的数字发生了什么变化;两个区间拼起来的数字又发生了什么变化。第一种情况下,外面那个数字肯定是 \(1\),并且里面所有数字加 \(1\),例如 \(\mathtt{12233144}\) 外面套一层区间肯定是 \(\mathtt{1233442551}\);第二种情况下,其实相当于两个有序数列进行归并,例如 \(\mathtt{1221}\) 和 \(\mathtt{11}\) 可以组成 \(\mathtt{233211,133122,122133}\)。于是系数要乘以归并系数,也就是一个组合数。
写出转移式子,这次吸取教训,选择 pull 的式子:
- \(dp_{i, k, 0} \leftarrow dp_{i-2, k+1(k=0 的话也可以是 0), 0/1} \times p\)
- \(dp_{i, k, 0} \leftarrow dp_{i-2, k-1(k=0的话不能用), 0/1} \times (1-p)\)
- \(dp_{i,k,1} \leftarrow \sum \limits_{j \in [1,i-1], x, y, \max(x,y)=k} dp_{j,x,0} \times dp_{i-j, y, 0/1} \times \dbinom{i}{j}\)
这里 \(j\) 的范围稍微注意一下。然后第三个式子显然可以用前缀和优化,拓扑序就 \(i\) 升序就好了,没啥问题。
但是写的时候犯了个错误,因为 \(j\) 枚举到 \(i\),前缀和数组也只是开到 \(i\) 了,这导致后面的用不上。dp 尤其需要检查是否有越界之类的情况。
【要点总结】
- 插入括号可以描述为操作序列进行处理。
- 归并系数。
- dp 尤其需要检查是否有越界之类的情况。
CF1830C Hyperregular Bracket Strings
对一个长度为 \(n\) 的合法括号序列 \(s\),给定 \(m\) 个限制,第 \(i\) 个形如,\(s_{l, ..., r}\) 也是一个合法括号序列。求符合所有条件的括号序列数量。
合法括号序列相当于是限制“\(\pm 1\) 序列”中的子段最小值恰好等于 \(0\)。据此,其实可以合并若干个这样的限制:
- 首先观察两个互相包含的限制。可以认为将中间的去掉之后,两端的并是一个合法括号序列。
- 然后观察两个相交的限制,假设两个限制头尾位置数值不一样的话,一定违反了最小值等于 \(0\) 的限制!因此头尾数值相同。这时候我们可以将其分成三个限制。
第一个转化转化成“交”有点难以处理,但是我们可以先进行所有第二个转化,变成一个树形结构。
看看画出来的区间会发现,这个过程其实相当于将所有位置按照覆盖它的区间分类,同一类的贡献就是 \(Cat_{类的大小}\)。
于是 xor hash 即可。可以认为是均匀哈希,均匀哈希使用生日悖论分析成功率。
我们需要给 \(n\) 个位置每一个位置分配一个 \([1, V]\) 的数,使得没有两个数相等。生日悖论的内容是,这样做的成功率是:\(\cfrac{V}{V} \times \cfrac{V-1}{V} \times ... \times \cfrac{V-n}{V}\)。
因为 \(n = 3 \times 10^5\),如果 \(V = 10^9\),那么几乎不可能成功。因此设置 \(V = 10^{18}\) 是必要的。
【要点总结】
- 知道了如何合并若干个“合法括号序列”的限制。
- 哈希的正确性除了随机性的保证,还需要算生日悖论。
1.2 转化为树形结构
将一棵节点数量为 \(n\) 的树转化为一个长度为 \(2n\) 的合法括号序列:
char bracket[];
int cnt;
void dfs(int x) {
bracket[++cnt] = '(';
for(int i : son(x)) dfs(i);
bracket[++cnt] = ')';
}
生成方式:从根节点 dfs
整棵树,从父节点 dfs
到该节点时,给括号序列插入一个 (
;从子节点回溯到该节点时,给括号序列插入一个 )
。
从括号序列逆推树结构的算法:
int now = 0, cnt, fa[];
for(char s : bracket){
if(s == '(') {
++cnt;
son(now).push(cnt);
fa[cnt] = now;
}
else now = fa[now];
}
常用技巧:如果串里面有 (...)(...)(...)
这样的,无法建成一棵树,这时在串的左右两边加两个括号变成 ((...)(...)(...))
,此时这两个括号是匹配的,可以保证建成一颗节点个数为 \(n+1\),编号为 \(0-n\) 的树。
一条链对应到括号序列上就是 ((((((()))))))
这样的括号序列。