1 条题解
-
0
自动搬运
来自洛谷,原作者为

oyoham
~RESSURRECTED~搬运于
2025-08-24 22:24:43,当前版本为作者最后更新于2025-07-22 14:07:05,作者可能在搬运后再次修改,您可在原文处查看最新版自动搬运只会搬运当前题目点赞数最高的题解,您可前往洛谷题解查看更多
以下是正文
Problem
P6864
维护一个字符串S,支持变成(S)、变成S()以及撤销一次操作(保证该操作在被撤销前生效,可撤销撤销操作),求每次操作后的合法括号字串数量。Solution
题解中大部分讲的都是矩阵 的做法。但是我在模拟赛中乱搞得到一种更快的做法。
$$f_u=\left(\sum_{v\in son_u}f_v\right)+\frac{\lvert son_u\rvert\cdot\left(\lvert son_u\rvert+1\right)}{2} $$
我们考虑将原字串每一对括号视为一个节点,每个节点的父亲是在原串上最小的能够包含节点代表的括号的括号对应的节点(即 是 的父亲节点,当前仅当 对应的括号包含 对应的括号,且不存在任何括号被 包含且包含 ),形成一棵树。
这样,我们可以考虑树上 dp,我们记 为以 为根的树的答案, 代表 的儿子的集合,则有:其中前面的求和是子树内答案,后面的统计在这个括号内一层的括号的答案。 可以建一个虚拟根节点方便统计答案。
$$f_u=\left(\sum_{v\in son_u}f_v\right)+\frac{\sum_{v\in son_u}g_v\cdot\left(\left(\sum_{v\in son_u}g_v+1\right)\right)}{2}\\ g_u=1 $$
考虑操作S(),相当于自己建一个新的节点,向虚拟根节点连边。
考虑操作(S),可以将虚拟根节点当作这个节点,并新建一个新的虚拟根节点由该点连边。
考虑撤销操作,若该撤销使字符串少一对括号,相当于删除对应节点并将该节点所有子节点连到被删除节点处。若该撤销使字符串多一对括号(或者说恢复操作),相当于将从该店连到其父亲的子节点从其父亲中删去并连到该点上,再有该店连向其父亲。
为处理撤销操作,我们定义 代表该节点为他父节点贡献的子节点个数,则当该节点未删去时,有:当该节点删去时,有:
$$f_u=\sum_{v\in son_u}f_v\\ g_u=\sum_{v\in son_u}g_v $$我们可以在每次修改后进行整条到根节点路径的更新。时间平均复杂度 。
Optimization
首先,所有节点可以先将 给维护起来,这样就可以方便的删除一个节点。
$$s_u=\sum_{v\in son_u}g_v\\ g_u=\begin{cases}1&\text{u isn't deleted}\\s_u&\text{u is deleted}\end{cases} $$
其次,对于一次删除或恢复,可以先用栈维护该节点到根节点的路径,取消该路径上的贡献,更改后在恢复贡献,这样这个算法的时间平均复杂度就降到 了。
此外,我们发现,没有必要对每个节点维护 ,可以发现答案等于 ,我们可以单独维护一个答案变量。
有了上面的优化后,转移就只有这时我们发现,当一个节点未被删除时,它子树内的删数并不会影响到这个结点的祖先(因为这个的 值始终为 )这时,我们可以将之前维护的到根节点的链改为到最近的未被删除的点即可,可以发现,这条链的期望值不会很大(大概只有 左右),所以在随机数据下这个做法是 的。
Code
贴上我丑陋的代码(勿喷 qwq
#include<bits/stdc++.h> using namespace std; #define ll long long inline ll read(){ ll x=0; short f=1; char c=getchar(); while(c>57||c<48){ if(c==45) f=-1; else f=1; c=getchar(); } while(c<58&&c>47){ x=(x<<1)+(x<<3)+(c^48); c=getchar(); } return x*f; } inline void write(ll x){ if(x<0ll) putchar(45),x=~x+1; if(x>9ll) write(x/10); putchar(x%10|48); } int Node[200005],Id=2; int RT=1; int alive[200005],g[200005],sg[200005],fa[200005]; ll ans=0; int st[200005],tp; signed main(){ alive[1]=alive[2]=ans=g[1]=sg[1]=fa[2]=g[2]=1; int n=read(); for(int i=1;i<=n;i++){ int op=read(); if(op==1){ Node[i]=++Id; fa[Id]=RT; alive[Id]=1; g[Id]=1; sg[RT]++; ans+=sg[RT]; } if(op==2){ Node[i]=RT; RT=++Id; alive[RT]=1; sg[RT]=1; g[RT]=1; ans++; fa[Node[i]]=RT; } if(op==3){ int x=Node[i]=-Node[read()]; if(x<0){ x=-x; //delete node x away st[++tp]=x; while(!alive[x=fa[x]])st[++tp]=x; for(int i=tp;i;i--){ int o=st[i]; sg[fa[o]]-=g[o]; if(alive[fa[o]]){ ans-=1ll*sg[fa[o]]*g[o]+(1ll*g[o]*(g[o]+1)>>1); } else{ g[fa[o]]-=g[o]; } } x=-Node[i]; g[x]=sg[x]; ans-=sg[x]*(sg[x]+1)>>1; alive[x]=0; for(int i=1;i<=tp;i++){ int o=st[i]; if(alive[fa[o]]){ ans+=1ll*sg[fa[o]]*g[o]+(1ll*g[o]*(g[o]+1)>>1); } else{ g[fa[o]]+=g[o]; } sg[fa[o]]+=g[o]; } tp=0; } else{ //insert node x back st[++tp]=x; while(!alive[x=fa[x]])st[++tp]=x; for(int i=tp;i;i--){ int o=st[i]; sg[fa[o]]-=g[o]; if(alive[fa[o]]){ ans-=1ll*sg[fa[o]]*g[o]+(1ll*g[o]*(g[o]+1)>>1); } else{ g[fa[o]]-=g[o]; } } x=Node[i]; alive[x]=1; g[x]=1; ans+=1ll*sg[x]*(sg[x]+1)>>1; for(int i=1;i<=tp;i++){ int o=st[i]; if(alive[fa[o]]){ ans+=1ll*sg[fa[o]]*g[o]+(1ll*g[o]*(g[o]+1)>>1); } else{ g[fa[o]]+=g[o]; } sg[fa[o]]+=g[o]; } tp=0; } } write(ans);putchar(10); } }Extra
我在模拟赛赛时打完了如上优化,但还是能找到 Hack 数据,可按如下方法构造:
cout<<"200000\n"; for(int i=1;i<=66667;i++)cout<<"2\n"; for(int i=1;i<=66667;i++)cout<<"3 "<<66668-i<<"\n"; for(int i=133335;i<=200000;i++)cout<<"3 "<<i-1<<"\n";但是,这个算法在随机情况下是 的,足以高速的通过此题。
- 1
信息
- ID
- 4923
- 时间
- 1000ms
- 内存
- 250MiB
- 难度
- 6
- 标签
- (无)
- 递交数
- 0
- 已通过
- 0
- 上传者