浅析2019CSP-S D1T2

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~28fi=i-1
3~4200
5~72000
8~10
11~14105fi=i-1
15~16
17~205×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。

  1. stk.top=1 dp[1]=0
  2. stk=empty dp[2]=dp[0]+1=1
  3. stk.top=3 dp[3]=0
  4. stk.top=4 dp[4]=0
  5. stk.top=3 dp[5]=dp[3]+1=1
  6. stk.top=6 dp[6]=0
  7. stk.top=3 dp[7]=dp[5]+1=2
  8. 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;
}

  其他同学似乎总是以为我们计算机竞赛生会修电脑,在这里笔者想要给我们“平反”,我们只是平时敲敲代码,至于修电脑这种“高级操作”,一般都是巨佬们来做,我们这些蒟蒻只能站在旁边瑟瑟发抖了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值