CSP-S2019 D1T2 括号树
CSP-S2019 D1T2 括号树
题目背景
本题中合法括号串的定义如下:
()
是合法括号串。- 如果
A
是合法括号串,则(A)
是合法括号串。 - 如果
A
,B
是合法括号串,则AB
是合法括号串。
本题中子串与不同的子串的定义如下:
- 字符串
S
的子串是S
中连续的任意个字符组成的字符串。S
的子串可用起始位置 ll 与终止位置 rr 来表示,记为 S (l, r)S(l,r)(1 \leq l \leq r \leq |S |1≤l≤r≤∣S∣,|S |∣S∣ 表示 S 的长度)。 S
的两个子串视作不同当且仅当它们在S
中的位置不同,即 ll 不同或 rr 不同。
题目描述
一个大小为 nn 的树包含 nn 个结点和 n − 1n−1 条边,每条边连接两个结点,且任意两个结点间有且仅有一条简单路径互相可达。
小 Q 是一个充满好奇心的小朋友,有一天他在上学的路上碰见了一个大小为 nn 的树,树上结点从 11 ∼ nn 编号,11 号结点为树的根。除 11 号结点外,每个结点有一个父亲结点,uu(2 \leq u \leq n2≤u≤n)号结点的父亲为 f_uf**u(1 ≤ f_u < u1≤f**u<u)号结点。
小 Q 发现这个树的每个结点上恰有一个括号,可能是(
或)
。小 Q 定义 s_is**i 为:将根结点到 ii 号结点的简单路径上的括号,按结点经过顺序依次排列组成的字符串。
显然 s_is**i 是个括号串,但不一定是合法括号串,因此现在小 Q 想对所有的 ii(1\leq i\leq n1≤i≤n)求出,s_is**i 中有多少个互不相同的子串是合法括号串。
这个问题难倒了小 Q,他只好向你求助。设 s_is**i 共有 k_ik**i 个不同子串是合法括号串, 你只需要告诉小 Q 所有 i \times k_ii×k**i 的异或和,即:
(1 \times k_1)\ \text{xor}\ (2 \times k_2)\ \text{xor}\ (3 \times k_3)\ \text{xor}\ \cdots\ \text{xor}\ (n \times k_n)(1×k1) xor (2×k2) xor (3×k3) xor ⋯ xor (n×k**n)
其中 xorxor 是位异或运算。
输入格式
第一行一个整数 nn,表示树的大小。
第二行一个长为 nn 的由(
与)
组成的括号串,第 ii 个括号表示 ii 号结点上的括号。
第三行包含 n − 1n−1 个整数,第 ii(1 \leq i \lt n1≤i<n)个整数表示 i + 1i+1 号结点的父亲编号 f_{i+1}f**i+1。
输出格式
仅一行一个整数表示答案。
题解:
还是先放回忆杀。
用了将近一个点来切T1,但是最后还是因为没开ull见了5分的祖宗。剩下的时间基本都在淦T2,因为T3基本上是毫无头绪。于是T2还是挂了。总之当时考场思路是一团乱麻,根本不知道自己想要写什么。还是蒟蒻太菜了。
细细思考,题意需要我们干什么?首先,处理出从1号点到每个点的括号序列,然后统计它的合法子串数目,最后异或和。
主要卡在如何统计合法子串数目(废话)。
这种统计数目的题,肯定要先试一试递推。那么,递推的转移过程,其实就是看一个新的半括号进场的时候,对答案有什么样子的影响。可以发现的性质是,一个合法括号串一定是一个平衡括号串。如果是左括号,肯定还没构成合法括号串,如果是右括号,首先要看它平衡掉了哪个左括号。这样才能进一步确定这个平衡的区间到底是哪个。
更进一步地,对于题目给出的合法括号串定义3,对于一个合法括号串的前面的所有合法括号串,皆可以与当前的括号串构成新的合法子串。这也是统计的时候唯一需要考虑加法原理的地方。
所以可以得出一个状态转移的方程:设\(cnt[i]\)表示以从根到\(i\)的合法串的数目(注意,并非合法子串)。\(last[i]\)表示离i最近的未被匹配的左括号位置。
即有:
新匹配到的当然是一个新子串,其之前的合法串数目和它自己都会构成一个新的合法子串。于是更新cnt数组,同时,因为last已经被换掉了,所以也要更新last,其更新方法为:
比较好理解的。
先放代码:
#include<cstdio>
#define ll long long
using namespace std;
const int maxn=5*1e5+10;
int n;
ll ans;
char s[maxn];
int tot,to[maxn],head[maxn],nxt[maxn],fa[maxn];
ll cnt[maxn],last[maxn];
//cnt[x]表示根节点到x点的合法串个数,last[x]表示距离x节点最近的未匹配左括号。
void add(int x,int y)
{
to[++tot]=y;
nxt[tot]=head[x];
head[x]=tot;
}
void dfs(int x)
{
last[x]=last[fa[x]];
if(s[x]=='(')
last[x]=x;
else if(last[x])
cnt[x]=cnt[fa[last[x]]]+1,last[x]=last[fa[last[x]]];
for(int i=head[x];i;i=nxt[i])
dfs(to[i]);
}
int main()
{
scanf("%d%s",&n,s+1);
for(int i=2;i<=n;i++)
{
int x;
scanf("%d",&x);
fa[i]=x;
add(x,i);
}
dfs(1);
ans=cnt[1];
for(int i=2;i<=n;i++)
cnt[i]+=cnt[fa[i]],ans^=(i*cnt[i]);
printf("%lld",ans);
return 0;
}
这个算法的精妙之处就在于:它的last数组采用了下传的形式,大大减少了时间复杂度。也就是在函数开始之前先把上面的信息复制下来,这样就免去了回溯的麻烦,以及,快速找到需要的信息。
妙哉!