CF117E 题解
题意简述
给出一个
题目分析
非常不错的“清新”树剖题。
我们先想一想如果是一棵树的话该怎么处理。这很明显是一个裸的树剖:两次 dfs 后再拿线段树维护,用每个线段树结点维护重链上的
回到原题。我们发现每次操作改变的路径可以分成三段:非环上的一段、环上的一段以及非环上的另外一段。考虑把环缩成一个点。如样例
那么非环上的修改就可以连起来变成一条路径,这样的修改就是上文说的树的情况。而剩下环上的部分则可以看作链,另开一棵线段树维护就好了。每次修改的要么是连续的
另外需要注意的是如果环上全都是
代码实现
点击折叠/展开
#include<bits/stdc++.h> using namespace std; int n,q,x,y,lx,ly,ans; int tot,hd[100010],nt[400010],v[400010],deg[400010]/*度*/;//建图有关 int d[100010],size[100010],fa[100010],son[100010];//树剖一轮 dfs 有关:深度、子树大小、父结点、重儿子 int top[100010],dfn[100010],cnt;//树剖二轮 dfs 有关:重链顶、dfs 序号 queue<int>que;//拓扑排序用的队列 bool vis[100010];//如果拓扑排序时出队 int num,cyc_rk[100010]/*环上点序号*/,id[100010]/*环上点序号对应的原点编号*/,rt_id[100010]/*离每个点最近的环上点*/; struct node { int l,r,cnt;//左右端点和 1 边数量 bool tag;//懒标记:是否取反 };//线段树结点 struct Segment_Tree { node tr[400010];//线段树 4 倍空间 void pushup(int p) { tr[p].cnt=tr[p<<1].cnt+tr[p<<1|1].cnt;//左右子结点更新父结点 } void addtag(int p) { tr[p].tag=!tr[p].tag;//标记取反 tr[p].cnt=tr[p].r-tr[p].l+1-tr[p].cnt;//答案更新 } void pushdown(int p) { if(tr[p].tag) { addtag(p<<1);//下传到左子结点 addtag(p<<1|1);//下传到右子节点 tr[p].tag=0;//清空标记 } } void build(int p,int l,int r) { tr[p].l=l,tr[p].r=r; tr[p].tag=0,tr[p].cnt=0; if(l==r) return; int mid=l+r>>1; build(p<<1,l,mid);//建左子结点 build(p<<1|1,mid+1,r);//建右子节点 } void change(int p,int l,int r) { if(l>r) return;//区间为空就不改了。 if(tr[p].l>=l&&tr[p].r<=r) { ans-=tr[p].r-tr[p].l+1-2*tr[p].cnt;//1 边增多了 tr[p].r-tr[p].l+1-2*tr[p].cnt 条,那么答案就减少这么多条。 addtag(p);//更新结点 return; } pushdown(p);//下传懒标记 int mid=tr[p].l+tr[p].r>>1; if(l<=mid) change(p<<1,l,r);//改左结点 if(r>mid) change(p<<1|1,l,r);//改右结点 pushup(p);//更新结点 } }tree/*缩点后的树*/,cyc/*环*/; void add(int x,int y) { deg[y]++; v[++tot]=y; nt[tot]=hd[x]; hd[x]=tot; }//加边 void dfs_cyc(int x)//深搜一遍环 { cyc_rk[x]=++num;//记录序号 id[num]=x; for(int i=hd[x];i;i=nt[i]) { int y=v[i]; if(!vis[y]&&y!=id[1]&&y!=id[cyc_rk[x]-1])//如果在环上,也不是刚刚搜到的那个或者重绕了一圈就搜下去 { dfs_cyc(y); break; } } } void dfs1(int x,int tp)//tp:离 x 最近的环上点 { size[x]=1; d[x]=d[fa[x]]+1;//更新深度 rt_id[x]=cyc_rk[tp];//离 x 最近的环上点 for(int i=hd[x];i;i=nt[i]) { int y=v[i]; if(vis[y]&&y!=fa[x]) { fa[y]=x;//更新父亲 dfs1(y,tp); size[x]+=size[y];//更新子树大小 if(size[y]>size[son[x]]) son[x]=y;//更新重儿子 } } } void dfs2(int x,int tp) { if(cyc_rk[x])//在环上就不搜了 return; top[x]=tp;//更新重链顶 dfn[x]=++cnt; if(!son[x])//搜到叶子了 return; dfs2(son[x],tp);//重儿子的重链顶不变 for(int i=hd[x];i;i=nt[i]) { int y=v[i]; if(y!=fa[x]&&y!=son[x]) dfs2(y,y);//非重儿子的重链顶是它本身 } } void change_range(int x,int y) { while(top[x]!=top[y]) { if(d[top[x]]<d[top[y]]) swap(x,y); tree.change(1,dfn[top[x]],dfn[x]);//更新一条重链 x=fa[top[x]]; } if(d[x]>d[y]) swap(x,y); tree.change(1,dfn[x]+1,dfn[y]);//边权不更新 LCA,dfn[x]+1 } int main() { scanf("%d%d",&n,&q); ans=n;//初始答案是 n for(int i=1;i<=n;i++) scanf("%d%d",&x,&y),add(x,y),add(y,x); for(int i=1;i<=n;i++) if(deg[i]==1) que.push(i);//叶子节点入队 while(!que.empty()) { int x=que.front(); vis[x]=1; que.pop(); for(int i=hd[x];i;i=nt[i]) { int y=v[i]; if(!vis[y]) { deg[y]--;//断边 if(deg[y]==1) que.push(y);//如果成叶子了就入队 } } } for(int i=1;i<=n;i++) if(!vis[i])//在环里 { dfs_cyc(i);//搜环 break; } for(int i=1;i<=n;i++) if(cyc_rk[i]) dfs1(i,i);//树剖第一次 dfs d[n+1]=1;//环缩成 n+1 号点 for(int i=1;i<=n;i++) if(cyc_rk[i]) { if(size[son[i]]>size[son[n+1]]) son[n+1]=son[i]; for(int j=hd[i];j;j=nt[j]) { int y=v[j]; if(vis[y]) fa[y]=n+1,add(y,n+1),add(n+1,y); } }//把环上所有点连接的非环上点都连到 n+1 点上 dfs2(n+1,n+1);//第二次 dfs tree.build(1,1,cnt+1);//树上点建线段树 cyc.build(1,1,n-cnt+1);//环上点建线段树 while(q--) { scanf("%d%d",&x,&y); change_range(vis[x]?x:n+1,vis[y]?y:n+1);//更新树上点(如果在环上就改 n+1) x=rt_id[x],y=rt_id[y]; lx=x,ly=y; if(x>y) swap(x,y); if(2*(y-x)<n-cnt+1)//环上路径长度小于环长度一半,直接改两端点中间那一段 cyc.change(1,x,y-1); else if(2*(y-x)>n-cnt+1)//环上路径长度大于环长度一半,改开头到小编号端点和大编号端点到结尾两段 cyc.change(1,1,x-1),cyc.change(1,y,n-cnt+1); else//环上路径长度正好等于环长度一半,分类讨论 { if(id[lx==1?n-cnt+1:lx-1]<id[lx==n-cnt+1?1:lx+1])//从 x 向小编号走字典序更小 { if(lx>ly) cyc.change(1,ly,lx-1); else cyc.change(1,1,lx-1),cyc.change(1,ly,n-cnt+1); } else//否则向大编号走 { if(lx>ly) cyc.change(1,1,ly-1),cyc.change(1,lx,n-cnt+1); else cyc.change(1,lx,ly-1); } } printf("%d\n",cyc.tr[1].cnt==n-cnt+1?ans+1:ans);//如果环上都是 1 边,那么答案 +1 } return 0; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!