POJ 3694 “缩点”/求割边+朴素求LCA+并查集路径压缩

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_41661919/article/details/85564080

题意:

     给一张无向图,q次操作,每次在指定两点之间添加一条路径,问添加上这条路径之后图中有多少条割边。

思路:

    首先我们知道,v-dcc缩点后图(就是一棵树)中的各点之间通过割边连接,且包含原图中所有割边,当我们在原图中的某两点之间添加一条路径之后,对应v-dcc缩点后图中 的两个点之间的路径上的边将不在是割边,其路径长度可以用LCA的方法来求。确定这些边不是割边后,需要作出标记,然后标记一下就可以了(如果朴素的由子节点向LCA走的话总的时间复杂度约为M+Q*N),(tarjan算法的复杂度差不多N+M)。因为被标记过的边可以不再被访问。所以我们可以用并查集优化,在从子节点向LCA走的时候,每标记一条边就将这条边的子节点与与其父节点合并。这样我们在再次标记该点之后路径时可以先得到该点所属于的v-dcc,然后并查集find(对应v-dcc的编号)就可以跳过那些已经被标记的边,直接到达还没有被标记的边的起点(这里注意判断跳多了越过LCA或者偏离向LCA走的情况)。

WA了几次:主要原因有这些小算法不熟练,理解不透彻,组合起来的时候量很多,容易混,其次有些地方的代码操作是重复的,如果处理不好(多写太多重复代码),会TLE。再就是从子节点往LCA走的过程容易出错了,因为涉及到要用并查集跳过许多边,所以容易跳过了,其次一个点连接着多条边,但怎么找出其连接父节点的那条边。

      自己的代码就是把那些小算法拼接了一下,网上题解相比做了如下优化:①不再单独存储缩点后的图(省出缩点的代码),直接在求v-dcc时用并查集将同一联通块内的点连接起来。就相当于他们之间的边都已经被标记为不是割边了。②查询时,一边求LCA一边合并不再是桥的那些点。(因为本身求LCA的过程本身就需要从两个子节点往上走一直到LCA,这里如果用倍增,求LCA是快了,但求完之后还要再重复走一次来记录非桥边,得不偿失)。

AC代码(自己的没有把握住算法本质的代码):

#include <cstdio>
#include <cmath>
#include <cstring>
#include <cstdlib>
#include <iostream>
#include<algorithm>
#include <set>
#include <queue>
#include <stack>
#include<vector>
#include<map>
#include<ctime>
#define ll long long
using namespace std;
const int N=100010;
const int M=100100*2*2;
int ans=0;
int head[N];
int ver[M];
int Next[M];
int dfn[N];
int low[N];//该点两个low值来源中最小的时间戳值
int n,m,tot,num;//边数、时间戳
bool bridge[M];
void add(int x,int y)
{
    ver[++tot]=y;
    Next[tot]=head[x];
    head[x]=tot;
}
void tarjan(int x,int in_edge)
{
    dfn[x]=low[x]=++num;//从上往下搜索时预处理x点的时间戳和回溯值
    for(int i=head[x];i;i=Next[i])
    {
        int y=ver[i];
        if(!dfn[y])
        {
            tarjan(y,i);
            low[x]=min(low[x],low[y]);//

            if(low[y]>dfn[x])bridge[i]=bridge[i^1]=true;//满足定理条件:割边
        }
        else if(i!=(in_edge^1))low[x]=min(low[x],dfn[y]);//low【x】值的两种来源之后一种,y通过一条非树边到达x
    }
}
int c[N];//点 所属于的联通块编号
int dcc;//联通块标号
void dfs(int x)
{
    c[x]=dcc;
    for(int i=head[x];i;i=Next[i])
    {
        int y=ver[i];
        if(c[y]||bridge[i])continue;
        dfs(y);
    }
}
int hc[N];//点的首条边,head
int vc[M];//边的终点ver
int nc[M];//Next数组
int tc;//缩点后的边数
void add_c(int x,int y)
{
    vc[++tc]=y;
    nc[tc]=hc[x];
    hc[x]=tc;
}

int f[N][20],d[N],t;
int dc[N];
int num2=0;
void bfs()//预处理,求深度、最短路,f数组;就是SPFA,时间复杂度O(NlogN)
{
    queue <int> q;
    while(q.size())q.pop();
    q.push(1);
    d[1]=1;
    while(q.size())
    {
        int x=q.front();
        dc[x]=++num2;//顺便求出缩点图各点的时间戳,用来明确前进方向
        q.pop();
        for(int i=hc[x];i;i=nc[i])
        {
            int y=vc[i];
            if(d[y])continue;
            d[y]=d[x]+1;   //求节点的深度;
            f[y][0]=x;
            for(int j=1;j<=t;++j)
            {
                f[y][j]=f[f[y][j-1]][j-1];//动态规划,y点 跳(2^j)步 能到达哪个点。
                //cout<<y<<" "<<j<<" "<<f[y][j]<<endl;
                q.push(y);
            }
        }
    }
}
int lca(int x,int y)//求LCA,时间复杂度:o(Log(N))
{
    if(d[x]>d[y])swap(x,y);
    for(int i=t;i>=0;--i)//倒着走******
        if(d[f[y][i]]>=d[x])y=f[y][i];
    if(x==y)return x;
    for(int i=t;i>=0;--i)
        if(f[x][i]!=f[y][i])x=f[x][i],y=f[y][i];//注意一下:x是不断变化的,而i整个一套下来;
    return f[x][0];//一直更新,直至x、y为目标点下面的那两个点;
}


