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

题意:

     给一张无向图,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 
#include 
#include 
#include 
#include 
#include
#include 
#include 
#include 
#include
#include
#include
#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  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<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]

大神的该题的真正代码:

(不过有个小问题,他的路径压缩只是优化了查询两点是否在同一双联

通块内,但向lca走的过程没有用路径压缩优化)。

#include
#include
#include
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]

The end;

 

 

 

你可能感兴趣的:(并查集,时间戳,树的直径与最近公共祖先)