2019CSP-S D1T2括号树详解及算法拓展
笔者着手写这篇文章的时候,正好是新型冠状病毒爆发的时候,所以正好有理由宅在家里写这篇文章,在这里也期待所有的奋斗在抗病一线的医护工作者能够结束这一次“战争”。2019年的CSP被我们戏称为“2019植树大赛”,树是一种指数级的结构,和病毒传播类似,这可能也是我选择这一道题目的原因。
如题:
题目背景
本题中合法括号串的定义如下:
() 是合法括号串。如果 A 是合法括号串,则 (A) 是合法括号串。如果 A,B 是合法括号串,则 AB 是合法括号串。
本题中子串与不同的子串的定义如下:
字符串 S 的子串是 S 中连续的任意个字符组成的字符串。S 的子串可用起始位置 l 与终止位置 r 来表示,记为 S (l, r)(1≤l≤r≤|S|,|S| 表示 S 的长度)。S 的两个子串视作不同当且仅当它们在 S 中的位置不同,即 l 不同或 r 不同。
题目描述
一个大小为 n 的树包含 n 个结点和 n−1 条边,每条边连接两个结点,且任意两个结点间有且仅有一条简单路径互相可达。
小 Q 是一个充满好奇心的小朋友,有一天他在上学的路上碰见了一个大小为 n 的树,树上结点从 1 ∼ n 编号,1 号结点为树的根。除 1 号结点外,每个结点有一个父亲结点,u(2≤u≤n)号结点的父亲为 fu(1≤fu<u)号结点。
小 Q 发现这个树的每个结点上恰有一个括号,可能是( 或)。小 Q 定义 si 为:将根结点到 i 号结点的简单路径上的括号,按结点经过顺序依次排列组成的字符串。
显然 si 是个括号串,但不一定是合法括号串,因此现在小 Q 想对所有的 i(1≤i≤n)求出,si 中有多少个互不相同的子串是合法括号串。
这个问题难倒了小 Q,他只好向你求助。设 si 共有 ki 个不同子串是合法括号串, 你只需要告诉小 Q 所有 i×ki 的异或和,即:
(1×k1) xor (2×k2) xor (3×k3) xor ⋯ xor (n×kn)
其中 xor 是位异或运算。
输入格式
第一行一个整数 n,表示树的大小。
第二行一个长为 n 的由( 与) 组成的括号串,第 i 个括号表示 i 号结点上的括号。
第三行包含 n−1 个整数,第 i(1≤i<n)个整数表示 i+1 号结点的父亲编号 fi+1。
输出格式
仅一行一个整数表示答案。
输入输出样例
输入 #1
5
(()()
1 1 2 2
输出 #1
6
说明/提示
【样例解释1】
树的形态如下图:
将根到 1 号结点的简单路径上的括号,按经过顺序排列所组成的字符串为 (,子串是合法括号串的个数为 0。
将根到 2 号结点的字符串为 ((,子串是合法括号串的个数为 0。
将根到 3 号结点的字符串为 (),子串是合法括号串的个数为 1。
将根到 4 号结点的字符串为 (((,子串是合法括号串的个数为 0。
将根到 5 号结点的字符串为 ((),子串是合法括号串的个数为 1。
【数据范围】
测试点编号 | n≤ | 特殊性质 |
1~2 | 8 | fi=i-1 |
3~4 | 200 | |
5~7 | 2000 | |
8~10 | ||
11~14 | 105 | fi=i-1 |
15~16 | ||
17~20 | 5×105 |
这一题乍一看似乎没什么,但其中的知识点非常多树形DP、括号匹配(栈的应用)。最后的求异或和类似于hash,只是为了方便得到答案。
考场上笔者有大致的思路,但是因为笔者是个蒟蒻,其实是因为不熟悉括号匹配,这一题得分惨不忍睹。
这一题,笔者准备了两种算法相同但结构不同的方法。
解决方法
既然是DP,状态转移方程自然是不可少的。分析:
又发现这题中所有串都从根节点开始,所以有两种思路。
一,定义状态dp[i] 表示从根节点到i节点中所有合法串数量,大佬们好像都用这种方法,本蒟蒻不会。
二,定义dp[i] 表示从根节点到i节点中,以i为结尾的合法串数。这样的话就比较好理解。这样在用栈判定时,如果是‘(’就把节点数push进去,如果‘)’就pop出来,定义pre为pop出来的栈顶数字。然后就转移…
dp[i] = dp[fa[pre]] + 1;
这是什么玩意呢?就是当前串接前一个合法串。
就好比(…) + (…)
当然如果前一个串不合法比如:
))) + (...)
根据上述定义, 前一个串继承过来是0, 结果是1。
最后在累加即可。例:
()(()())从左往右依次编号为1~8。
- stk.top=1 dp[1]=0
- stk=empty dp[2]=dp[0]+1=1
- stk.top=3 dp[3]=0
- stk.top=4 dp[4]=0
- stk.top=3 dp[5]=dp[3]+1=1
- stk.top=6 dp[6]=0
- stk.top=3 dp[7]=dp[5]+1=2
- stk=empty dp[8]=dp[6]+1=1
此时再来看每个节点到祖先节点的可匹配的括号的个数:
sum[1]=0,sum[2]=1,sum[3]=1,sum[4]=1,sum[5]=2,sum[6]=2,sum[7]=4,sum[8]=5。根据定义,这一串的确有五个合法括号。
因此我们可以很轻松地得到以下代码:
//以下省去注释
void dfs(int p) {
int flag = 0,pre;
if(a[p] == '(') {
stk.push(p);
flag = 1;
}
if(a[p] == ')' && !stk.empty()) {
pre = stk.top();
stk.pop();
flag = -1;
}
if(flag == -1) {
dp[p] = dp[fa[pre]] + 1;
}
sum[p] = sum[fa[p]] + dp[p];
res ^= (sum[p] * p);
for(int i = head[p]; i; i = g[i].next) {
dfs(g[i].to);
}
if(flag == 1) {
stk.pop();
}
if(flag == -1) {
stk.push(pre);
}
}
最后输出res即可。
但是,我们不难发现,可以在空间上做一些优化,比如sum数组其实是在重复利用,我们只需要定义一个sum,在递归过程中重复利用即可,在常数不变的情况下还省了空间,岂不是一举两得?
代码
完整代码如下:
#include<iostream>
#include<cstdio>
#include<cstring>
#include<stack>
#define ll long long
using namespace std;
const int N = 5e5+10;
inline int read() {
int x = 0, w = 1;
char c = getchar();
while(c < '0' || c > '9') {
if(c == '-') w = -1;
c = getchar();
}
while(c >= '0' && c <= '9') {
x = (x << 3) + (x << 1) + c - '0';
c = getchar();
}
return x * w;
}//快读
int n;
int fa[N];
char a[N];
ll dp[N];
struct edge {
int to;
int next;
};//链式前向星建图(树)
edge g[N];
int head[N];
int cnt;
stack<int> stk;//定义栈,通过栈来实现括号匹配
ll res;//十年OI一场空,不开long long见祖宗
void add(int from, int to) {
g[++cnt].next = head[from];
g[cnt].to = to;
head[from] = cnt;
}
void Input() {
n = read();
for(int i = 1; i <= n; i++) {
scanf("%c",a + i);
// printf("%c %d\n",a[i],i);
}
for(int i = 2; i <= n; i++) {
fa[i] = read();
// printf("%d %d\n",fa[i],i);
add(fa[i],i);
}
}
void dfs(int p) { //dfs至当前节点
// printf("%d ",p);
int flag = 0;//用于回溯时撤销当前操作
int pre;
if(a[p] == '(') { //如果是左括号,直接入栈
stk.push(p);
flag = 1;
}
if(a[p] == ')' && !stk.empty()) { //如果是右括号,且栈不为空
pre = stk.top();//成功匹配
stk.pop();//左括号弹出栈
flag = -1;
}
if(flag == -1) { //有可以匹配的括号,+ 1
dp[p] = dp[fa[pre]] + 1;
}
for(int i = head[p]; i; i = g[i].next) {
dfs(g[i].to);//向下传递
}
//打完收工,整理现场
if(flag == 1) {
stk.pop();
}
if(flag == -1) {
stk.push(pre);
}
}
void get_sum(int p, ll sum) {
sum += dp[p];
res ^= (sum * p);
for(int i = head[p]; i; i = g[i].next) {
get_sum(g[i].to, sum);
}
}
void Debug() {
for(int i = 1; i <= n; i++) {
printf("%lld ",dp[i]);
}
printf("\n");
}
int main() {
Input();
dfs(1);//求出所有的节点的可匹配的括号数
// printf("\n");
get_sum(1,0);//求异或和
// Debug();
printf("%lld\n",res);
return 0;
}
其他同学似乎总是以为我们计算机竞赛生会修电脑,在这里笔者想要给我们“平反”,我们只是平时敲敲代码,至于修电脑这种“高级操作”,一般都是巨佬们来做,我们这些蒟蒻只能站在旁边瑟瑟发抖了。