CF117E 题解

题意简述

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

题目分析

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

我们先想一想如果是一棵树的话该怎么处理。这很明显是一个裸的树剖:两次 dfs 后再拿线段树维护,用每个线段树结点维护重链上的 $1$ 边数量 $cnt$,取反时直接把数量改为长度 $r-l+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 @ 2023-07-29 17:40  Hadtsti  阅读(1)  评论(0编辑  收藏  举报  来源