int fat[N];
int find(int x)
{
    if(x==fat[x])return fat[x];
    return fat[x]=find(fat[x]);
}
void unionn(int x,int y)
{
    int fa=find(x),fb=find(y);
    if(fa!=fb)
    {
        fat[fa]=fb;
    }
}
int lc;
void solve(int x,int e)
{
    x=find(x);
  
    if(dc[x]<=dc[lc])return ;//注意当前点应该往哪个方向走
    //if(x==lc)return ;
    for(int i=hc[x];i;i=nc[i])
    {
        if(i==(e^1))continue;
        int y=vc[i];
        y=find(y);//跳过重复的边

        if(!(dc[y]<dc[x]))continue;//保证当前点顺着指向lca的方向走
        ans--;
        unionn(x,y);
        //cout<<".."<<endl;
        solve(y,i);


    }
}
int main()
{
    int cas=0;
    while(scanf("%d%d",&n,&m)&&n+m)
    {
        tot=1;
        tc=1;
        ans=0;
        num=num2=0;
        dcc=0;
        memset(Next,0,sizeof(Next));
        memset(head,0,sizeof(head));
        memset(nc,0,sizeof(nc));
        memset(hc,0,sizeof(hc));
        memset(dc,0,sizeof(dc));
        memset(dfn,0,sizeof(dfn));
        memset(bridge,0,sizeof(bridge));
        memset(low,0x3f,sizeof(low));
        memset(ver,0,sizeof(ver));
        memset(vc,0,sizeof(vc));

        memset(d,0,sizeof(d));//多组输入,这一堆很容易错,尤其像这种if中出现的
        memset(c,0,sizeof(c));
        for(int i=1;i<=m;++i)
        {
            int x,y;
            scanf("%d%d",&x,&y);
            add(x,y);
            add(y,x);
        }
        tarjan(1,-1);//求V-DCC
        for(int i=1;i<=n;++i)//并给每个v-dcc块标号
        {
            if(!c[i])//当前不属于任何联通块的点
            {
                ++dcc;//联通块的标号
                dfs(i);
            }
        }

        //缩点

        tc=1;
        for(int i=2;i<tot;i+=2)//i+=2,有区别和书上
        {
            int x=ver[i],y=ver[i^1];
            if(c[x]==c[y])continue;
            add_c(c[x],c[y]);
            add_c(c[y],c[x]);

        }
        t=(int)log(n)/log(2)+2;//倍增t
        bfs();//预处理f【】【】,d【】


        int q;
        scanf("%d",&q);
        ans=(tc-1)/2;//原图割边数
        for(int i=0;i<=dcc;++i)fat[i]=i;
        printf("Case %d:\n",++cas);
        while(q--)
        {
            int x,y;
            scanf("%d%d",&x,&y);
            x=c[x];//别把原图与缩点图弄混
            y=c[y];
            lc=lca(x,y);
            solve(x,-1);//更新ans(减小)
            solve(y,-1);

            printf("%d\n",ans);
        }
        printf("\n");

    }

    return 0;
}

大神的该题的真正代码:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
#define _rep(i,a,b) for(int i=(a);i<=(b);i++)
const int N=1e5+10;
const int M=2e5+10;
int ca,n,m,q,head[N],tot,fa[N],low[N],dfn[N],num,cnt,pre[N];
struct Edge{
    int v,nx;
}edge[M<<1];
inline void addedge(int u,int v)
{
    edge[tot].v=v;
    edge[tot].nx=head[u];
    head[u]=tot++;
}
int findfa(int x){return fa[x]==x?x:fa[x]=findfa(fa[x]);}
bool merge(int x,int y)
{
    int fx=findfa(x);
    int fy=findfa(y);
    if(fx==fy)return false;
    fa[fy]=fx;return true;
}
void tarjan(int u,int f)
{
    dfn[u]=low[u]=++num;
    int flag=0;
    for(int i=head[u];~i;i=edge[i].nx)
    {
        int v=edge[i].v;
        if(v==f&&!flag){flag=1;continue;}
        if(dfn[v]==-1)
        {
            pre[v]=u;
            tarjan(v,u);
            low[u]=min(low[u],low[v]);
            if(low[v]>dfn[u])cnt++;
            else merge(u,v);//求割边的同时处理处理原图中那些非桥边
        }
        else low[u]=min(low[u],dfn[v]);
    }
}
inline int lca(int u,int v)//求LCA的较朴素算法
{
    if(findfa(u)==findfa(v))return cnt;
    if(dfn[u]>dfn[v])swap(u,v);
    while(dfn[u]<dfn[v])
    {
        if(merge(pre[v],v))cnt--;
        v=pre[v];
    }
    while(u!=v)
    {
        if(merge(u,pre[u]))cnt--;//一边求LCA一边更新答案,“标记”非树边
        u=pre[u];
    }
    return cnt;
}
int main()
{
    //freopen("in.txt","r",stdin);
    int u,v;
    while(scanf("%d%d",&n,&m)&&n&&m)
    {
        printf("Case %d:\n",++ca);
        cnt=num=tot=0;
        memset(head,-1,sizeof(head));
        memset(dfn,-1,sizeof(dfn));
        memset(low,-1,sizeof(low));
        pre[1]=1;
        _rep(i,1,n)fa[i]=i;
        _rep(i,1,m)scanf("%d%d",&u,&v),addedge(u,v),addedge(v,u);
        tarjan(1,1);
        scanf("%d",&q);
        _rep(i,1,q)
        {
            scanf("%d%d",&u,&v);
            printf("%d\n",lca(u,v));
        }
        puts("");
    }
    return 0;
}

The end;

猜你喜欢

转载自blog.csdn.net/qq_41661919/article/details/85564080