【JZOJ1418】【COCI2007】追捕盗贼(双联通分量)

Problem

  给出一个N(2<=N<=100000)个点的联通图,有E(1<=E<=500000)条双向边,Q(1<=Q<=300000)个询问,每个询问有两种: 

  1. 如果G1和G2间的边被删掉,A与B是否联通(保证A和B不同,G1和G2之间一定存在路);
  2. 如果点C被删掉,A与B是否联通(A、B、C互不相同)。

Solution

  这道题我刚看以为tarjan缩点+LCT。。。比赛快结束时才发现LCT不行。。。


  正解是双联通分量。
  我们先跑一遍tarjan,求出dfs序。对于询问1,设G1为深度较小的,那么当且仅当A与B中的一个在G2的子树内、另一个不在G1的子树内时,A与B才不联通。
  此题的关键就在于询问2了。
  首先,判断C是否是割点。如果不是,对联通性不会有任何影响。
  然后,我们要解决如何判断点C是否在搜索树中的A、B的路径上的问题。设A在C的子树中,设C到A的路径上的第二个点(即C的儿子)为 y A ,那么当且仅当B不在 y A 的子树中时,C才在A、B的路径上。
  这样已经可以判断C是否在A、B的路径上了,然而我还处理了一个 y B ,表示C到B的路径上的第二个点。这样的话,如果 y A = y B y A > 0 y B > 0 ,则说明C 不在 A、B的路径上,因为lca(A,B)都在 y A 的子树中了;而如果 y A = y B = 0 ,则C依然 不在 A、B的路径上,因为此时A、B与C分别都没有祖孙关系。


  然而,C在A、B的路径上是不联通的必要条件,但并非充要条件。换句话说,即使C在A、B的路径上,A、B也有可能联通。因为它们可以通过返祖边跳上去。
  当C为lca(A,B)时,我们要让A、B分别跳出C的子树。此时,A可以到达 y A 的子树中任意节点,B也可以到达 y B 的子树中任意节点;它们有可能从其中的任何一个节点通过一条返祖边跳出C。那么,囿于 l o w [ y A ] 是根据 y A 的子树中的low取min求的,所以只需判断 l o w [ y A ] 是否<dfn[C]。如果是,则A能跳得出去。B同理。
  上段是对于C是lca(A,B)的解法,如果A是B、C的祖先且C是B的祖先(即C在A、B中间),则我们只需将B跳出C;B是A的祖先的情况同理。


  现在讲一讲 y A y B 怎么求。
  发现没有要求在线,考虑离线做。
  我们在做tarjan时,对于每个A,我们遍历到它时将它的i(即它是第几个操作)压入相应的C的栈中(用vector存储);回溯到C时,我们清理一下C的栈,对于此时栈中的某个元素i,如果刚才我们走的是y(我们刚才从C遍历到y),则 y A [ i ] = y 。具体看代码。 y B 同理。


  时间复杂度: O ( N + E + Q )

Code

#include <cstdio>
#include <cctype>
#include <algorithm>
#include <vector>
#define fi first
#define se second
#define mp make_pair 
#define min(a,b) ((a)<(b)?(a):(b))
#define fo(i,a,b) for(i=a;i<=b;i++)
#define YES {printf("yes\n");continue;}//YES表示联通
#define NO {printf("no\n");continue;}//NO表示不联通
using namespace std;
typedef pair<int,int> P;//表示某个A或B。P.fi表示第几个操作,P.se表示是A(0)还是B(1)

const int N=1e5+1,E=10*N,maxq=3*N;
int i,n,e,x,y,tot,final[N],q,tp[maxq],A[maxq],B[maxq],C[maxq],D[maxq],yA[maxq],yB[maxq];
struct side
{
    int ne,to;
}a[E];

void read(int&x)
{
    char ch=getchar();x=0;
    for(;!isdigit(ch);ch=getchar());
    for(;isdigit(ch);x=(x<<3)+(x<<1)+(ch^48),ch=getchar());
}

inline void link(int x,int y)
{
    a[++tot].ne=final[x]; final[x]=tot; a[tot].to=y;
    a[++tot].ne=final[y]; final[y]=tot; a[tot].to=x;
}

int idx,dfn[N],low[N],Q[N],fat[N];
vector<int>b[2][N];//b[0][x]表示所有在点x的A的i(即它是第几个操作),b[1][x]表示在点x的B的i
vector<P>bC[N];//bC[x]表示所有以点x为C的操作
void tarjan(int x)
{   
    int i,j,y;

    fo(j,0,1)//j=0时是A,j=1时使B
        while(!b[j][x].empty())//看看所有在点x的A或B
        {
            i=b[j][x].back();
            if(dfn[C[i]])//必须要判断,因为dfn[C[i]]>0了C[i]才有可能是x的祖先
            bC[C[i]].push_back(mp(i,j));
            b[j][x].pop_back();
        }

    dfn[x]=low[x]=++idx;
    for(i=final[x];i;i=a[i].ne)
    {
        y=a[i].to;
        if(y!=fat[x])//求双联通分量不能重复经过某条边
            if(!dfn[y])
            {
                fat[y]=x; tarjan(y);
                low[x]=min(low[x],low[y]);

                while(!bC[x].empty())//清理x的栈
                {
                    P t=bC[x].back();
                    t.se?yB[t.fi]=y:yA[t.fi]=y;//t.se=1则表示t是个B,否则是个A
                    bC[x].pop_back();
                }
            }
            else    low[x]=min(low[x],dfn[y]);
    }
    Q[x]=idx;//计算x的子树范围
}

inline bool in(int x,int y){return dfn[y]<=dfn[x]&&dfn[x]<=Q[y];}//判断点x是否在点y的子树中

int main()
{
    read(n);read(e);
    fo(i,1,e)read(x),read(y),link(x,y);

    scanf("%d",&q);
    fo(i,1,q)
    {
        read(tp[i]);read(A[i]);read(B[i]);read(C[i]); 
        if(tp[i]==1)read(D[i]);
        else    b[0][A[i]].push_back(i),b[1][B[i]].push_back(i);//将每个A、B存进图中
    }

    fo(i,1,n)if(!dfn[i])tarjan(i);

    fo(i,1,q)
    {
        if(tp[i]==1)
        {
            if(C[i]==D[i])YES;
            if(dfn[C[i]]>dfn[D[i]])swap(C[i],D[i]);
            if(low[D[i]]==dfn[D[i]])
            {
                if(in(A[i],D[i])&&!in(B[i],D[i]))NO;
                if(in(B[i],D[i])&&!in(A[i],D[i]))NO;
            }
            YES;
        }

        if(yA[i]==yB[i])YES;
        if(yA[i]&&low[yA[i]]>=dfn[C[i]])NO;
        if(yB[i]&&low[yB[i]]>=dfn[C[i]])NO;
        YES;
    }
}

猜你喜欢

转载自blog.csdn.net/qq_36551189/article/details/80657330