CF117E 题解

题意简述

给出一个 n 个点的基环树,初始 n 条边的边权都是 0m 次操作,每次指定两个点 uv,把从 uv 的字典序最小的路径上的所有边的边权取反。每次操作输出当前由边权为 1 的边连接的极大连通块数量。

题目分析

非常不错的“清新”树剖题。

我们先想一想如果是一棵树的话该怎么处理。这很明显是一个裸的树剖:两次 dfs 后再拿线段树维护,用每个线段树结点维护重链上的 1 边数量 cnt,取反时直接把数量改为长度 rl+1 减掉原来的数量 cnt 即可。考虑每次把 0 边改成 1 边时,1 边连接的极大连通块数量会 1(新的 1 边把原来不连通的两个极大连通块连接起来了);相反,把 1 边改成 0 边时,极大连通块数量会 +1(把原来连通的一个极大连通块分成了两个)。那么让初始答案为 n(都是 0 边),每次操作在线段树修改时顺带统计一下 1 边变化量以更新答案就好了,实现也非常简单直观。

回到原题。我们发现每次操作改变的路径可以分成三段:非环上的一段、环上的一段以及非环上的另外一段。考虑把环缩成一个点。如样例 1 的图:

那么非环上的修改就可以连起来变成一条路径,这样的修改就是上文说的树的情况。而剩下环上的部分则可以看作链,另开一棵线段树维护就好了。每次修改的要么是连续的 1 段,要么首尾相接则是开头和结尾各 1 段。至于环上修改的区间首尾,就是离原始两个端点最近的环上点。具体实现细节看代码。

另外需要注意的是如果环上全都是 1 边答案要 +1。因为此时环上最后变成 1 的那条边贡献无效(这条边即使不变成 1 整个环同样连通)。

代码实现

点击折叠/展开
#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;
}
posted @   Hadtsti  阅读(5)  评论(0编辑  收藏  举报  
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
点击右上角即可分享
微信分享提示