括号树
题目简述:
PS:具体描述请见上面的题目链接
给定n表示树的大小,然后给出每个点是左括号还是右括号,再给出2~n-1每个点的父节点f的编号(1一定为根节点)
定义si:将根结点到i号结点的简单路径上的括号,按节点经过顺序依次排列组成的字符串
要求对所有的i求出,si中有多少个互不相同的子串是合法括号串
合法字符串的定义:
- ()是合法括号串
- 如果A是合法括号串,则(A)是合法括号串
- 如果A,B是合法括号串,则AB是合法括号串
设si共有ki个不同子串是合法括号串,你只需要求出所有i×ki的异或和
数据范围:
算法&知识点:
链&树、栈、DFS
解题历程
因为是考试的题,我们就用解决考试题的思路来解决这道题
- 读懂题意
- 求出所有i×ki的异或和,注意是先相乘再异或和
- 了解所有合法字符串的样子:()、(())、()()
- 分析数据范围
看到特殊性质一栏,发现有接近一半的数据满足“fi=i-1”,这是链啊!!相对于直接处理复杂的树,先将链的部分分拿到岂不是更稳?
- 处理链的部分分
- 直接处理链好像也没什么思路,那我们就先用暴力来模拟处理链的情况,如下(思路就是纯枚举,纯暴力,20pts):
/* 接近O(N^4)的复杂度,只能跑过前四个点,20pts */
#include <bits/stdc++.h>
using namespace std;
int n,x,ans,sum[510005];
char op[510005];
string ops;
inline bool check(string s) { //check子函数,开栈来判断括号是否匹配
stack<char> fh;
for(register int i=0;i<s.length();i++) {
if(s[i]=='(') { //左括号直接入栈
fh.push(s[i]);
}
else if(s[i]==')') {
if(fh.empty()) return false; //如果栈为空且当前为有括号,肯定不合法
if(!fh.empty()&&fh.top()=='(') fh.pop(); //如果栈顶为左括号,则一一对应,直接弹出
}
}
if(fh.empty()) return true; //如果为空,说明合法;反之,不合法
else return false;
}
int main() {
scanf("%d",&n);
scanf("%s",op+1);
for(register int i=2;i<=n;i++) { //因为只处理链的情况,所以父亲节点可以不管
scanf("%d",&x);
}
for(register int i=1;i<=n;i++) { //表示从根节点到第i号节点
for(register int l=1;l<i;l++) { //枚举字符串
ops=op[l]; //注意初始化
if(op[l]==')') continue; //一开始就是左括号,肯定不合法
for(register int r=l+1;r<=i;r++) {
ops+=op[r];
if(ops.length()%2!=0) continue; //长度为奇数,肯定不合法
if(check(ops)==true) sum[i]++; //sum[i]存储到i号节点有多少个合法字符串
}
}
}
for(register int i=1;i<=n;i++) { //根据题目要求求出答案
ans=ans xor (sum[i]*i);
}
printf("%d",ans);
return 0;
}
- 上面的代码的问题就在于枚举每一条路上的所有字符串太浪费时间,所以我们需要找递推式,使得计算每个点以前的合法字符串更简便,先来手推:
我们设每个节点及以前的单个合法字符串'()'为now
再设全部合法字符串'(())'或'()()'为sum
例1:
字符串:()()
节点:1 2 3 4
每个节点的now:0 1 0 2
每个节点的sum:0 1 1 3
例2:
字符串:()(())
节点:1 2 3 4 5 6
now:0 1 0 0 1 2
sum:0 1 1 1 2 4
我们发现:一个后括号如果能匹配一个前括号,假设这个前括号的前1位同样有一个已经匹配了的后括号,那么我们可以把当前的匹配和之前的匹配序列合并,当前now,其实就等于前面那个后括号的now+1
计算出了now,那sum就很容易得出:sum[i]=sum[i-1]+now[i]
/* 跑过链的全部分:1-7点和11-14点 , 55pts*/
#include <bits/stdc++.h>
using namespace std;
int n,x;
long long ans,sum[510005],now[510005]; //注意开long long
char op[510005];
stack<int> fh; //注意一下现在栈的类型是int(因为存的下标)
int main() {
scanf("%d",&n);
scanf("%s",op+1);
for(register int i=2;i<=n;i++) { //依旧没有用
scanf("%d",&x);
}
for(register int i=1;i<=n;i++) {
if(op[i]==')') {
if(!fh.empty()) {
int t=fh.top(); //如果栈不为空则满足now的条件,找到最近的合法括号下标
fh.pop();
now[i]=now[t-1]+1; //核心递推式1
}
}
else if(op[i]=='(') fh.push(i); //存放的是下标
sum[i]=sum[i-1]+now[i]; //核心递推式2
}
for(register int i=1;i<=n;i++) {
ans=ans xor (sum[i]*i);
}
printf("%lld",ans);
return 0;
}
- 树
解决了链的所有分,我们尝试解决树的情况,因为树可以看做很多条链组成,所以我们在链的核心思路上进行进一步思考
- 链的父亲就是当前节点i的前一个,但是在树上父亲编号不一定就是i-1,所以我们要想办法转换求now和sum值的递推式
解决:now是从上一个合法括号递推而来,在链中是now[t-1],在树中就是t的父亲节点now[fa[t]];sum同理,将sum[i-1]改成sum[fa[i]]即可
- 链直接遍历1~n即可,但是树的遍历是从父亲到儿子的递归,所以我们要知道怎么遍历
解决:记录每个点的父亲节点,再建图
- 因为在树上,每个父亲可能有多个儿子,那么就会涉及到每一次dfs后栈的状态问题,所以我们还需要回溯栈的状态
解决:如果当前括号为右括号,则回溯时需要重新压入;如果是左括号,则弹出
/* 满分Code */
#include <bits/stdc++.h>
using namespace std;
stack<int> fh;
char op[510005];
int n,x,tot,head[510005];
long long ans,fa[510005],sum[510005],now[510005];
struct node {
int to,net;
} a[510005];
inline void add(int u,int v) {
a[++tot].to=v;
a[tot].net=head[u];
head[u]=tot;
}
inline void dfs(int x) {
int t=0;
if(op[x]==')') {
if(!fh.empty()) {
t=fh.top();
fh.pop();
now[x]=now[fa[t]]+1;
}
}
else if(op[x]=='(') fh.push(x);
sum[x]=sum[fa[x]]+now[x];
for(register int i=head[x];i;i=a[i].net) { //邻接表存图遍历
int v=a[i].to;
dfs(v);
}
if(t!=0) fh.push(t); //注意不能直接判断栈是否为空来压入
else if(op[x]=='(') fh.pop();
}
int main() {
scanf("%d",&n);
scanf("%s",op+1);
for(register int i=2;i<=n;i++) {
scanf("%d",&x);
fa[i]=x; //记录父亲节点
add(x,i); //建边
}
dfs(1);
for(register int i=1;i<=n;i++) {
ans=ans xor (sum[i]*(long long)i);
}
printf("%lld",ans);
return 0;
}
后序:
啊...终于解决了这道题,来总结一下:
-
敲代码前一定要确保读懂题意,否则会错得不知所以然
-
不要上来就想满分思路(当然你强就想怎么就怎么,蒟蒻在线卑微QAQ),分析数据范围和数据特性,从简单的情况开始做
-
一定要多手推,不要怕麻烦,说不定推着推着思路就出来了
本题的解决过程:暴力->链->树(正解)
写给自己:
去年考试简直是惨不忍睹...
对我本身算是一种毁灭性的打击吧,学OI的兴趣和信心一下子就全部丧失完了,且赛后一直处于滑铁卢的阴霾中,真的挺难受的
在多方开导下慢慢走出来,继续OI之路,但是内心还是一直在回避考试、害怕失败
嗯...希望自己能坚定OI的心,不管学的好还不好、不管别人怎么说,沿着自己的路一步一步地前进,努力终不会白费的