[BZOJ4545]DQS的Trie(广义SAM+离线+树状数组)
[BZOJ4545]DQS的Trie(广义SAM+离线+树状数组)
题面
一颗 Trie 树,q 次操作,操作有3种:
1.求这棵树上本质不同的子串数量
2.插入一个子树,保证总大小不超过 100000
3.询问一个字符串在 Trie 树上出现过多少次,保证所有询问串总长度不超过 100000
分析
不考虑插入,操作1就是问所有节点的len(x)−len(link(x))之和。操作3就是字符串在SAM上匹配到的节点,节点的right集合大小,即parent树子树大小(去掉复制出来的节点)之和。
SAM+LCT维护子树显然是可行的,但在考场上不一定能写出来。不妨把操作离线,把所有子树插入后的Trie树建出来,然后对它建广义后缀自动机。然后DFS整个parent树,记录DFS序,用来把子树和转化成区间和。容易发现虽然树的形态在变,但用最终树的DFS序来求和是等价的。于是可以用树状数组按照DFS序维护每个点的子树大小
接着遍历操作,发现操作2实际上就是最终自动机的一些节点由未出现变成出现的过程。首先按照DFS序单点加1,表示增加了一个出现的节点。然后沿着link往上跳,把节点由未出现标记成出现,跳到已经出现的节点位置。一边跳一边累加当前节点的len(x)−len(link(x))之和。由于每个点只会被标记一次,总复杂度是O(n)的。
对于询问,1操作直接输出累加的和。3操作在SAM上匹配,然后找到匹配到的节点,DFS序区间查询子树大小。
该离线做法不仅不容易写错,且常数比LCT小。
代码
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #include<vector> #include<queue> #define maxn 500000 #define maxc 26 using namespace std; typedef long long ll; template<typename T> void qread(T &x) { x=0; T sign=1; char c=getchar(); while(c<'0'||c>'9') { if(c=='-') sign=-1; c=getchar(); } while(c>='0'&&c<='9') { x=x*10+c-'0'; c=getchar(); } x=x*sign; } template<typename T> void qprint(T x) { if(x<0) { putchar('-'); qprint(-x); } else if(x==0) { putchar('0'); return; } else { if(x>=10) qprint(x/10); putchar('0'+x%10); } } int m; struct qtype { int opt; string qs;//离线记录询问串 vector<int>subtr;//修改子树 } q[maxn+5]; struct fenwick_tree { int sz; int c[maxn+5]; inline int lowbit(int x) { return x&(-x); } void add(int x,int val) { for(int i=x; i<=sz; i+=lowbit(i)) c[i]+=val; } int sum(int x) { int ans=0; for(int i=x; i>0; i-=lowbit(i)) ans+=c[i]; return ans; } int query(int l,int r) { return sum(r)-sum(l-1); } } B; struct SAM { #define len(x) (t[x].len) #define link(x) (t[x].link) struct node { int len; int link; int ch[maxc]; } t[maxn+5]; vector<int>E[maxn+5]; const int root=1; int ptr=1; int extend(int last,int c) { // if(t[last].ch[c]&&len(last)+1==len(t[last].ch[c])) return t[last].ch[c]; int p=last,cur=++ptr; len(cur)=len(last)+1; while(p&&!t[p].ch[c]) { t[p].ch[c]=cur; p=link(p); } // bool flag=0; // int clo; if(p==0) link(cur)=root; else { int q=t[p].ch[c]; if(len(p)+1==len(q)) link(cur)=q; else { // if(p==last) flag=1; int clo=++ptr; link(clo)=link(q); len(clo)=len(p)+1; for(int i=0; i<maxc; i++) t[clo].ch[i]=t[q].ch[i]; link(q)=link(cur)=clo; while(p&&t[p].ch[c]==q) { t[p].ch[c]=clo; p=link(p); } } } return cur; } int tim; int dfnl[maxn+5],dfnr[maxn+5]; void get_dfn(int x,int f) { dfnl[x]=++tim; for(int i=0; i<(int)E[x].size(); i++) { int y=E[x][i]; if(y!=f) get_dfn(y,x); } dfnr[x]=tim; } bool vis[maxn+5]; ll strcnt=0;//本质不同子串个数 void update(int x) { for(int y=x; y&&!vis[y]; y=link(y)) { //计算对子串个数的贡献 vis[y]=1; strcnt+=len(y)-len(link(y)); } B.add(dfnl[x],1); } ll query1() { return strcnt; } int query3(string &s) { int x=root; for(int i=0; i<(int)s.length(); i++) { int c=s[i]-'a'; if(!t[x].ch[c]) return 0; x=t[x].ch[c]; } return B.query(dfnl[x],dfnr[x]); } void build_fail() { for(int i=2; i<=ptr; i++) E[link(i)].push_back(i); get_dfn(1,0); B.sz=tim; } } T1; struct Trie { vector<pair<int,int> >E[maxn+5]; void add_edge(int u,int v,int w) { E[u].push_back(make_pair(v,w)); E[v].push_back(make_pair(u,w)); } int pos[maxn+5]; void bfs(int id,int s) {//bfs建广义SAM static bool vis[maxn+5]; queue<int>qu; qu.push(s); while(!qu.empty()) { int x=qu.front(); qu.pop(); vis[x]=1; if(x!=s) q[id].subtr.push_back(x);//根节点之前存在,不用更新 for(int i=0; i<(int)E[x].size(); i++) { int y=E[x][i].first; if(!vis[y]) { pos[y]=T1.extend(pos[x],E[x][i].second); qu.push(y); } } } } } T2; int main() { int id,n0,u,v,rt,sz; static char tmp[maxn+5]; qread(id); qread(n0); for(int i=1; i<n0; i++) { qread(u); qread(v); scanf("%s",tmp); T2.add_edge(u,v,tmp[0]-'a'); } T2.pos[1]=1; T2.bfs(0,1); qread(m); for(int i=1; i<=m; i++) { qread(q[i].opt); if(q[i].opt==2) { qread(rt); qread(sz); for(int j=1; j<sz; j++) { qread(u); qread(v); scanf("%s",tmp); T2.add_edge(u,v,tmp[0]-'a'); } T2.bfs(i,rt); } else if(q[i].opt==3) { scanf("%s",tmp); q[i].qs=string(tmp); } } T1.build_fail(); for(int j=0; j<(int)q[0].subtr.size(); j++) {//更新初始答案 int x=q[0].subtr[j]; T1.update(T2.pos[x]); } for(int i=1; i<=m; i++) { if(q[i].opt==1) { qprint(T1.query1()); putchar('\n'); } else if(q[i].opt==2) { for(int j=0; j<(int)q[i].subtr.size(); j++) { int x=q[i].subtr[j]; T1.update(T2.pos[x]); } }else{ qprint(T1.query3(q[i].qs)); putchar('\n'); } } }
版权声明:因为我是蒟蒻,所以请大佬和神犇们不要转载(有坑)的文章,并指出问题,谢谢
【推荐】还在用 ECharts 开发大屏?试试这款永久免费的开源 BI 工具!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 理解Rust引用及其生命周期标识(下)
· 从二进制到误差:逐行拆解C语言浮点运算中的4008175468544之谜
· .NET制作智能桌面机器人:结合BotSharp智能体框架开发语音交互
· 软件产品开发中常见的10个问题及处理方法
· .NET 原生驾驭 AI 新基建实战系列:向量数据库的应用与畅想
· 2025成都.NET开发者Connect圆满结束
· 后端思维之高并发处理方案
· 千万级大表的优化技巧
· 在 VS Code 中,一键安装 MCP Server!
· 10年+ .NET Coder 心语 ── 继承的思维:从思维模式到架构设计的深度解析